diff --git a/_ide_helper_custom.php b/_ide_helper_custom.php index bfe7b673dd79..747187a2d147 100644 --- a/_ide_helper_custom.php +++ b/_ide_helper_custom.php @@ -13,5 +13,10 @@ namespace Illuminate\Contracts\Mail { return true; } + + public function brevo_config(string $key) + { + return true; + } } } diff --git a/app/Console/Commands/CreateSingleAccount.php b/app/Console/Commands/CreateSingleAccount.php index eb9395c1b152..75258b35f3e7 100644 --- a/app/Console/Commands/CreateSingleAccount.php +++ b/app/Console/Commands/CreateSingleAccount.php @@ -11,53 +11,54 @@ namespace App\Console\Commands; -use App\DataMapper\ClientRegistrationFields; -use App\DataMapper\CompanySettings; -use App\DataMapper\FeesAndLimits; -use App\Events\Invoice\InvoiceWasCreated; -use App\Events\RecurringInvoice\RecurringInvoiceWasCreated; -use App\Factory\GroupSettingFactory; -use App\Factory\InvoiceFactory; -use App\Factory\InvoiceItemFactory; -use App\Factory\RecurringInvoiceFactory; -use App\Factory\SubscriptionFactory; -use App\Helpers\Invoice\InvoiceSum; -use App\Jobs\Company\CreateCompanyTaskStatuses; -use App\Libraries\MultiDB; -use App\Models\Account; -use App\Models\BankIntegration; -use App\Models\BankTransaction; -use App\Models\BankTransactionRule; +use stdClass; +use Carbon\Carbon; +use Faker\Factory; +use App\Models\Task; +use App\Models\User; +use App\Utils\Ninja; +use App\Models\Quote; use App\Models\Client; -use App\Models\ClientContact; -use App\Models\Company; -use App\Models\CompanyGateway; -use App\Models\CompanyToken; -use App\Models\Country; use App\Models\Credit; +use App\Models\Vendor; +use App\Models\Account; +use App\Models\Company; +use App\Models\Country; use App\Models\Expense; use App\Models\Invoice; use App\Models\Product; use App\Models\Project; -use App\Models\Quote; -use App\Models\RecurringInvoice; -use App\Models\Task; -use App\Models\TaskStatus; use App\Models\TaxRate; -use App\Models\User; -use App\Models\Vendor; +use App\Libraries\MultiDB; +use App\Models\TaskStatus; +use App\Models\CompanyToken; +use App\Models\ClientContact; use App\Models\VendorContact; -use App\Repositories\InvoiceRepository; -use App\Utils\Ninja; -use App\Utils\Traits\GeneratesCounter; +use App\Models\CompanyGateway; +use App\Factory\InvoiceFactory; +use App\Models\BankIntegration; +use App\Models\BankTransaction; use App\Utils\Traits\MakesHash; -use Carbon\Carbon; -use Faker\Factory; use Illuminate\Console\Command; -use Illuminate\Support\Facades\Cache; +use App\Models\RecurringInvoice; +use App\DataMapper\FeesAndLimits; +use App\DataMapper\ClientSettings; +use App\DataMapper\CompanySettings; +use App\Factory\InvoiceItemFactory; +use App\Helpers\Invoice\InvoiceSum; +use App\Models\BankTransactionRule; +use App\Factory\GroupSettingFactory; +use App\Factory\SubscriptionFactory; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Cache; +use App\Utils\Traits\GeneratesCounter; use Illuminate\Support\Facades\Schema; -use stdClass; +use App\Repositories\InvoiceRepository; +use App\Factory\RecurringInvoiceFactory; +use App\Events\Invoice\InvoiceWasCreated; +use App\DataMapper\ClientRegistrationFields; +use App\Jobs\Company\CreateCompanyTaskStatuses; +use App\Events\RecurringInvoice\RecurringInvoiceWasCreated; class CreateSingleAccount extends Command { @@ -951,7 +952,7 @@ class CreateSingleAccount extends Command } - if (config('ninja.testvars.paytrace.decrypted') && ($this->gateway == 'all' || $this->gateway == 'paytrace')) { + if (config('ninja.testvars.paytrace') && ($this->gateway == 'all' || $this->gateway == 'paytrace')) { $cg = new CompanyGateway(); $cg->company_id = $company->id; $cg->user_id = $user->id; @@ -960,7 +961,7 @@ class CreateSingleAccount extends Command $cg->require_billing_address = true; $cg->require_shipping_address = true; $cg->update_details = true; - $cg->config = encrypt(config('ninja.testvars.paytrace.decrypted')); + $cg->config = encrypt(config('ninja.testvars.paytrace')); $cg->save(); @@ -1015,6 +1016,85 @@ class CreateSingleAccount extends Command $cg->fees_and_limits = $fees_and_limits; $cg->save(); } + + if (config('ninja.testvars.eway') && ($this->gateway == 'all' || $this->gateway == 'eway')) { + $cg = new CompanyGateway(); + $cg->company_id = $company->id; + $cg->user_id = $user->id; + $cg->gateway_key = '944c20175bbe6b9972c05bcfe294c2c7'; + $cg->require_cvv = true; + $cg->require_billing_address = true; + $cg->require_shipping_address = true; + $cg->update_details = true; + $cg->config = encrypt(config('ninja.testvars.eway')); + $cg->save(); + + $gateway_types = $cg->driver()->gatewayTypes(); + + $fees_and_limits = new stdClass(); + $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits(); + + $cg->fees_and_limits = $fees_and_limits; + $cg->save(); + } + + + if (config('ninja.testvars.gocardless') && ($this->gateway == 'all' || $this->gateway == 'gocardless')) { + + $c_settings = ClientSettings::defaults(); + $c_settings->currency_id = '2'; + + $client = Client::factory()->create([ + 'user_id' => $user->id, + 'company_id' => $company->id, + 'name' => 'cypress', + 'country_id' => 826, + 'settings' => $c_settings + ]); + + $cg = new CompanyGateway(); + $cg->company_id = $company->id; + $cg->user_id = $user->id; + $cg->gateway_key = 'b9886f9257f0c6ee7c302f1c74475f6c'; + $cg->require_cvv = true; + $cg->require_billing_address = true; + $cg->require_shipping_address = true; + $cg->update_details = true; + $cg->config = encrypt(config('ninja.testvars.gocardless')); + $cg->save(); + + $gateway_types = $cg->driver($client)->gatewayTypes(); + + $fees_and_limits = new stdClass(); + $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits(); + + $cg->fees_and_limits = $fees_and_limits; + $cg->save(); + } + + if (config('ninja.testvars.forte') && ($this->gateway == 'all' || $this->gateway == 'forte')) { + $cg = new CompanyGateway(); + $cg->company_id = $company->id; + $cg->user_id = $user->id; + $cg->gateway_key = 'kivcvjexxvdiyqtj3mju5d6yhpeht2xs'; + $cg->require_cvv = true; + $cg->require_billing_address = true; + $cg->require_shipping_address = true; + $cg->update_details = true; + $cg->config = encrypt(config('ninja.testvars.forte')); + $cg->save(); + + $gateway_types = $cg->driver()->gatewayTypes(); + + $fees_and_limits = new stdClass(); + $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits(); + + $cg->fees_and_limits = $fees_and_limits; + $cg->save(); + } + + + } private function createRecurringInvoice(Client $client) diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index d2241f95b2f5..8ddd16ea362d 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -229,7 +229,7 @@ class CompanySettings extends BaseSettings public $require_quote_signature = false; //@TODO ben to confirm //email settings - public $email_sending_method = 'default'; //enum 'default','gmail','office365' 'client_postmark', 'client_mailgun', 'mailgun' //@implemented + public $email_sending_method = 'default'; //enum 'default','gmail','office365' 'client_postmark', 'client_mailgun', 'mailgun', 'client_brevo' //@implemented public $gmail_sending_user_id = '0'; //@implemented @@ -451,6 +451,8 @@ class CompanySettings extends BaseSettings public $mailgun_endpoint = 'api.mailgun.net'; //api.eu.mailgun.net + public $brevo_secret = ''; + public $auto_bill_standard_invoices = false; public $email_alignment = 'center'; // center , left, right @@ -497,7 +499,10 @@ class CompanySettings extends BaseSettings public $use_unapplied_payment = 'off'; //always, option, off //@implemented + public $enable_rappen_rounding = false; + public static $casts = [ + 'enable_rappen_rounding' => 'bool', 'use_unapplied_payment' => 'string', 'show_pdfhtml_on_mobile' => 'bool', 'payment_email_all_contacts' => 'bool', @@ -517,262 +522,263 @@ class CompanySettings extends BaseSettings 'show_task_item_description' => 'bool', 'allow_billable_task_items' => 'bool', 'accept_client_input_quote_approval' => 'bool', - 'custom_sending_email' => 'string', - 'show_paid_stamp' => 'bool', - 'show_shipping_address' => 'bool', - 'company_logo_size' => 'string', - 'show_email_footer' => 'bool', - 'email_alignment' => 'string', - 'auto_bill_standard_invoices' => 'bool', - 'postmark_secret' => 'string', - 'mailgun_secret' => 'string', - 'mailgun_domain' => 'string', - 'send_email_on_mark_paid' => 'bool', - 'vendor_portal_enable_uploads' => 'bool', - 'besr_id' => 'string', - 'qr_iban' => 'string', - 'email_subject_purchase_order' => 'string', - 'email_template_purchase_order' => 'string', - 'require_purchase_order_signature' => 'bool', - 'purchase_order_public_notes' => 'string', - 'purchase_order_terms' => 'string', - 'purchase_order_design_id' => 'string', - 'purchase_order_footer' => 'string', - 'purchase_order_number_pattern' => 'string', - 'page_numbering_alignment' => 'string', - 'page_numbering' => 'bool', - 'auto_archive_invoice_cancelled' => 'bool', - 'email_from_name' => 'string', - 'show_all_tasks_client_portal' => 'string', - 'entity_send_time' => 'int', - 'shared_invoice_credit_counter' => 'bool', - 'reply_to_name' => 'string', - 'hide_empty_columns_on_pdf' => 'bool', - 'enable_reminder_endless' => 'bool', - 'use_credits_payment' => 'string', - 'recurring_invoice_number_pattern' => 'string', - 'recurring_invoice_number_counter' => 'int', + 'custom_sending_email' => 'string', + 'show_paid_stamp' => 'bool', + 'show_shipping_address' => 'bool', + 'company_logo_size' => 'string', + 'show_email_footer' => 'bool', + 'email_alignment' => 'string', + 'auto_bill_standard_invoices' => 'bool', + 'postmark_secret' => 'string', + 'mailgun_secret' => 'string', + 'mailgun_domain' => 'string', + 'brevo_secret' => 'string', + 'send_email_on_mark_paid' => 'bool', + 'vendor_portal_enable_uploads' => 'bool', + 'besr_id' => 'string', + 'qr_iban' => 'string', + 'email_subject_purchase_order' => 'string', + 'email_template_purchase_order' => 'string', + 'require_purchase_order_signature' => 'bool', + 'purchase_order_public_notes' => 'string', + 'purchase_order_terms' => 'string', + 'purchase_order_design_id' => 'string', + 'purchase_order_footer' => 'string', + 'purchase_order_number_pattern' => 'string', + 'page_numbering_alignment' => 'string', + 'page_numbering' => 'bool', + 'auto_archive_invoice_cancelled' => 'bool', + 'email_from_name' => 'string', + 'show_all_tasks_client_portal' => 'string', + 'entity_send_time' => 'int', + 'shared_invoice_credit_counter' => 'bool', + 'reply_to_name' => 'string', + 'hide_empty_columns_on_pdf' => 'bool', + 'enable_reminder_endless' => 'bool', + 'use_credits_payment' => 'string', + 'recurring_invoice_number_pattern' => 'string', + 'recurring_invoice_number_counter' => 'int', 'client_portal_under_payment_minimum' => 'float', - 'auto_bill_date' => 'string', - 'primary_color' => 'string', - 'secondary_color' => 'string', - 'client_portal_allow_under_payment' => 'bool', - 'client_portal_allow_over_payment' => 'bool', - 'auto_bill' => 'string', - 'lock_invoices' => 'string', - 'client_portal_terms' => 'string', - 'client_portal_privacy_policy' => 'string', - 'client_can_register' => 'bool', - 'portal_design_id' => 'string', - 'late_fee_endless_percent' => 'float', - 'late_fee_endless_amount' => 'float', - 'auto_email_invoice' => 'bool', - 'reminder_send_time' => 'int', - 'email_sending_method' => 'string', - 'gmail_sending_user_id' => 'string', - 'counter_number_applied' => 'string', - 'quote_number_applied' => 'string', - 'email_subject_custom1' => 'string', - 'email_subject_custom2' => 'string', - 'email_subject_custom3' => 'string', - 'email_template_custom1' => 'string', - 'email_template_custom2' => 'string', - 'email_template_custom3' => 'string', - 'enable_reminder1' => 'bool', - 'enable_reminder2' => 'bool', - 'enable_reminder3' => 'bool', - 'num_days_reminder1' => 'int', - 'num_days_reminder2' => 'int', - 'num_days_reminder3' => 'int', - 'schedule_reminder1' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date) - 'schedule_reminder2' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date) - 'schedule_reminder3' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date) - 'late_fee_amount1' => 'float', - 'late_fee_amount2' => 'float', - 'late_fee_amount3' => 'float', - 'late_fee_percent1' => 'float', - 'late_fee_percent2' => 'float', - 'late_fee_percent3' => 'float', - 'endless_reminder_frequency_id' => 'integer', + 'auto_bill_date' => 'string', + 'primary_color' => 'string', + 'secondary_color' => 'string', + 'client_portal_allow_under_payment' => 'bool', + 'client_portal_allow_over_payment' => 'bool', + 'auto_bill' => 'string', + 'lock_invoices' => 'string', + 'client_portal_terms' => 'string', + 'client_portal_privacy_policy' => 'string', + 'client_can_register' => 'bool', + 'portal_design_id' => 'string', + 'late_fee_endless_percent' => 'float', + 'late_fee_endless_amount' => 'float', + 'auto_email_invoice' => 'bool', + 'reminder_send_time' => 'int', + 'email_sending_method' => 'string', + 'gmail_sending_user_id' => 'string', + 'counter_number_applied' => 'string', + 'quote_number_applied' => 'string', + 'email_subject_custom1' => 'string', + 'email_subject_custom2' => 'string', + 'email_subject_custom3' => 'string', + 'email_template_custom1' => 'string', + 'email_template_custom2' => 'string', + 'email_template_custom3' => 'string', + 'enable_reminder1' => 'bool', + 'enable_reminder2' => 'bool', + 'enable_reminder3' => 'bool', + 'num_days_reminder1' => 'int', + 'num_days_reminder2' => 'int', + 'num_days_reminder3' => 'int', + 'schedule_reminder1' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date) + 'schedule_reminder2' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date) + 'schedule_reminder3' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date) + 'late_fee_amount1' => 'float', + 'late_fee_amount2' => 'float', + 'late_fee_amount3' => 'float', + 'late_fee_percent1' => 'float', + 'late_fee_percent2' => 'float', + 'late_fee_percent3' => 'float', + 'endless_reminder_frequency_id' => 'integer', 'client_online_payment_notification' => 'bool', 'client_manual_payment_notification' => 'bool', - 'document_email_attachment' => 'bool', - 'enable_client_portal_password' => 'bool', - 'enable_email_markup' => 'bool', - 'enable_client_portal_dashboard' => 'bool', - 'enable_client_portal' => 'bool', - 'email_template_statement' => 'string', - 'email_subject_statement' => 'string', - 'signature_on_pdf' => 'bool', - 'quote_footer' => 'string', - 'page_size' => 'string', - 'page_layout' => 'string', - 'font_size' => 'int', - 'primary_font' => 'string', - 'secondary_font' => 'string', - 'hide_paid_to_date' => 'bool', - 'embed_documents' => 'bool', - 'all_pages_header' => 'bool', - 'all_pages_footer' => 'bool', - 'project_number_pattern' => 'string', - 'project_number_counter' => 'int', - 'task_number_pattern' => 'string', - 'task_number_counter' => 'int', - 'expense_number_pattern' => 'string', - 'expense_number_counter' => 'int', - 'recurring_expense_number_pattern' => 'string', - 'recurring_expense_number_counter' => 'int', - 'recurring_quote_number_pattern' => 'string', - 'recurring_quote_number_counter' => 'int', - 'vendor_number_pattern' => 'string', - 'vendor_number_counter' => 'int', - 'ticket_number_pattern' => 'string', - 'ticket_number_counter' => 'int', - 'payment_number_pattern' => 'string', - 'payment_number_counter' => 'int', - 'reply_to_email' => 'string', - 'bcc_email' => 'string', - 'pdf_email_attachment' => 'bool', - 'ubl_email_attachment' => 'bool', - 'email_style' => 'string', - 'email_style_custom' => 'string', - 'company_gateway_ids' => 'string', - 'address1' => 'string', - 'address2' => 'string', - 'city' => 'string', - 'company_logo' => 'string', - 'country_id' => 'string', - 'client_number_pattern' => 'string', - 'client_number_counter' => 'integer', - 'credit_number_pattern' => 'string', - 'credit_number_counter' => 'integer', - 'currency_id' => 'string', - 'custom_value1' => 'string', - 'custom_value2' => 'string', - 'custom_value3' => 'string', - 'custom_value4' => 'string', - 'custom_message_dashboard' => 'string', - 'custom_message_unpaid_invoice' => 'string', - 'custom_message_paid_invoice' => 'string', - 'custom_message_unapproved_quote' => 'string', - 'default_task_rate' => 'float', - 'email_signature' => 'string', - 'email_subject_invoice' => 'string', - 'email_subject_quote' => 'string', - 'email_subject_credit' => 'string', - 'email_subject_payment' => 'string', - 'email_subject_payment_partial' => 'string', - 'email_template_invoice' => 'string', - 'email_template_quote' => 'string', - 'email_template_credit' => 'string', - 'email_template_payment' => 'string', - 'email_template_payment_partial' => 'string', - 'email_subject_reminder1' => 'string', - 'email_subject_reminder2' => 'string', - 'email_subject_reminder3' => 'string', - 'email_subject_reminder_endless' => 'string', - 'email_template_reminder1' => 'string', - 'email_template_reminder2' => 'string', - 'email_template_reminder3' => 'string', - 'email_template_reminder_endless' => 'string', - 'inclusive_taxes' => 'bool', - 'invoice_number_pattern' => 'string', - 'invoice_number_counter' => 'integer', - 'invoice_design_id' => 'string', + 'document_email_attachment' => 'bool', + 'enable_client_portal_password' => 'bool', + 'enable_email_markup' => 'bool', + 'enable_client_portal_dashboard' => 'bool', + 'enable_client_portal' => 'bool', + 'email_template_statement' => 'string', + 'email_subject_statement' => 'string', + 'signature_on_pdf' => 'bool', + 'quote_footer' => 'string', + 'page_size' => 'string', + 'page_layout' => 'string', + 'font_size' => 'int', + 'primary_font' => 'string', + 'secondary_font' => 'string', + 'hide_paid_to_date' => 'bool', + 'embed_documents' => 'bool', + 'all_pages_header' => 'bool', + 'all_pages_footer' => 'bool', + 'project_number_pattern' => 'string', + 'project_number_counter' => 'int', + 'task_number_pattern' => 'string', + 'task_number_counter' => 'int', + 'expense_number_pattern' => 'string', + 'expense_number_counter' => 'int', + 'recurring_expense_number_pattern' => 'string', + 'recurring_expense_number_counter' => 'int', + 'recurring_quote_number_pattern' => 'string', + 'recurring_quote_number_counter' => 'int', + 'vendor_number_pattern' => 'string', + 'vendor_number_counter' => 'int', + 'ticket_number_pattern' => 'string', + 'ticket_number_counter' => 'int', + 'payment_number_pattern' => 'string', + 'payment_number_counter' => 'int', + 'reply_to_email' => 'string', + 'bcc_email' => 'string', + 'pdf_email_attachment' => 'bool', + 'ubl_email_attachment' => 'bool', + 'email_style' => 'string', + 'email_style_custom' => 'string', + 'company_gateway_ids' => 'string', + 'address1' => 'string', + 'address2' => 'string', + 'city' => 'string', + 'company_logo' => 'string', + 'country_id' => 'string', + 'client_number_pattern' => 'string', + 'client_number_counter' => 'integer', + 'credit_number_pattern' => 'string', + 'credit_number_counter' => 'integer', + 'currency_id' => 'string', + 'custom_value1' => 'string', + 'custom_value2' => 'string', + 'custom_value3' => 'string', + 'custom_value4' => 'string', + 'custom_message_dashboard' => 'string', + 'custom_message_unpaid_invoice' => 'string', + 'custom_message_paid_invoice' => 'string', + 'custom_message_unapproved_quote' => 'string', + 'default_task_rate' => 'float', + 'email_signature' => 'string', + 'email_subject_invoice' => 'string', + 'email_subject_quote' => 'string', + 'email_subject_credit' => 'string', + 'email_subject_payment' => 'string', + 'email_subject_payment_partial' => 'string', + 'email_template_invoice' => 'string', + 'email_template_quote' => 'string', + 'email_template_credit' => 'string', + 'email_template_payment' => 'string', + 'email_template_payment_partial' => 'string', + 'email_subject_reminder1' => 'string', + 'email_subject_reminder2' => 'string', + 'email_subject_reminder3' => 'string', + 'email_subject_reminder_endless' => 'string', + 'email_template_reminder1' => 'string', + 'email_template_reminder2' => 'string', + 'email_template_reminder3' => 'string', + 'email_template_reminder_endless' => 'string', + 'inclusive_taxes' => 'bool', + 'invoice_number_pattern' => 'string', + 'invoice_number_counter' => 'integer', + 'invoice_design_id' => 'string', // 'invoice_fields' => 'string', - 'invoice_taxes' => 'int', + 'invoice_taxes' => 'int', //'enabled_item_tax_rates' => 'int', - 'invoice_footer' => 'string', - 'invoice_labels' => 'string', - 'invoice_terms' => 'string', - 'credit_footer' => 'string', - 'credit_terms' => 'string', - 'name' => 'string', - 'payment_terms' => 'string', - 'payment_type_id' => 'string', - 'phone' => 'string', - 'postal_code' => 'string', - 'quote_design_id' => 'string', - 'credit_design_id' => 'string', - 'quote_number_pattern' => 'string', - 'quote_number_counter' => 'integer', - 'quote_terms' => 'string', - 'recurring_number_prefix' => 'string', - 'reset_counter_frequency_id' => 'integer', - 'reset_counter_date' => 'string', - 'require_invoice_signature' => 'bool', - 'require_quote_signature' => 'bool', - 'state' => 'string', - 'email' => 'string', - 'vat_number' => 'string', - 'id_number' => 'string', - 'tax_name1' => 'string', - 'tax_name2' => 'string', - 'tax_name3' => 'string', - 'tax_rate1' => 'float', - 'tax_rate2' => 'float', - 'tax_rate3' => 'float', - 'show_accept_quote_terms' => 'bool', - 'show_accept_invoice_terms' => 'bool', - 'timezone_id' => 'string', - 'valid_until' => 'string', - 'date_format_id' => 'string', - 'military_time' => 'bool', - 'language_id' => 'string', - 'show_currency_code' => 'bool', - 'send_reminders' => 'bool', - 'enable_client_portal_tasks' => 'bool', - 'auto_archive_invoice' => 'bool', - 'auto_archive_quote' => 'bool', - 'auto_convert_quote' => 'bool', - 'shared_invoice_quote_counter' => 'bool', - 'counter_padding' => 'integer', + 'invoice_footer' => 'string', + 'invoice_labels' => 'string', + 'invoice_terms' => 'string', + 'credit_footer' => 'string', + 'credit_terms' => 'string', + 'name' => 'string', + 'payment_terms' => 'string', + 'payment_type_id' => 'string', + 'phone' => 'string', + 'postal_code' => 'string', + 'quote_design_id' => 'string', + 'credit_design_id' => 'string', + 'quote_number_pattern' => 'string', + 'quote_number_counter' => 'integer', + 'quote_terms' => 'string', + 'recurring_number_prefix' => 'string', + 'reset_counter_frequency_id' => 'integer', + 'reset_counter_date' => 'string', + 'require_invoice_signature' => 'bool', + 'require_quote_signature' => 'bool', + 'state' => 'string', + 'email' => 'string', + 'vat_number' => 'string', + 'id_number' => 'string', + 'tax_name1' => 'string', + 'tax_name2' => 'string', + 'tax_name3' => 'string', + 'tax_rate1' => 'float', + 'tax_rate2' => 'float', + 'tax_rate3' => 'float', + 'show_accept_quote_terms' => 'bool', + 'show_accept_invoice_terms' => 'bool', + 'timezone_id' => 'string', + 'valid_until' => 'string', + 'date_format_id' => 'string', + 'military_time' => 'bool', + 'language_id' => 'string', + 'show_currency_code' => 'bool', + 'send_reminders' => 'bool', + 'enable_client_portal_tasks' => 'bool', + 'auto_archive_invoice' => 'bool', + 'auto_archive_quote' => 'bool', + 'auto_convert_quote' => 'bool', + 'shared_invoice_quote_counter' => 'bool', + 'counter_padding' => 'integer', //'design' => 'string', - 'website' => 'string', - 'pdf_variables' => 'object', - 'portal_custom_head' => 'string', - 'portal_custom_css' => 'string', - 'portal_custom_footer' => 'string', - 'portal_custom_js' => 'string', - 'client_portal_enable_uploads' => 'bool', - 'purchase_order_number_counter' => 'integer', + 'website' => 'string', + 'pdf_variables' => 'object', + 'portal_custom_head' => 'string', + 'portal_custom_css' => 'string', + 'portal_custom_footer' => 'string', + 'portal_custom_js' => 'string', + 'client_portal_enable_uploads' => 'bool', + 'purchase_order_number_counter' => 'integer', ]; public static $free_plan_casts = [ - 'currency_id' => 'string', - 'company_gateway_ids' => 'string', - 'address1' => 'string', - 'address2' => 'string', - 'city' => 'string', - 'company_logo' => 'string', - 'country_id' => 'string', - 'custom_value1' => 'string', - 'custom_value2' => 'string', - 'custom_value3' => 'string', - 'custom_value4' => 'string', - 'inclusive_taxes' => 'bool', - 'name' => 'string', - 'payment_terms' => 'string', - 'payment_type_id' => 'string', - 'phone' => 'string', - 'postal_code' => 'string', - 'state' => 'string', - 'email' => 'string', - 'vat_number' => 'string', - 'id_number' => 'string', - 'tax_name1' => 'string', - 'tax_name2' => 'string', - 'tax_name3' => 'string', - 'tax_rate1' => 'float', - 'tax_rate2' => 'float', - 'tax_rate3' => 'float', - 'timezone_id' => 'string', - 'date_format_id' => 'string', - 'military_time' => 'bool', - 'language_id' => 'string', - 'show_currency_code' => 'bool', - 'website' => 'string', - 'default_task_rate' => 'float', + 'currency_id' => 'string', + 'company_gateway_ids' => 'string', + 'address1' => 'string', + 'address2' => 'string', + 'city' => 'string', + 'company_logo' => 'string', + 'country_id' => 'string', + 'custom_value1' => 'string', + 'custom_value2' => 'string', + 'custom_value3' => 'string', + 'custom_value4' => 'string', + 'inclusive_taxes' => 'bool', + 'name' => 'string', + 'payment_terms' => 'string', + 'payment_type_id' => 'string', + 'phone' => 'string', + 'postal_code' => 'string', + 'state' => 'string', + 'email' => 'string', + 'vat_number' => 'string', + 'id_number' => 'string', + 'tax_name1' => 'string', + 'tax_name2' => 'string', + 'tax_name3' => 'string', + 'tax_rate1' => 'float', + 'tax_rate2' => 'float', + 'tax_rate3' => 'float', + 'timezone_id' => 'string', + 'date_format_id' => 'string', + 'military_time' => 'bool', + 'language_id' => 'string', + 'show_currency_code' => 'bool', + 'website' => 'string', + 'default_task_rate' => 'float', ]; /** @@ -848,7 +854,7 @@ class CompanySettings extends BaseSettings $company_settings = (object) get_class_vars(self::class); foreach ($company_settings as $key => $value) { - if (! property_exists($settings, $key)) { + if (!property_exists($settings, $key)) { $settings->{$key} = self::castAttribute($key, $company_settings->{$key}); } } @@ -881,7 +887,7 @@ class CompanySettings extends BaseSettings { $notification = new stdClass(); $notification->email = []; - $notification->email = ['invoice_sent_all','payment_success_all','payment_manual_all']; + $notification->email = ['invoice_sent_all', 'payment_success_all', 'payment_manual_all']; return $notification; } diff --git a/app/DataMapper/Settings/SettingsData.php b/app/DataMapper/Settings/SettingsData.php index 189864a27e47..14ddcf123f3a 100644 --- a/app/DataMapper/Settings/SettingsData.php +++ b/app/DataMapper/Settings/SettingsData.php @@ -213,7 +213,7 @@ class SettingsData public bool $show_accept_quote_terms = false; //@TODO ben to confirm - public string $email_sending_method = 'default'; // enum 'default', 'gmail', 'office365', 'client_postmark', 'client_mailgun' //@implemented + public string $email_sending_method = 'default'; // enum 'default', 'gmail', 'office365', 'client_postmark', 'client_mailgun' , 'client_brevo' //@implemented public string $gmail_sending_user_id = '0'; //@implemented @@ -433,6 +433,8 @@ class SettingsData public string $mailgun_endpoint = 'api.mailgun.net'; // api.eu.mailgun.net + public string $brevo_secret = ''; + public bool $auto_bill_standard_invoices = false; public string $email_alignment = 'center'; // center, left, right @@ -469,8 +471,8 @@ class SettingsData public function cast(mixed $object) { - if(is_array($object)) { - $object = (object)$object; + if (is_array($object)) { + $object = (object) $object; } if (is_object($object)) { @@ -478,9 +480,9 @@ class SettingsData try { settype($object->{$key}, gettype($this->{$key})); - } catch(\Exception | \Error | \Throwable $e) { + } catch (\Exception | \Error | \Throwable $e) { - if(property_exists($this, $key)) { + if (property_exists($this, $key)) { $object->{$key} = $this->{$key}; } else { unset($object->{$key}); @@ -506,11 +508,11 @@ class SettingsData public function toObject(): object { - return (object)$this->object; + return (object) $this->object; } public function toArray(): array { - return (array)$this->object; + return (array) $this->object; } } diff --git a/app/DataProviders/SMSNumbers.php b/app/DataProviders/SMSNumbers.php new file mode 100644 index 000000000000..0b777f477714 --- /dev/null +++ b/app/DataProviders/SMSNumbers.php @@ -0,0 +1,88398 @@ + 'item.line_total', 'gross_line_total' => 'item.gross_line_total', 'tax_amount' => 'item.tax_amount', + 'product_cost' => 'item.product_cost' ]; protected array $quote_report_keys = [ diff --git a/app/Filters/DesignFilters.php b/app/Filters/DesignFilters.php index 69c6cf60df55..45071226a161 100644 --- a/app/Filters/DesignFilters.php +++ b/app/Filters/DesignFilters.php @@ -58,7 +58,10 @@ class DesignFilters extends QueryFilters public function entities(string $entities = ''): Builder { - + + if(stripos($entities, 'statement') !== false) + $entities = 'client'; + if (strlen($entities) == 0 || str_contains($entities, ',')) { return $this->builder; } diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index c1b41dd4b7e2..bd03ca01d42a 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -329,7 +329,9 @@ class InvoiceFilters extends QueryFilters if($sort_col[0] == 'number') { // return $this->builder->orderByRaw('CAST(number AS UNSIGNED), number ' . $dir); - return $this->builder->orderByRaw('ABS(number) ' . $dir); + // return $this->builder->orderByRaw("number REGEXP '^[A-Za-z]+$',CAST(number as SIGNED INTEGER),CAST(REPLACE(number,'-','')AS SIGNED INTEGER) ,number"); + // return $this->builder->orderByRaw('ABS(number) ' . $dir); + return $this->builder->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . $dir); } return $this->builder->orderBy($sort_col[0], $dir); diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index d6379356cd11..f142ce0b9f4a 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -52,6 +52,7 @@ class InvoiceSum public InvoiceItemSum $invoice_items; + private $rappen_rounding = false; /** * Constructs the object with Invoice and Settings object. * @@ -63,8 +64,11 @@ class InvoiceSum if ($this->invoice->client) { $this->precision = $this->invoice->client->currency()->precision; + $this->rappen_rounding = $this->invoice->client->getSetting('enable_rappen_rounding'); } else { $this->precision = $this->invoice->vendor->currency()->precision; + $this->rappen_rounding = $this->invoice->vendor->getSetting('enable_rappen_rounding'); + } $this->tax_map = new Collection(); @@ -252,11 +256,20 @@ class InvoiceSum /* Set new calculated total */ $this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision); + if($this->rappen_rounding) + $this->invoice->amount = $this->roundRappen($this->invoice->amount); + $this->invoice->total_taxes = $this->getTotalTaxes(); return $this; } + + function roundRappen($value): float + { + return round($value / .05, 0) * .05; + } + public function getSubTotal() { return $this->sub_total; diff --git a/app/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index 3786b55f99e6..d7c8a75dbeae 100644 --- a/app/Helpers/Invoice/InvoiceSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceSumInclusive.php @@ -47,6 +47,8 @@ class InvoiceSumInclusive private $precision; + private $rappen_rounding = false; + public InvoiceItemSumInclusive $invoice_items; /** * Constructs the object with Invoice and Settings object. @@ -59,8 +61,10 @@ class InvoiceSumInclusive if ($this->invoice->client) { $this->precision = $this->invoice->client->currency()->precision; + $this->rappen_rounding = $this->invoice->client->getSetting('enable_rappen_rounding'); } else { $this->precision = $this->invoice->vendor->currency()->precision; + $this->rappen_rounding = $this->invoice->vendor->getSetting('enable_rappen_rounding'); } $this->tax_map = new Collection(); @@ -268,12 +272,22 @@ class InvoiceSumInclusive } /* Set new calculated total */ + /** @todo - rappen rounding here */ $this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision); + if($this->rappen_rounding) { + $this->invoice->amount = $this->roundRappen($this->invoice->amount); + } + $this->invoice->total_taxes = $this->getTotalTaxes(); return $this; } + + function roundRappen($value): float + { + return round($value / .05, 0) * .05; + } public function getSubTotal() { diff --git a/app/Http/Controllers/BrevoController.php b/app/Http/Controllers/BrevoController.php new file mode 100644 index 000000000000..e1dce175f485 --- /dev/null +++ b/app/Http/Controllers/BrevoController.php @@ -0,0 +1,72 @@ +has('token') && $request->get('token') == config('services.brevo.key')) { + ProcessBrevoWebhook::dispatch($request->all())->delay(10); + + return response()->json(['message' => 'Success'], 200); + } + + return response()->json(['message' => 'Unauthorized'], 403); + } +} diff --git a/app/Http/Controllers/CompanyGatewayController.php b/app/Http/Controllers/CompanyGatewayController.php index 6c58f090a248..8d69b3738a1c 100644 --- a/app/Http/Controllers/CompanyGatewayController.php +++ b/app/Http/Controllers/CompanyGatewayController.php @@ -11,26 +11,29 @@ namespace App\Http\Controllers; +use App\Models\Client; +use App\Libraries\MultiDB; +use Illuminate\Http\Response; +use App\Models\CompanyGateway; +use App\Utils\Traits\MakesHash; use App\DataMapper\FeesAndLimits; +use App\Jobs\Util\ApplePayDomain; +use Illuminate\Support\Facades\Cache; use App\Factory\CompanyGatewayFactory; use App\Filters\CompanyGatewayFilters; +use App\Repositories\CompanyRepository; +use Illuminate\Foundation\Bus\DispatchesJobs; +use App\Transformers\CompanyGatewayTransformer; +use App\PaymentDrivers\Stripe\Jobs\StripeWebhook; +use App\PaymentDrivers\CheckoutCom\CheckoutSetupWebhook; use App\Http\Requests\CompanyGateway\BulkCompanyGatewayRequest; -use App\Http\Requests\CompanyGateway\CreateCompanyGatewayRequest; -use App\Http\Requests\CompanyGateway\DestroyCompanyGatewayRequest; use App\Http\Requests\CompanyGateway\EditCompanyGatewayRequest; use App\Http\Requests\CompanyGateway\ShowCompanyGatewayRequest; +use App\Http\Requests\CompanyGateway\TestCompanyGatewayRequest; use App\Http\Requests\CompanyGateway\StoreCompanyGatewayRequest; +use App\Http\Requests\CompanyGateway\CreateCompanyGatewayRequest; use App\Http\Requests\CompanyGateway\UpdateCompanyGatewayRequest; -use App\Jobs\Util\ApplePayDomain; -use App\Models\Client; -use App\Models\CompanyGateway; -use App\PaymentDrivers\CheckoutCom\CheckoutSetupWebhook; -use App\PaymentDrivers\Stripe\Jobs\StripeWebhook; -use App\Repositories\CompanyRepository; -use App\Transformers\CompanyGatewayTransformer; -use App\Utils\Traits\MakesHash; -use Illuminate\Foundation\Bus\DispatchesJobs; -use Illuminate\Http\Response; +use App\Http\Requests\CompanyGateway\DestroyCompanyGatewayRequest; /** * Class CompanyGatewayController. @@ -52,6 +55,9 @@ class CompanyGatewayController extends BaseController private string $checkout_key = '3758e7f7c6f4cecf0f4f348b9a00f456'; + private string $forte_key = 'kivcvjexxvdiyqtj3mju5d6yhpeht2xs'; + + /** * CompanyGatewayController constructor. * @param CompanyRepository $company_repo @@ -225,6 +231,13 @@ class CompanyGatewayController extends BaseController StripeWebhook::dispatch($company_gateway->company->company_key, $company_gateway->id); } elseif($company_gateway->gateway_key == $this->checkout_key) { CheckoutSetupWebhook::dispatch($company_gateway->company->company_key, $company_gateway->id); + } elseif($company_gateway->gateway_key == $this->forte_key) { + + dispatch(function () use ($company_gateway) { + MultiDB::setDb($company_gateway->company->db); + $company_gateway->driver()->updateFees(); + })->afterResponse(); + } return $this->itemResponse($company_gateway); @@ -407,6 +420,13 @@ class CompanyGatewayController extends BaseController if($company_gateway->gateway_key == $this->checkout_key) { CheckoutSetupWebhook::dispatch($company_gateway->company->company_key, $company_gateway->fresh()->id); + }elseif($company_gateway->gateway_key == $this->forte_key){ + + dispatch(function () use ($company_gateway) { + MultiDB::setDb($company_gateway->company->db); + $company_gateway->driver()->updateFees(); + })->afterResponse(); + } return $this->itemResponse($company_gateway); @@ -535,4 +555,28 @@ class CompanyGatewayController extends BaseController return $this->listResponse(CompanyGateway::withTrashed()->company()->whereIn('id', $request->ids)); } + + public function test(TestCompanyGatewayRequest $request, CompanyGateway $company_gateway) + { + + return response()->json(['message' => $company_gateway->driver()->auth() ? 'true' : 'false'], 200); + + } + + public function importCustomers(TestCompanyGatewayRequest $request, CompanyGateway $company_gateway) + { + //Throttle here + // if (Cache::get("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}")) + // return response()->json(['message' => ctrans('texts.import_started')], 200); + + dispatch(function () use($company_gateway) { + MultiDB::setDb($company_gateway->company->db); + $company_gateway->driver()->importCustomers(); + })->afterResponse(); + + Cache::put("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}", true, 300); + + return response()->json(['message' => ctrans('texts.import_started')], 200); + } + } diff --git a/app/Http/Controllers/LogoutController.php b/app/Http/Controllers/LogoutController.php index 2889b777d0dd..423a382e71e1 100644 --- a/app/Http/Controllers/LogoutController.php +++ b/app/Http/Controllers/LogoutController.php @@ -63,8 +63,12 @@ class LogoutController extends BaseController $ct->company ->tokens() ->where('is_system', true) - ->forceDelete(); - + ->cursor() + ->each(function ($ct){ + $ct->token = \Illuminate\Support\Str::random(64); + $ct->save(); + }); + return response()->json(['message' => 'All tokens deleted'], 200); } } diff --git a/app/Http/Controllers/SmtpController.php b/app/Http/Controllers/SmtpController.php index edfc24c38689..629a92bc1b48 100644 --- a/app/Http/Controllers/SmtpController.php +++ b/app/Http/Controllers/SmtpController.php @@ -55,7 +55,17 @@ class SmtpController extends BaseController (new \Illuminate\Mail\MailServiceProvider(app()))->register(); try { - Mail::mailer('smtp')->to($user->email, $user->present()->name())->send(new TestMailServer('Email Server Works!', strlen($company->settings->custom_sending_email) > 1 ? $company->settings->custom_sending_email : $user->email)); + + $sending_email = (isset($company->settings->custom_sending_email) && stripos($company->settings->custom_sending_email, "@")) ? $company->settings->custom_sending_email : $user->email; + $sending_user = (isset($company->settings->email_from_name) && strlen($company->settings->email_from_name) > 2) ? $company->settings->email_from_name : $user->name(); + + $mailable = new TestMailServer('Email Server Works!', $sending_email); + $mailable->from($sending_email,$sending_user); + + Mail::mailer('smtp') + ->to($user->email, $user->present()->name()) + ->send($mailable); + } catch (\Exception $e) { app('mail.manager')->forgetMailers(); return response()->json(['message' => $e->getMessage()], 400); diff --git a/app/Http/Requests/BankIntegration/UploadBankIntegrationRequest.php b/app/Http/Requests/BankIntegration/UploadBankIntegrationRequest.php index 560a44d02c74..700e3e47daa8 100644 --- a/app/Http/Requests/BankIntegration/UploadBankIntegrationRequest.php +++ b/app/Http/Requests/BankIntegration/UploadBankIntegrationRequest.php @@ -33,15 +33,15 @@ class UploadBankIntegrationRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/BankTransaction/UploadBankTransactionRequest.php b/app/Http/Requests/BankTransaction/UploadBankTransactionRequest.php index 9c014cc008cb..c6b88410a929 100644 --- a/app/Http/Requests/BankTransaction/UploadBankTransactionRequest.php +++ b/app/Http/Requests/BankTransaction/UploadBankTransactionRequest.php @@ -33,15 +33,15 @@ class UploadBankTransactionRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/Client/StoreClientRequest.php b/app/Http/Requests/Client/StoreClientRequest.php index a0d807d7d6dd..d3b4d3982e0a 100644 --- a/app/Http/Requests/Client/StoreClientRequest.php +++ b/app/Http/Requests/Client/StoreClientRequest.php @@ -45,18 +45,18 @@ class StoreClientRequest extends Request $user = auth()->user(); if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } /* Ensure we have a client name, and that all emails are unique*/ diff --git a/app/Http/Requests/Client/UpdateClientRequest.php b/app/Http/Requests/Client/UpdateClientRequest.php index 3ffea3250f05..9ed1acde3fc3 100644 --- a/app/Http/Requests/Client/UpdateClientRequest.php +++ b/app/Http/Requests/Client/UpdateClientRequest.php @@ -44,15 +44,15 @@ class UpdateClientRequest extends Request $user = auth()->user(); if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } else { $rules['documents'] = 'bail|sometimes|array'; } diff --git a/app/Http/Requests/Client/UploadClientRequest.php b/app/Http/Requests/Client/UploadClientRequest.php index bf12d3c8df00..17e159b2ee37 100644 --- a/app/Http/Requests/Client/UploadClientRequest.php +++ b/app/Http/Requests/Client/UploadClientRequest.php @@ -33,15 +33,15 @@ class UploadClientRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/Company/UploadCompanyRequest.php b/app/Http/Requests/Company/UploadCompanyRequest.php index 2b85f6a1ff08..0e3a545e2d05 100644 --- a/app/Http/Requests/Company/UploadCompanyRequest.php +++ b/app/Http/Requests/Company/UploadCompanyRequest.php @@ -30,15 +30,15 @@ class UploadCompanyRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } return $rules; diff --git a/app/Http/Requests/CompanyGateway/TestCompanyGatewayRequest.php b/app/Http/Requests/CompanyGateway/TestCompanyGatewayRequest.php new file mode 100644 index 000000000000..8442dc91cd72 --- /dev/null +++ b/app/Http/Requests/CompanyGateway/TestCompanyGatewayRequest.php @@ -0,0 +1,49 @@ +user(); + + return $user->isAdmin(); + } + + public function rules() + { + + return [ + + ]; + } + + public function prepareForValidation() + { + $input = $this->all(); + + $this->replace($input); + } +} diff --git a/app/Http/Requests/Credit/StoreCreditRequest.php b/app/Http/Requests/Credit/StoreCreditRequest.php index c24005063d68..7520dd465060 100644 --- a/app/Http/Requests/Credit/StoreCreditRequest.php +++ b/app/Http/Requests/Credit/StoreCreditRequest.php @@ -47,17 +47,17 @@ class StoreCreditRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } /** @var \App\Models\User $user */ diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php index 7c0c3adc0274..d521ce39db58 100644 --- a/app/Http/Requests/Credit/UpdateCreditRequest.php +++ b/app/Http/Requests/Credit/UpdateCreditRequest.php @@ -49,17 +49,17 @@ class UpdateCreditRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('credits')->where('company_id', $user->company()->id)->ignore($this->credit->id)]; diff --git a/app/Http/Requests/Credit/UploadCreditRequest.php b/app/Http/Requests/Credit/UploadCreditRequest.php index 6e78de9cd4ef..3411ad44739e 100644 --- a/app/Http/Requests/Credit/UploadCreditRequest.php +++ b/app/Http/Requests/Credit/UploadCreditRequest.php @@ -33,15 +33,15 @@ class UploadCreditRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/Expense/UploadExpenseRequest.php b/app/Http/Requests/Expense/UploadExpenseRequest.php index d50f6af80df7..867cfc34390c 100644 --- a/app/Http/Requests/Expense/UploadExpenseRequest.php +++ b/app/Http/Requests/Expense/UploadExpenseRequest.php @@ -33,15 +33,15 @@ class UploadExpenseRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/GroupSetting/UploadGroupSettingRequest.php b/app/Http/Requests/GroupSetting/UploadGroupSettingRequest.php index 9a142a689029..baa92c483f5d 100644 --- a/app/Http/Requests/GroupSetting/UploadGroupSettingRequest.php +++ b/app/Http/Requests/GroupSetting/UploadGroupSettingRequest.php @@ -30,15 +30,15 @@ class UploadGroupSettingRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } return $rules; diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index 393ada465aa0..df893bed2716 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -44,17 +44,17 @@ class StoreInvoiceRequest extends Request $user = auth()->user(); if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.$user->company()->id.',is_deleted,0'; diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 42d32ef81479..d863c5323cd8 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -46,17 +46,17 @@ class UpdateInvoiceRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } // $rules['id'] = new LockedInvoiceRule($this->invoice); diff --git a/app/Http/Requests/Invoice/UploadInvoiceRequest.php b/app/Http/Requests/Invoice/UploadInvoiceRequest.php index 3f5b0f8b8bad..8ca57bd1edca 100644 --- a/app/Http/Requests/Invoice/UploadInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UploadInvoiceRequest.php @@ -33,15 +33,15 @@ class UploadInvoiceRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/Payment/StorePaymentRequest.php b/app/Http/Requests/Payment/StorePaymentRequest.php index 25b3b43500c6..1bb5808b82e8 100644 --- a/app/Http/Requests/Payment/StorePaymentRequest.php +++ b/app/Http/Requests/Payment/StorePaymentRequest.php @@ -123,17 +123,17 @@ class StorePaymentRequest extends Request ]; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } return $rules; diff --git a/app/Http/Requests/Payment/UpdatePaymentRequest.php b/app/Http/Requests/Payment/UpdatePaymentRequest.php index 52a3c29d895b..f4b90758adcf 100644 --- a/app/Http/Requests/Payment/UpdatePaymentRequest.php +++ b/app/Http/Requests/Payment/UpdatePaymentRequest.php @@ -52,17 +52,17 @@ class UpdatePaymentRequest extends Request } if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } return $rules; diff --git a/app/Http/Requests/Payment/UploadPaymentRequest.php b/app/Http/Requests/Payment/UploadPaymentRequest.php index bf8d88d44464..a52d0a68f098 100644 --- a/app/Http/Requests/Payment/UploadPaymentRequest.php +++ b/app/Http/Requests/Payment/UploadPaymentRequest.php @@ -33,15 +33,15 @@ class UploadPaymentRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/Product/StoreProductRequest.php b/app/Http/Requests/Product/StoreProductRequest.php index c53ad07edbea..8d696bc132a4 100644 --- a/app/Http/Requests/Product/StoreProductRequest.php +++ b/app/Http/Requests/Product/StoreProductRequest.php @@ -32,17 +32,17 @@ class StoreProductRequest extends Request public function rules() { if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['cost'] = 'sometimes|numeric'; diff --git a/app/Http/Requests/Product/UpdateProductRequest.php b/app/Http/Requests/Product/UpdateProductRequest.php index 0a76f6a4ab08..1572ddbc1b82 100644 --- a/app/Http/Requests/Product/UpdateProductRequest.php +++ b/app/Http/Requests/Product/UpdateProductRequest.php @@ -35,17 +35,17 @@ class UpdateProductRequest extends Request public function rules() { if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['cost'] = 'numeric'; diff --git a/app/Http/Requests/Product/UploadProductRequest.php b/app/Http/Requests/Product/UploadProductRequest.php index d5eaf1e7e391..ef6b6d49db82 100644 --- a/app/Http/Requests/Product/UploadProductRequest.php +++ b/app/Http/Requests/Product/UploadProductRequest.php @@ -32,15 +32,15 @@ class UploadProductRequest extends Request { $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/Project/StoreProjectRequest.php b/app/Http/Requests/Project/StoreProjectRequest.php index 3bc1bca1bc52..365cfaf8ea46 100644 --- a/app/Http/Requests/Project/StoreProjectRequest.php +++ b/app/Http/Requests/Project/StoreProjectRequest.php @@ -51,17 +51,17 @@ class StoreProjectRequest extends Request } if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } return $this->globalRules($rules); diff --git a/app/Http/Requests/Project/UpdateProjectRequest.php b/app/Http/Requests/Project/UpdateProjectRequest.php index cbcf6882cf89..a0d0eaf39dc5 100644 --- a/app/Http/Requests/Project/UpdateProjectRequest.php +++ b/app/Http/Requests/Project/UpdateProjectRequest.php @@ -48,17 +48,17 @@ class UpdateProjectRequest extends Request $rules['budgeted_hours'] = 'sometimes|numeric'; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } return $this->globalRules($rules); diff --git a/app/Http/Requests/Project/UploadProjectRequest.php b/app/Http/Requests/Project/UploadProjectRequest.php index 08d66f2b048b..6b8f9909b759 100644 --- a/app/Http/Requests/Project/UploadProjectRequest.php +++ b/app/Http/Requests/Project/UploadProjectRequest.php @@ -33,15 +33,15 @@ class UploadProjectRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php index 91564eab092d..2b28882b297e 100644 --- a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php @@ -54,17 +54,17 @@ class StorePurchaseOrderRequest extends Request $rules['line_items'] = 'array'; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['status_id'] = 'nullable|integer|in:1,2,3,4,5'; diff --git a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php index 66984cd29a51..55abc60559c4 100644 --- a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php @@ -56,17 +56,17 @@ class UpdatePurchaseOrderRequest extends Request $rules['is_amount_discount'] = ['boolean']; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['status_id'] = 'sometimes|integer|in:1,2,3,4,5'; diff --git a/app/Http/Requests/PurchaseOrder/UploadPurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/UploadPurchaseOrderRequest.php index c41de6989e62..9c5b4ed4f5ec 100644 --- a/app/Http/Requests/PurchaseOrder/UploadPurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/UploadPurchaseOrderRequest.php @@ -33,15 +33,15 @@ class UploadPurchaseOrderRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/Quote/StoreQuoteRequest.php b/app/Http/Requests/Quote/StoreQuoteRequest.php index 3e0a498610cc..59f3409abb2e 100644 --- a/app/Http/Requests/Quote/StoreQuoteRequest.php +++ b/app/Http/Requests/Quote/StoreQuoteRequest.php @@ -46,17 +46,17 @@ class StoreQuoteRequest extends Request $rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['number'] = ['nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)]; diff --git a/app/Http/Requests/Quote/UpdateQuoteRequest.php b/app/Http/Requests/Quote/UpdateQuoteRequest.php index 4644e5af691c..4e93c58d560e 100644 --- a/app/Http/Requests/Quote/UpdateQuoteRequest.php +++ b/app/Http/Requests/Quote/UpdateQuoteRequest.php @@ -43,17 +43,17 @@ class UpdateQuoteRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } diff --git a/app/Http/Requests/Quote/UploadQuoteRequest.php b/app/Http/Requests/Quote/UploadQuoteRequest.php index 3c5a4395d73e..d7883c8b6ae9 100644 --- a/app/Http/Requests/Quote/UploadQuoteRequest.php +++ b/app/Http/Requests/Quote/UploadQuoteRequest.php @@ -33,15 +33,15 @@ class UploadQuoteRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php index 3607edae7870..a334b97fbde0 100644 --- a/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php +++ b/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php @@ -57,15 +57,15 @@ class StoreRecurringExpenseRequest extends Request $rules['currency_id'] = 'bail|required|integer|exists:currencies,id'; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } return $this->globalRules($rules); diff --git a/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php index e36333566fda..80a5759f0f10 100644 --- a/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php +++ b/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php @@ -49,15 +49,15 @@ class UpdateRecurringExpenseRequest extends Request $rules['category_id'] = 'bail|nullable|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0'; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } return $this->globalRules($rules); diff --git a/app/Http/Requests/RecurringExpense/UploadRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/UploadRecurringExpenseRequest.php index fa6b04c92580..fb1d50b5f769 100644 --- a/app/Http/Requests/RecurringExpense/UploadRecurringExpenseRequest.php +++ b/app/Http/Requests/RecurringExpense/UploadRecurringExpenseRequest.php @@ -30,15 +30,15 @@ class UploadRecurringExpenseRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } return $rules; diff --git a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php index 7d69a2587e1d..94cd8caba951 100644 --- a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php @@ -46,17 +46,17 @@ class StoreRecurringInvoiceRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id; diff --git a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php index f59edc91d61a..8aa9da8ed5cd 100644 --- a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php @@ -45,17 +45,17 @@ class UpdateRecurringInvoiceRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['number'] = ['bail', 'sometimes', Rule::unique('recurring_invoices')->where('company_id', $user->company()->id)->ignore($this->recurring_invoice->id)]; diff --git a/app/Http/Requests/RecurringInvoice/UploadRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UploadRecurringInvoiceRequest.php index 345e566434c4..e401ef6958b4 100644 --- a/app/Http/Requests/RecurringInvoice/UploadRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UploadRecurringInvoiceRequest.php @@ -33,15 +33,15 @@ class UploadRecurringInvoiceRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php index 0cb7ef56d378..12b2c4441081 100644 --- a/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php @@ -46,15 +46,15 @@ class StoreRecurringQuoteRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id; diff --git a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php index a0290e252744..d52f4b0974c7 100644 --- a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php @@ -38,15 +38,15 @@ class UpdateRecurringQuoteRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } if ($this->number) { diff --git a/app/Http/Requests/RecurringQuote/UploadRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/UploadRecurringQuoteRequest.php index 0631ec361579..3c8b91f96e0a 100644 --- a/app/Http/Requests/RecurringQuote/UploadRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/UploadRecurringQuoteRequest.php @@ -30,15 +30,15 @@ class UploadRecurringQuoteRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } return $rules; diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index e70c7f6b3a55..3f7c3f23008e 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -20,7 +20,7 @@ class Request extends FormRequest use MakesHash; use RuntimeFormRequest; - protected $file_validation = 'sometimes|file|mimes:png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx,webp,xml,zip,csv,ods,odt,odp|max:100000'; + protected $file_validation = 'sometimes|file|max:100000|mimes:png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx,webp,xml,zip,csv,ods,odt,odp'; /** * Get the validation rules that apply to the request. * @@ -31,6 +31,15 @@ class Request extends FormRequest return []; } + public function fileValidation() + { + if(config('ninja.upload_extensions')) + return $this->file_validation. ",".config('ninja.upload_extensions'); + + return $this->file_validation; + + } + public function globalRules($rules) { $merge_rules = []; diff --git a/app/Http/Requests/Task/StoreTaskRequest.php b/app/Http/Requests/Task/StoreTaskRequest.php index e5fee49132a0..66577a23d97c 100644 --- a/app/Http/Requests/Task/StoreTaskRequest.php +++ b/app/Http/Requests/Task/StoreTaskRequest.php @@ -79,17 +79,17 @@ class StoreTaskRequest extends Request }]; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } diff --git a/app/Http/Requests/Task/UpdateTaskRequest.php b/app/Http/Requests/Task/UpdateTaskRequest.php index 299905d8cd58..c71c7f0452e0 100644 --- a/app/Http/Requests/Task/UpdateTaskRequest.php +++ b/app/Http/Requests/Task/UpdateTaskRequest.php @@ -85,17 +85,17 @@ class UpdateTaskRequest extends Request }]; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } return $this->globalRules($rules); diff --git a/app/Http/Requests/Task/UploadTaskRequest.php b/app/Http/Requests/Task/UploadTaskRequest.php index 99737bd71536..4415cf8a995e 100644 --- a/app/Http/Requests/Task/UploadTaskRequest.php +++ b/app/Http/Requests/Task/UploadTaskRequest.php @@ -33,15 +33,15 @@ class UploadTaskRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/Requests/Vendor/StoreVendorRequest.php b/app/Http/Requests/Vendor/StoreVendorRequest.php index 7cb6fc14f193..3c282aeaeaf9 100644 --- a/app/Http/Requests/Vendor/StoreVendorRequest.php +++ b/app/Http/Requests/Vendor/StoreVendorRequest.php @@ -61,17 +61,17 @@ class StoreVendorRequest extends Request $rules['currency_id'] = 'bail|required|exists:currencies,id'; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['language_id'] = 'bail|nullable|sometimes|exists:languages,id'; diff --git a/app/Http/Requests/Vendor/UpdateVendorRequest.php b/app/Http/Requests/Vendor/UpdateVendorRequest.php index b06b861adfe6..3049a9c2d1b6 100644 --- a/app/Http/Requests/Vendor/UpdateVendorRequest.php +++ b/app/Http/Requests/Vendor/UpdateVendorRequest.php @@ -62,17 +62,17 @@ class UpdateVendorRequest extends Request $rules['currency_id'] = 'bail|sometimes|exists:currencies,id'; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); }else { $rules['documents'] = 'bail|sometimes|array'; } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['language_id'] = 'bail|nullable|sometimes|exists:languages,id'; diff --git a/app/Http/Requests/Vendor/UploadVendorRequest.php b/app/Http/Requests/Vendor/UploadVendorRequest.php index 80b6195c600d..826a5ed14d65 100644 --- a/app/Http/Requests/Vendor/UploadVendorRequest.php +++ b/app/Http/Requests/Vendor/UploadVendorRequest.php @@ -30,15 +30,15 @@ class UploadVendorRequest extends Request $rules = []; if ($this->file('documents') && is_array($this->file('documents'))) { - $rules['documents.*'] = $this->file_validation; + $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { - $rules['documents'] = $this->file_validation; + $rules['documents'] = $this->fileValidation(); } if ($this->file('file') && is_array($this->file('file'))) { - $rules['file.*'] = $this->file_validation; + $rules['file.*'] = $this->fileValidation(); } elseif ($this->file('file')) { - $rules['file'] = $this->file_validation; + $rules['file'] = $this->fileValidation(); } $rules['is_public'] = 'sometimes|boolean'; diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php index 0d74f646b31a..e8149d23bc7b 100644 --- a/app/Http/ValidationRules/Account/BlackListRule.php +++ b/app/Http/ValidationRules/Account/BlackListRule.php @@ -5,7 +5,7 @@ * @link https://github.com/invoiceninja/invoiceninja source repository * * @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com) - * + *1` * @license https://www.elastic.co/licensing/elastic-license */ @@ -21,6 +21,7 @@ class BlackListRule implements ValidationRule { /** Bad domains +/- dispoable email domains */ private array $blacklist = [ + 'wireconnected.com', 'secure-coinspot.com', 'casasotombo.com', 'otpku.com', diff --git a/app/Jobs/Bank/ProcessBankTransactionsNordigen.php b/app/Jobs/Bank/ProcessBankTransactionsNordigen.php index dd7c6c657b9e..8ff86efd14a5 100644 --- a/app/Jobs/Bank/ProcessBankTransactionsNordigen.php +++ b/app/Jobs/Bank/ProcessBankTransactionsNordigen.php @@ -85,6 +85,8 @@ class ProcessBankTransactionsNordigen implements ShouldQueue $this->bank_integration->company->notification(new GenericNinjaAdminNotification($content))->ninja(); + sleep(5); + throw $e; } if (!$this->nordigen_account) { diff --git a/app/Jobs/Brevo/ProcessBrevoWebhook.php b/app/Jobs/Brevo/ProcessBrevoWebhook.php new file mode 100644 index 000000000000..92959f7d2e8b --- /dev/null +++ b/app/Jobs/Brevo/ProcessBrevoWebhook.php @@ -0,0 +1,492 @@ + '', + 'subject' => 'Message not found.', + 'entity' => '', + 'entity_id' => '', + 'events' => [], + ]; + + private ?Company $company = null; + + /** + * Create a new job instance. + * + */ + public function __construct(private array $request) + { + } + + private function getSystemLog(string $message_id): ?SystemLog + { + return SystemLog::query() + ->where('company_id', $this->invitation->company_id) + ->where('type_id', SystemLog::TYPE_WEBHOOK_RESPONSE) + ->whereJsonContains('log', ['message-id' => $message_id]) + ->orderBy('id', 'desc') + ->first(); + + } + + private function updateSystemLog(SystemLog $system_log, array $data): void + { + $system_log->log = $data; + $system_log->save(); + } + + /** + * Execute the job. + * + * + * @return void + */ + public function handle() + { + MultiDB::findAndSetDbByCompanyKey($this->request['tags'][0]); + $this->company = Company::where('company_key', $this->request['tags'][0])->first(); + + $this->invitation = $this->discoverInvitation($this->request['message-id']); + + if ($this->company && $this->request['event'] == 'spam' && config('ninja.notification.slack')) { + $this->company->notification(new EmailSpamNotification($this->company))->ninja(); + } + + if (!$this->invitation) { + return; + } + + if (array_key_exists('reason', $this->request)) { + $this->invitation->email_error = $this->request['reason']; + } + + switch ($this->request['event']) { + case 'delivered': + return $this->processDelivery(); + case 'soft_bounce': + case 'hard_bounce': + case 'invalid_email': + case 'blocked': + + if ($this->request['subject'] == ctrans('texts.confirmation_subject')) { + $this->company->notification(new EmailBounceNotification($this->request['email']))->ninja(); + } + + return $this->processBounce(); + case 'spam': + return $this->processSpamComplaint(); + case 'unique_opened': + case 'opened': + case 'click': + return $this->processOpen(); + default: + # code... + break; + } + } + + // { + // "id": 948562, + // "email": "test@example.com", + // "message-id": "<202312211546.94160606300@smtp-relay.mailin.fr>", + // "date": "2023-12-21 18:34:42", + // "tags": [ + // "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV" + // ], + // "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]", + // "event": "unique_opened", + // "subject": "Reminder: Invoice 0002 from Untitled Company", + // "sending_ip": "74.125.208.8", + // "ts": 1703180082, + // "ts_epoch": 1703180082286, + // "ts_event": 1703180082, + // "link": "", + // "sender_email": "user@example.com" + // } + // { + // "id": 948562, + // "email": "test@example.com", + // "message-id": "<202312211555.14720890391@smtp-relay.mailin.fr>", + // "date": "2023-12-21 18:34:53", + // "tags": [ + // "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV" + // ], + // "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]", + // "event": "opened", + // "subject": "Reminder: Invoice 0002 from Untitled Company", + // "sending_ip": "74.125.208.8", + // "ts": 1703180093, + // "ts_epoch": 1703180093075, + // "ts_event": 1703180093, + // "link": "", + // "sender_email": "user@example.com" + // } + // { + // "id": 948562, + // "email": "paul@wer-ner.de", + // "message-id": "<202312280812.10968711117@smtp-relay.mailin.fr>", + // "date": "2023-12-28 09:20:18", + // "tags": [ + // "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV" + // ], + // "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]", + // "event": "click", + // "subject": "Reminder: Invoice 0002 from Untitled Company", + // "sending_ip": "79.235.133.157", + // "ts": 1703751618, + // "ts_epoch": 1703751618831, + // "ts_event": 1703751618, + // "link": "http://localhost/client/invoice/CssCvqOcKsenMCgYJ7EUNRZwxSDGUkau", + // "sender_email": "user@example.com" + // } + + private function processOpen() + { + $this->invitation->opened_date = now(); + $this->invitation->save(); + + $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + + $sl = $this->getSystemLog($this->request['message-id']); + + if ($sl) { + $this->updateSystemLog($sl, $data); + return; + } + + ( + new SystemLogger( + $data, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_OPENED, + SystemLog::TYPE_WEBHOOK_RESPONSE, + $this->invitation->contact->client, + $this->invitation->company + ) + )->handle(); + } + + // { + // "id": 948562, + // "email": "test@example", + // "message-id": "<202312211742.12697514322@smtp-relay.mailin.fr>", + // "date": "2023-12-21 18:42:31", + // "tags": [ + // "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV" + // ], + // "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]", + // "event": "delivered", + // "subject": "Reminder: Invoice 0002 from Untitled Company", + // "sending_ip": "77.32.148.26", + // "ts_event": 1703180551, + // "ts": 1703180551, + // "reason": "sent", + // "ts_epoch": 1703180551324, + // "sender_email": "user@example.com" + // } + private function processDelivery() + { + $this->invitation->email_status = 'delivered'; + $this->invitation->save(); + + $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + + $sl = $this->getSystemLog($this->request['message-id']); + + if ($sl) { + $this->updateSystemLog($sl, $data); + return; + } + + ( + new SystemLogger( + $data, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_DELIVERY, + SystemLog::TYPE_WEBHOOK_RESPONSE, + $this->invitation->contact->client, + $this->invitation->company + ) + )->handle(); + } + + // { + // "id": 948562, + // "email": "ryder36@example.net", + // "message-id": "<202312211744.55168080257@smtp-relay.mailin.fr>", + // "date": "2023-12-21 18:44:52", + // "tags": [ + // "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV" + // ], + // "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]", + // "event": "soft_bounce", + // "subject": "Reminder: Invoice 0001 from Untitled Company", + // "sending_ip": "77.32.148.26", + // "ts_event": 1703180692, + // "ts": 1703180692, + // "reason": "Unable to find MX of domain example.net", + // "ts_epoch": 1703180692382, + // "sender_email": "user@example.com" + // } + // { + // "id": 948562, + // "email": "gloria46@example.com", + // "message-id": "<202312211744.57456703957@smtp-relay.mailin.fr>", + // "date": "2023-12-21 18:44:54", + // "tags": [ + // "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV" + // ], + // "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]", + // "event": "hard_bounce", + // "subject": "Reminder: Invoice 0001 from Untitled Company", + // "sending_ip": "77.32.148.25", + // "ts_event": 1703180694, + // "ts": 1703180694, + // "reason": "blocked by Admin", + // "ts_epoch": 1703180694175, + // "sender_email": "user@example.com" + // } + // { + // "event" : "invalid_email", + // "email" : "example@example.com", + // "id" : 1, + // "date" : "yyyy-mm-dd hh:i:s", + // "message-id" : "", + // "subject" : "Test subject", + // "tag" : "",//json of array + // "tags": [ + // "company_key" + // ], + // "sending_ip" : "xxx.xx.xxx.xx", + // "ts_epoch" : 1534486682000, + // "template_id" : 1, + // "sender_email": "user@example.com", + // } + // { + // "id": 948562, + // "email": "neoma.langosh@example.com", + // "message-id": "<202312211745.65538701430@smtp-relay.mailin.fr>", + // "date": "2023-12-21 18:45:48", + // "tags": [ + // "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV" + // ], + // "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]", + // "event": "blocked", + // "subject": "Reminder: Invoice 0001 from Untitled Company", + // "ts_event": 1703180748, + // "ts": 1703180748, + // "reason": "blocked : due to blacklist user", + // "ts_epoch": 1703180748987, + // "sender_email": "user@example.com" + // } + + private function processBounce() + { + $this->invitation->email_status = 'bounced'; + $this->invitation->save(); + + $bounce = new EmailBounce( + $this->request['tags'][0], + $this->request['sender_email'], // TODO: @turbo124 is this the recipent? + $this->request['message-id'] + ); + + LightLogs::create($bounce)->send(); + + $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + + $sl = $this->getSystemLog($this->request['message-id']); + + if ($sl) { + $this->updateSystemLog($sl, $data); + return; + } + + (new SystemLogger($data, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company))->handle(); + + // if(config('ninja.notification.slack')) + // $this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja(); + } + + // { + // "event" : "spam", + // "email" : "example@example.com", + // "id" : 1, + // "date" : "yyyy-mm-dd hh:i:s", + // "message-id" : "", + // "tag" : "",//json of array + // "tags": [ + // "company_key" + // ], + // "sending_ip" : "xxx.xx.xxx.xx", + // "sender_email": "user@example.com", + // } + private function processSpamComplaint() + { + $this->invitation->email_status = 'spam'; + $this->invitation->save(); + + $spam = new EmailSpam( + $this->request['tags'][0], + $this->request['sender_email'], + $this->request['message-id'] + ); + + LightLogs::create($spam)->send(); + + $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + + $sl = $this->getSystemLog($this->request['message-id']); + + if ($sl) { + $this->updateSystemLog($sl, $data); + return; + } + + (new SystemLogger($data, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company))->handle(); + + if (config('ninja.notification.slack')) { + $this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja(); + } + } + + private function discoverInvitation(string $message_id) + { + $invitation = false; + + if ($invitation = InvoiceInvitation::where('message_id', $message_id)->first()) { + $this->entity = 'invoice'; + return $invitation; + } elseif ($invitation = QuoteInvitation::where('message_id', $message_id)->first()) { + $this->entity = 'quote'; + return $invitation; + } elseif ($invitation = RecurringInvoiceInvitation::where('message_id', $message_id)->first()) { + $this->entity = 'recurring_invoice'; + return $invitation; + } elseif ($invitation = CreditInvitation::where('message_id', $message_id)->first()) { + $this->entity = 'credit'; + return $invitation; + } elseif ($invitation = PurchaseOrderInvitation::where('message_id', $message_id)->first()) { + $this->entity = 'purchase_order'; + return $invitation; + } else { + return $invitation; + } + } + + public function getRawMessage(string $message_id) + { + + $brevo_secret = !empty($this->company->settings->brevo_secret) ? $this->company->settings->brevo_secret : config('services.brevo.key'); + + $brevo = new TransactionalEmailsApi(null, Configuration::getDefaultConfiguration()->setApiKey('api-key', $brevo_secret)); + $messageDetail = $brevo->getTransacEmailContent($message_id); + return $messageDetail; + + } + + + public function getBounceId(string $message_id): ?int + { + + $messageDetail = $this->getRawMessage($message_id); + + $event = collect($messageDetail->getEvents())->first(function ($event) { + + return $event?->Details?->BounceID ?? false; + + }); + + return $event?->Details?->BounceID ?? null; + + } + + // TODO + private function fetchMessage(): array + { + if (strlen($this->request['message-id']) < 1) { + return $this->default_response; + } + + try { + + $messageDetail = $this->getRawMessage($this->request['message-id']); + + $recipient = array_key_exists("email", $this->request) ? $this->request["email"] : ''; + $server_ip = array_key_exists("sending_ip", $this->request) ? $this->request["sending_ip"] : ''; + $delivery_message = array_key_exists("reason", $this->request) ? $this->request["reason"] : ''; + $subject = $messageDetail->getSubject() ?? ''; + + $events = collect($messageDetail->getEvents())->map(function (GetTransacEmailContentEvents $event) use ($recipient, $server_ip, $delivery_message) { // @turbo124 event does only contain name & time property, how to handle transformation?! + + return [ + 'bounce_id' => '', + 'recipient' => $recipient, + 'status' => $event->name ?? '', + 'delivery_message' => $delivery_message, // TODO: @turbo124 this results in all cases for the history in the string, which may be incorrect + 'server' => '', + 'server_ip' => $server_ip, + 'date' => \Carbon\Carbon::parse($event->getTime())->format('Y-m-d H:i:s') ?? '', + ]; + + })->toArray(); + + return [ + 'recipients' => $recipient, + 'subject' => $subject, + 'entity' => $this->entity ?? '', + 'entity_id' => $this->invitation->{$this->entity}->hashed_id ?? '', + 'events' => $events, + ]; + + } catch (\Exception $e) { + + return $this->default_response; + + } + } +} diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index f421667a31a4..bff1f3f5aa7c 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -62,6 +62,7 @@ class NinjaMailerJob implements ShouldQueue protected $client_mailgun_domain = false; + protected $client_brevo_secret = false; public function __construct(public ?NinjaMailerObject $nmo, public bool $override = false) { @@ -99,7 +100,7 @@ class NinjaMailerJob implements ShouldQueue } $this->nmo->mailable->replyTo($this->nmo->settings->reply_to_email, $reply_to_name); - } elseif(isset($this->nmo->invitation->user)) { + } elseif (isset ($this->nmo->invitation->user)) { $this->nmo->mailable->replyTo($this->nmo->invitation->user->email, $this->nmo->invitation->user->present()->name()); } else { $this->nmo->mailable->replyTo($this->company->owner()->email, $this->company->owner()->present()->name()); @@ -112,16 +113,16 @@ class NinjaMailerJob implements ShouldQueue /* If we have an invitation present, we pass the invitation key into the email headers*/ if ($this->nmo->invitation) { $this->nmo - ->mailable - ->withSymfonyMessage(function ($message) { - $message->getHeaders()->addTextHeader('x-invitation', $this->nmo->invitation->key); - }); + ->mailable + ->withSymfonyMessage(function ($message) { + $message->getHeaders()->addTextHeader('x-invitation', $this->nmo->invitation->key); + }); } //send email try { - nlog("Trying to send to {$this->nmo->to_user->email} ". now()->toDateTimeString()); - nlog("Using mailer => ". $this->mailer); + nlog("Trying to send to {$this->nmo->to_user->email} " . now()->toDateTimeString()); + nlog("Using mailer => " . $this->mailer); $mailer = Mail::mailer($this->mailer); @@ -133,10 +134,14 @@ class NinjaMailerJob implements ShouldQueue $mailer->mailgun_config($this->client_mailgun_secret, $this->client_mailgun_domain, $this->nmo->settings->mailgun_endpoint); } + if ($this->client_brevo_secret) { + $mailer->brevo_config($this->client_brevo_secret); + } + $mailable = $this->nmo->mailable; /** May need to re-build it here */ - if(Ninja::isHosted() && method_exists($mailable, 'build')) { + if (Ninja::isHosted() && method_exists($mailable, 'build')) { $mailable->build(); } @@ -149,15 +154,15 @@ class NinjaMailerJob implements ShouldQueue $this->incrementEmailCounter(); LightLogs::create(new EmailSuccess($this->nmo->company->company_key, $this->nmo->mailable->subject)) - ->send(); + ->send(); - } catch(\Symfony\Component\Mime\Exception\RfcComplianceException $e) { + } catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) { nlog("Mailer failed with a Logic Exception {$e->getMessage()}"); $this->fail(); $this->cleanUpMailers(); $this->logMailError($e->getMessage(), $this->company->clients()->first()); return; - } catch(\Symfony\Component\Mime\Exception\LogicException $e) { + } catch (\Symfony\Component\Mime\Exception\LogicException $e) { nlog("Mailer failed with a Logic Exception {$e->getMessage()}"); $this->fail(); $this->cleanUpMailers(); @@ -224,7 +229,7 @@ class NinjaMailerJob implements ShouldQueue private function incrementEmailCounter(): void { - if(in_array($this->mailer, ['default','mailgun'])) + if(in_array($this->mailer, ['default','mailgun','postmark'])) Cache::increment("email_quota".$this->company->account->key); } @@ -273,7 +278,7 @@ class NinjaMailerJob implements ShouldQueue // return $this; // } - if(Ninja::isHosted() && $this->company->account->isPaid() && $this->nmo->settings->email_sending_method == 'default') { + if (Ninja::isHosted() && $this->company->account->isPaid() && $this->nmo->settings->email_sending_method == 'default') { //check if outlook. try { @@ -281,7 +286,7 @@ class NinjaMailerJob implements ShouldQueue $domain = explode("@", $email)[1] ?? ""; $dns = dns_get_record($domain, DNS_MX); $server = $dns[0]["target"]; - if(stripos($server, "outlook.com") !== false) { + if (stripos($server, "outlook.com") !== false) { $this->mailer = 'postmark'; $this->client_postmark_secret = config('services.postmark-outlook.token'); @@ -293,13 +298,13 @@ class NinjaMailerJob implements ShouldQueue } $this->nmo - ->mailable - ->from(config('services.postmark-outlook.from.address'), $email_from_name); + ->mailable + ->from(config('services.postmark-outlook.from.address'), $email_from_name); return $this; } - } catch(\Exception $e) { - + } catch (\Exception $e) { + nlog("problem switching outlook driver - hosted"); nlog($e->getMessage()); } @@ -331,6 +336,10 @@ class NinjaMailerJob implements ShouldQueue $this->mailer = 'mailgun'; $this->setMailgunMailer(); return $this; + case 'client_brevo': + $this->mailer = 'brevo'; + $this->setBrevoMailer(); + return $this; case 'smtp': $this->mailer = 'smtp'; $this->configureSmtpMailer(); @@ -380,11 +389,11 @@ class NinjaMailerJob implements ShouldQueue } $user = $this->resolveSendingUser(); - $sending_email = (isset($this->nmo->settings->custom_sending_email) && stripos($this->nmo->settings->custom_sending_email, "@")) ? $this->nmo->settings->custom_sending_email : $user->email; + $sending_email = (isset ($this->nmo->settings->custom_sending_email) && stripos($this->nmo->settings->custom_sending_email, "@")) ? $this->nmo->settings->custom_sending_email : $user->email; $this->nmo - ->mailable - ->from($sending_email, $email_from_name); + ->mailable + ->from($sending_email, $email_from_name); } @@ -407,8 +416,8 @@ class NinjaMailerJob implements ShouldQueue if (env($this->company->id . '_MAIL_FROM_ADDRESS')) { $this->nmo - ->mailable - ->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME'))); + ->mailable + ->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME'))); } } } @@ -426,6 +435,8 @@ class NinjaMailerJob implements ShouldQueue $this->client_mailgun_domain = false; + $this->client_brevo_secret = false; + //always dump the drivers to prevent reuse app('mail.manager')->forgetMailers(); } @@ -475,8 +486,8 @@ class NinjaMailerJob implements ShouldQueue } $this->nmo - ->mailable - ->from(config('services.mailgun.from.address'), $email_from_name); + ->mailable + ->from(config('services.mailgun.from.address'), $email_from_name); } @@ -496,12 +507,35 @@ class NinjaMailerJob implements ShouldQueue $user = $this->resolveSendingUser(); - $sending_email = (isset($this->nmo->settings->custom_sending_email) && stripos($this->nmo->settings->custom_sending_email, "@")) ? $this->nmo->settings->custom_sending_email : $user->email; - $sending_user = (isset($this->nmo->settings->email_from_name) && strlen($this->nmo->settings->email_from_name) > 2) ? $this->nmo->settings->email_from_name : $user->name(); + $sending_email = (isset ($this->nmo->settings->custom_sending_email) && stripos($this->nmo->settings->custom_sending_email, "@")) ? $this->nmo->settings->custom_sending_email : $user->email; + $sending_user = (isset ($this->nmo->settings->email_from_name) && strlen($this->nmo->settings->email_from_name) > 2) ? $this->nmo->settings->email_from_name : $user->name(); $this->nmo - ->mailable - ->from($sending_email, $sending_user); + ->mailable + ->from($sending_email, $sending_user); + } + + /** + * Configures Brevo using client supplied secret + * as the Mailer + */ + private function setBrevoMailer() + { + if (strlen($this->nmo->settings->brevo_secret) > 2) { + $this->client_brevo_secret = $this->nmo->settings->brevo_secret; + } else { + $this->nmo->settings->email_sending_method = 'default'; + return $this->setMailDriver(); + } + + $user = $this->resolveSendingUser(); + + $sending_email = (isset ($this->nmo->settings->custom_sending_email) && stripos($this->nmo->settings->custom_sending_email, "@")) ? $this->nmo->settings->custom_sending_email : $user->email; + $sending_user = (isset ($this->nmo->settings->email_from_name) && strlen($this->nmo->settings->email_from_name) > 2) ? $this->nmo->settings->email_from_name : $user->name(); + + $this->nmo + ->mailable + ->from($sending_email, $sending_user); } /** @@ -519,12 +553,12 @@ class NinjaMailerJob implements ShouldQueue $user = $this->resolveSendingUser(); - $sending_email = (isset($this->nmo->settings->custom_sending_email) && stripos($this->nmo->settings->custom_sending_email, "@")) ? $this->nmo->settings->custom_sending_email : $user->email; - $sending_user = (isset($this->nmo->settings->email_from_name) && strlen($this->nmo->settings->email_from_name) > 2) ? $this->nmo->settings->email_from_name : $user->name(); + $sending_email = (isset ($this->nmo->settings->custom_sending_email) && stripos($this->nmo->settings->custom_sending_email, "@")) ? $this->nmo->settings->custom_sending_email : $user->email; + $sending_user = (isset ($this->nmo->settings->email_from_name) && strlen($this->nmo->settings->email_from_name) > 2) ? $this->nmo->settings->email_from_name : $user->name(); $this->nmo - ->mailable - ->from($sending_email, $sending_user); + ->mailable + ->from($sending_email, $sending_user); } /** @@ -550,11 +584,11 @@ class NinjaMailerJob implements ShouldQueue } $this->nmo - ->mailable - ->from($user->email, $user->name()) - ->withSymfonyMessage(function ($message) use ($token) { - $message->getHeaders()->addTextHeader('gmailtoken', $token); - }); + ->mailable + ->from($user->email, $user->name()) + ->withSymfonyMessage(function ($message) use ($token) { + $message->getHeaders()->addTextHeader('gmailtoken', $token); + }); } /** @@ -578,7 +612,7 @@ class NinjaMailerJob implements ShouldQueue } $google->getClient()->setAccessToken(json_encode($user->oauth_user_token)); - } catch(\Exception $e) { + } catch (\Exception $e) { $this->logMailError('Gmail Token Invalid', $this->company->clients()->first()); $this->nmo->settings->email_sending_method = 'default'; return $this->setMailDriver(); @@ -598,7 +632,7 @@ class NinjaMailerJob implements ShouldQueue * Now that our token is refreshed and valid we can boot the * mail driver at runtime and also set the token which will persist * just for this request. - */ + */ $token = $user->oauth_user_token->access_token; @@ -609,11 +643,11 @@ class NinjaMailerJob implements ShouldQueue } $this->nmo - ->mailable - ->from($user->email, $user->name()) - ->withSymfonyMessage(function ($message) use ($token) { - $message->getHeaders()->addTextHeader('gmailtoken', $token); - }); + ->mailable + ->from($user->email, $user->name()) + ->withSymfonyMessage(function ($message) use ($token) { + $message->getHeaders()->addTextHeader('gmailtoken', $token); + }); } /** @@ -626,7 +660,7 @@ class NinjaMailerJob implements ShouldQueue private function preFlightChecksFail(): bool { /* Always send regardless */ - if($this->override) { + if ($this->override) { return false; } @@ -646,7 +680,7 @@ class NinjaMailerJob implements ShouldQueue } /* GMail users are uncapped */ - if (Ninja::isHosted() && (in_array($this->nmo->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun']))) { + if (Ninja::isHosted() && (in_array($this->nmo->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun', 'client_brevo']))) { return false; } @@ -690,21 +724,23 @@ class NinjaMailerJob implements ShouldQueue */ private function logMailError($errors, $recipient_object): void { - (new SystemLogger( - $errors, - SystemLog::CATEGORY_MAIL, - SystemLog::EVENT_MAIL_SEND, - SystemLog::TYPE_FAILURE, - $recipient_object, - $this->nmo->company - ))->handle(); + ( + new SystemLogger( + $errors, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_SEND, + SystemLog::TYPE_FAILURE, + $recipient_object, + $this->nmo->company + ) + )->handle(); $job_failure = new EmailFailure($this->nmo->company->company_key); $job_failure->string_metric5 = 'failed_email'; $job_failure->string_metric6 = substr($errors, 0, 150); LightLogs::create($job_failure) - ->send(); + ->send(); $job_failure = null; } @@ -729,8 +765,8 @@ class NinjaMailerJob implements ShouldQueue $token = json_decode($guzzle->post($url, [ 'form_params' => [ - 'client_id' => config('ninja.o365.client_id') , - 'client_secret' => config('ninja.o365.client_secret') , + 'client_id' => config('ninja.o365.client_id'), + 'client_secret' => config('ninja.o365.client_secret'), 'scope' => 'email Mail.Send offline_access profile User.Read openid', 'grant_type' => 'refresh_token', 'refresh_token' => $user->oauth_user_refresh_token diff --git a/app/Jobs/Ninja/BankTransactionSync.php b/app/Jobs/Ninja/BankTransactionSync.php index 89a0b33e4df7..a0d077a570c8 100644 --- a/app/Jobs/Ninja/BankTransactionSync.php +++ b/app/Jobs/Ninja/BankTransactionSync.php @@ -74,7 +74,7 @@ class BankTransactionSync implements ShouldQueue if ($account->isEnterprisePaidClient()) { $account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->where('disabled_upstream', 0)->cursor()->each(function ($bank_integration) use ($account) { - (new ProcessBankTransactionsYodlee($account->id, $bank_integration))->handle(); + (new ProcessBankTransactionsYodlee($account->bank_integration_account_id, $bank_integration))->handle(); }); } @@ -90,7 +90,14 @@ class BankTransactionSync implements ShouldQueue if ((Ninja::isSelfHost() || (Ninja::isHosted() && $account->isEnterprisePaidClient()))) { $account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_NORDIGEN)->where('auto_sync', true)->where('disabled_upstream', 0)->cursor()->each(function ($bank_integration) { - (new ProcessBankTransactionsNordigen($bank_integration))->handle(); + try { + (new ProcessBankTransactionsNordigen($bank_integration))->handle(); + } + catch(\Exception $e) { + sleep(20); + } + + sleep(5); }); } diff --git a/app/Jobs/PostMark/ProcessPostmarkWebhook.php b/app/Jobs/PostMark/ProcessPostmarkWebhook.php index 963a4c64f8a9..1664cbafdcc7 100644 --- a/app/Jobs/PostMark/ProcessPostmarkWebhook.php +++ b/app/Jobs/PostMark/ProcessPostmarkWebhook.php @@ -45,7 +45,7 @@ class ProcessPostmarkWebhook implements ShouldQueue private $entity; - private array $default_response = [ + private array $default_response = [ 'recipients' => '', 'subject' => 'Message not found.', 'entity' => '', @@ -53,6 +53,8 @@ class ProcessPostmarkWebhook implements ShouldQueue 'events' => [], ]; + private ?Company $company = null; + /** * Create a new job instance. * @@ -64,11 +66,11 @@ class ProcessPostmarkWebhook implements ShouldQueue private function getSystemLog(string $message_id): ?SystemLog { return SystemLog::query() - ->where('company_id', $this->invitation->company_id) - ->where('type_id', SystemLog::TYPE_WEBHOOK_RESPONSE) - ->whereJsonContains('log', ['MessageID' => $message_id]) - ->orderBy('id', 'desc') - ->first(); + ->where('company_id', $this->invitation->company_id) + ->where('type_id', SystemLog::TYPE_WEBHOOK_RESPONSE) + ->whereJsonContains('log', ['MessageID' => $message_id]) + ->orderBy('id', 'desc') + ->first(); } @@ -87,12 +89,12 @@ class ProcessPostmarkWebhook implements ShouldQueue public function handle() { MultiDB::findAndSetDbByCompanyKey($this->request['Tag']); - $company = Company::where('company_key', $this->request['Tag'])->first(); + $this->company = Company::where('company_key', $this->request['Tag'])->first(); $this->invitation = $this->discoverInvitation($this->request['MessageID']); - if ($company && $this->request['RecordType'] == 'SpamComplaint' && config('ninja.notification.slack')) { - $company->notification(new EmailSpamNotification($company))->ninja(); + if ($this->company && $this->request['RecordType'] == 'SpamComplaint' && config('ninja.notification.slack')) { + $this->company->notification(new EmailSpamNotification($this->company))->ninja(); } if (!$this->invitation) { @@ -108,8 +110,8 @@ class ProcessPostmarkWebhook implements ShouldQueue return $this->processDelivery(); case 'Bounce': - if($this->request['Subject'] == ctrans('texts.confirmation_subject')) { - $company->notification(new EmailBounceNotification($this->request['Email']))->ninja(); + if ($this->request['Subject'] == ctrans('texts.confirmation_subject')) { + $this->company->notification(new EmailBounceNotification($this->request['Email']))->ninja(); } return $this->processBounce(); @@ -169,19 +171,21 @@ class ProcessPostmarkWebhook implements ShouldQueue $sl = $this->getSystemLog($this->request['MessageID']); - if($sl) { + if ($sl) { $this->updateSystemLog($sl, $data); return; } - (new SystemLogger( - $data, - SystemLog::CATEGORY_MAIL, - SystemLog::EVENT_MAIL_OPENED, - SystemLog::TYPE_WEBHOOK_RESPONSE, - $this->invitation->contact->client, - $this->invitation->company - ))->handle(); + ( + new SystemLogger( + $data, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_OPENED, + SystemLog::TYPE_WEBHOOK_RESPONSE, + $this->invitation->contact->client, + $this->invitation->company + ) + )->handle(); } // { @@ -207,19 +211,21 @@ class ProcessPostmarkWebhook implements ShouldQueue $sl = $this->getSystemLog($this->request['MessageID']); - if($sl) { + if ($sl) { $this->updateSystemLog($sl, $data); return; } - (new SystemLogger( - $data, - SystemLog::CATEGORY_MAIL, - SystemLog::EVENT_MAIL_DELIVERY, - SystemLog::TYPE_WEBHOOK_RESPONSE, - $this->invitation->contact->client, - $this->invitation->company - ))->handle(); + ( + new SystemLogger( + $data, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_DELIVERY, + SystemLog::TYPE_WEBHOOK_RESPONSE, + $this->invitation->contact->client, + $this->invitation->company + ) + )->handle(); } // { @@ -265,7 +271,7 @@ class ProcessPostmarkWebhook implements ShouldQueue $sl = $this->getSystemLog($this->request['MessageID']); - if($sl) { + if ($sl) { $this->updateSystemLog($sl, $data); return; } @@ -316,7 +322,7 @@ class ProcessPostmarkWebhook implements ShouldQueue $sl = $this->getSystemLog($this->request['MessageID']); - if($sl) { + if ($sl) { $this->updateSystemLog($sl, $data); } @@ -349,7 +355,9 @@ class ProcessPostmarkWebhook implements ShouldQueue public function getRawMessage(string $message_id) { - $postmark = new PostmarkClient(config('services.postmark.token')); + $postmark_secret = !empty($this->company->settings->postmark_secret) ? $this->company->settings->postmark_secret : config('services.postmark.token'); + + $postmark = new PostmarkClient($postmark_secret); $messageDetail = $postmark->getOutboundMessageDetails($message_id); return $messageDetail; @@ -362,7 +370,7 @@ class ProcessPostmarkWebhook implements ShouldQueue $messageDetail = $this->getRawMessage($message_id); - $event = collect($messageDetail->messageevents)->first(function ($event) { + $event = collect($messageDetail->messageevents)->first(function ($event) { return $event?->Details?->BounceID ?? false; @@ -374,29 +382,31 @@ class ProcessPostmarkWebhook implements ShouldQueue private function fetchMessage(): array { - if(strlen($this->request['MessageID']) < 1) { + if (strlen($this->request['MessageID']) < 1) { return $this->default_response; } try { - $postmark = new PostmarkClient(config('services.postmark.token')); + $postmark_secret = !empty($this->company->settings->postmark_secret) ? $this->company->settings->postmark_secret : config('services.postmark.token'); + + $postmark = new PostmarkClient($postmark_secret); $messageDetail = $postmark->getOutboundMessageDetails($this->request['MessageID']); $recipients = collect($messageDetail['recipients'])->flatten()->implode(','); $subject = $messageDetail->subject ?? ''; - $events = collect($messageDetail->messageevents)->map(function ($event) { + $events = collect($messageDetail->messageevents)->map(function ($event) { return [ - 'bounce_id' => $event?->Details?->BounceID ?? '', - 'recipient' => $event->Recipient ?? '', - 'status' => $event->Type ?? '', - 'delivery_message' => $event->Details->DeliveryMessage ?? $event->Details->Summary ?? '', - 'server' => $event->Details->DestinationServer ?? '', - 'server_ip' => $event->Details->DestinationIP ?? '', - 'date' => \Carbon\Carbon::parse($event->ReceivedAt)->format('Y-m-d H:i:s') ?? '', - ]; + 'bounce_id' => $event?->Details?->BounceID ?? '', + 'recipient' => $event->Recipient ?? '', + 'status' => $event->Type ?? '', + 'delivery_message' => $event->Details->DeliveryMessage ?? $event->Details->Summary ?? '', + 'server' => $event->Details->DestinationServer ?? '', + 'server_ip' => $event->Details->DestinationIP ?? '', + 'date' => \Carbon\Carbon::parse($event->ReceivedAt)->format('Y-m-d H:i:s') ?? '', + ]; })->toArray(); diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index def24172b528..5bed2111b564 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -76,12 +76,12 @@ class SendRecurring implements ShouldQueue $invoice = $invoice->service() ->markSent() ->applyNumber() - ->fillDefaults() + ->fillDefaults(true) ->adjustInventory() ->save(); } else { $invoice = $invoice->service() - ->fillDefaults() + ->fillDefaults(true) ->save(); } diff --git a/app/Libraries/MultiDB.php b/app/Libraries/MultiDB.php index 517ceb83175e..3b7e5140da36 100644 --- a/app/Libraries/MultiDB.php +++ b/app/Libraries/MultiDB.php @@ -21,6 +21,7 @@ use Illuminate\Support\Str; use App\Models\CompanyToken; use App\Models\ClientContact; use App\Models\VendorContact; +use App\DataProviders\SMSNumbers; use Illuminate\Support\Facades\DB; /** @@ -586,6 +587,10 @@ class MultiDB $current_db = config('database.default'); + if(SMSNumbers::hasNumber($phone)){ + return true; + } + foreach (self::$dbs as $db) { self::setDB($db); if ($exists = Account::where('account_sms_verification_number', $phone)->where('account_sms_verified', true)->exists()) { diff --git a/app/Livewire/RequiredClientInfo.php b/app/Livewire/RequiredClientInfo.php index 733f9a0b4559..c661bbcb8c53 100644 --- a/app/Livewire/RequiredClientInfo.php +++ b/app/Livewire/RequiredClientInfo.php @@ -222,8 +222,9 @@ class RequiredClientInfo extends Component $this->show_form = true; $hash = Cache::get(request()->input('hash')); + $invoice = Invoice::find($this->decodePrimaryKey($hash['invoice_id'])); - $this->invoice_terms = Invoice::find($this->decodePrimaryKey($hash['invoice_id']))->terms; + $this->invoice_terms = $invoice->terms; } count($this->fields) > 0 || $this->show_terms diff --git a/app/Models/Account.php b/app/Models/Account.php index 382eec8692ee..adf95c62086f 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -102,7 +102,7 @@ class Account extends BaseModel private $free_plan_email_quota = 20; - private $paid_plan_email_quota = 400; + private $paid_plan_email_quota = 300; /** * @var string @@ -494,7 +494,7 @@ class Account extends BaseModel return 0; } - if (Carbon::createFromTimestamp($this->created_at)->diffInWeeks() == 0) { + if (Carbon::createFromTimestamp($this->created_at)->diffInWeeks() <= 1) { return 20; } @@ -503,11 +503,13 @@ class Account extends BaseModel } if ($this->isPaid()) { + $multiplier = $this->plan == 'enterprise' ? 2 : 1.2; + $limit = $this->paid_plan_email_quota; - $limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 50; + $limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * (20 * $multiplier); } else { $limit = $this->free_plan_email_quota; - $limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 2; + $limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 1.5; } return min($limit, 1000); diff --git a/app/Models/Client.php b/app/Models/Client.php index 61bda7230946..6ac021a4bc43 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -485,7 +485,7 @@ class Client extends BaseModel implements HasLocalePreference } /*Company Settings*/ - elseif ((property_exists($this->company->settings, $setting) != false) && (isset($this->company->settings->{$setting}) !== false)) { + elseif ((property_exists($this->company->settings, $setting) !== false) && (isset($this->company->settings->{$setting}) !== false)) { return $this->company->settings->{$setting}; } elseif (property_exists(CompanySettings::defaults(), $setting)) { return CompanySettings::defaults()->{$setting}; diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index 253af5585fe6..aa8f0438e5a2 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -132,6 +132,7 @@ class CompanyGateway extends BaseModel // const TYPE_EWAY = 313; // const TYPE_FORTE = 314; // const PAYPAL_PPCP = 323; + // const SQUARE = 320; public $gateway_consts = [ '38f2c48af60c7dd69e04248cbb24c36e' => 300, @@ -144,7 +145,7 @@ class CompanyGateway extends BaseModel '8fdeed552015b3c7b44ed6c8ebd9e992' => 309, 'd6814fc83f45d2935e7777071e629ef9' => 310, 'bbd736b3254b0aabed6ad7fda1298c88' => 311, - '1bd651fb213ca0c9d66ae3c336dc77e7' => 312, + '1bd651fb213ca0c9d66ae3c336dc77e8' => 312, '944c20175bbe6b9972c05bcfe294c2c7' => 313, 'kivcvjexxvdiyqtj3mju5d6yhpeht2xs' => 314, '65faab2ab6e3223dbe848b1686490baz' => 320, diff --git a/app/PaymentDrivers/AuthorizePaymentDriver.php b/app/PaymentDrivers/AuthorizePaymentDriver.php index a9ac9d568367..87c74fdb9209 100644 --- a/app/PaymentDrivers/AuthorizePaymentDriver.php +++ b/app/PaymentDrivers/AuthorizePaymentDriver.php @@ -193,6 +193,18 @@ class AuthorizePaymentDriver extends BaseDriver public function import() { + $this->init(); + return (new AuthorizeCustomer($this))->importCustomers(); } + + public function importCustomers() + { + return $this->import(); + } + + public function auth(): bool + { + return $this->init()->getPublicClientKey() ?? false; + } } diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 8ea61173467d..a8ffd38cd61e 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -806,4 +806,14 @@ class BaseDriver extends AbstractPaymentDriver { return true; } + + public function auth(): bool + { + return true; + } + + public function importCustomers() + { + + } } diff --git a/app/PaymentDrivers/BraintreePaymentDriver.php b/app/PaymentDrivers/BraintreePaymentDriver.php index 6f90f0cd1482..cb214f62be02 100644 --- a/app/PaymentDrivers/BraintreePaymentDriver.php +++ b/app/PaymentDrivers/BraintreePaymentDriver.php @@ -12,29 +12,40 @@ namespace App\PaymentDrivers; -use App\Jobs\Util\SystemLogger; -use App\Models\ClientGatewayToken; -use App\Models\GatewayType; +use Exception; +use App\Models\Client; +use Braintree\Gateway; use App\Models\Invoice; use App\Models\Payment; +use App\Models\SystemLog; +use App\Models\GatewayType; use App\Models\PaymentHash; use App\Models\PaymentType; -use App\Models\SystemLog; +use App\Models\ClientContact; +use App\Factory\ClientFactory; +use Illuminate\Support\Carbon; +use App\Jobs\Util\SystemLogger; +use App\Models\ClientGatewayToken; +use App\Factory\ClientContactFactory; use App\PaymentDrivers\Braintree\ACH; -use App\PaymentDrivers\Braintree\CreditCard; +use App\Utils\Traits\GeneratesCounter; +use Illuminate\Database\QueryException; use App\PaymentDrivers\Braintree\PayPal; -use Braintree\Gateway; -use Exception; use Illuminate\Support\Facades\Validator; +use App\PaymentDrivers\Braintree\CreditCard; class BraintreePaymentDriver extends BaseDriver { + use GeneratesCounter; + public $refundable = true; public $token_billing = true; public $can_authorise_credit_card = true; + private bool $completed = true; + /** * @var Gateway; */ @@ -157,34 +168,6 @@ class BraintreePaymentDriver extends BaseDriver } } - // public function updateCustomer() - // { - // $customer = $this->findOrCreateCustomer(); - - // $result = $this->gateway->customer()->update( - // $customer->id, - // [ - // 'firstName' => $this->client->present()->name(), - // 'email' => $this->client->present()->email(), - // 'phone' => $this->client->present()->phone(), - // 'creditCard' => [ - // 'billingAddress' => [ - // 'options' => [ - // 'updateExisting' => true - // ], - // 'firstName' => $this->client->present()->first_name() ?: $this->client->present()->name(), - // 'lastName' => $this->client->present()->last_name() ?: '', - // 'streetAddress' => $this->client->address1 ?: '', - // 'extendedAddress' =>$this->client->address2 ?: '', - // 'locality' => $this->client->city ?: '', - // 'postalCode' => $this->client->postal_code ?: '', - // 'countryCodeAlpha2' => $this->client->country ? $this->client->country->iso_3166_2 : 'US', - // ], - // ], - // ] - // ); - // } - public function refund(Payment $payment, $amount, $return_client_response = false) { $this->init(); @@ -324,13 +307,198 @@ class BraintreePaymentDriver extends BaseDriver nlog('braintree webhook'); - // if($webhookNotification) - // nlog($webhookNotification->kind); - - // // Example values for webhook notification properties - // $message = $webhookNotification->kind; // "subscription_went_past_due" - // $message = $webhookNotification->timestamp->format('D M j G:i:s T Y'); // "Sun Jan 1 00:00:00 UTC 2012" - return response()->json([], 200); } + + public function auth(): bool + { + + try { + $ct =$this->init()->gateway->clientToken()->generate(); + + return true; + } + catch(\Exception $e) { + + } + + return false; + } + + private function find(string $customer_id = '') { + + try { + return $this->init()->gateway->customer()->find($customer_id); + } + catch(\Exception $e){ + return false; + } + + return false; + } + + private function findTokens(string $gateway_customer_reference) + { + return ClientGatewayToken::where('company_id', $this->company_gateway->company_id) + ->where('gateway_customer_reference', $gateway_customer_reference) + ->exists(); + } + + private function getToken(string $token, string $gateway_customer_reference) + { + + return ClientGatewayToken::where('company_id', $this->company_gateway->company_id) + ->where('gateway_customer_reference', $gateway_customer_reference) + ->where('token', $token) + ->first(); + + } + + private function findClient(string $email) { + return ClientContact::where('company_id', $this->company_gateway->company_id) + ->where('email', $email) + ->first()->client ?? false; + } + + private function addClientCards(Client $client, array $cards) + { + + $this->client = $client; + + foreach($cards as $card) { + + if($this->getToken($card->token, $card->customerId) || Carbon::createFromDate($card->expirationYear, $card->expirationMonth, '1')->lt(now())) + continue; + + $payment_meta = new \stdClass(); + $payment_meta->exp_month = (string) $card->expirationMonth; + $payment_meta->exp_year = (string) $card->expirationYear; + $payment_meta->brand = (string) $card->cardType; + $payment_meta->last4 = (string) $card->last4; + $payment_meta->type = GatewayType::CREDIT_CARD; + + $data = [ + 'payment_meta' => $payment_meta, + 'token' => $card->token, + 'payment_method_id' => GatewayType::CREDIT_CARD, + ]; + + $this->storeGatewayToken($data, ['gateway_customer_reference' => $card->customerId]); + + nlog("adding card to customer payment profile"); + + } + + } + + public function createNinjaClient(mixed $customer): Client + { + + $client = ClientFactory::create($this->company_gateway->company_id, $this->company_gateway->user_id); + + $b_business_address = count($customer->addresses) >= 1 ? $customer->addresses[0] : false; + $b_shipping_address = count($customer->addresses) > 1 ? $customer->addresses[1] : false; + $import_client_data = []; + + if($b_business_address) { + + $braintree_address = + [ + 'address1' => $b_business_address->extendedAddress ?? '', + 'address2' => $b_business_address->streetAddress ?? '', + 'city' => $b_business_address->locality ?? '', + 'postal_code' => $b_business_address->postalCode ?? '', + 'state' => $b_business_address->region ?? '', + 'country_id' => $b_business_address->countryCodeNumeric ?? '840', + ]; + + $import_client_data = array_merge($import_client_data, $braintree_address); + } + + if($b_shipping_address) { + + $braintree_shipping_address = + [ + 'shipping_address1' => $b_shipping_address->extendedAddress ?? '', + 'shipping_address2' => $b_shipping_address->streetAddress ?? '', + 'shipping_city' => $b_shipping_address->locality ?? '', + 'shipping_postal_code' => $b_shipping_address->postalCode ?? '', + 'shipping_state' => $b_shipping_address->region ?? '', + 'shipping_country_id' => $b_shipping_address->countryCodeNumeric ?? '840', + ]; + + $import_client_data = array_merge($import_client_data, $braintree_shipping_address); + + } + + $client->fill($import_client_data); + + $client->phone = $customer->phone ?? ''; + $client->name = $customer->company ?? $customer->firstName; + + $settings = $client->settings; + $settings->currency_id = (string) $this->company_gateway->company->settings->currency_id; + $client->settings = $settings; + $client->save(); + + $contact = ClientContactFactory::create($this->company_gateway->company_id, $this->company_gateway->user_id); + $contact->first_name = $customer->firstName ?? ''; + $contact->last_name = $customer->lastName ?? ''; + $contact->email = $customer->email ?? ''; + $contact->phone = $customer->phone ?? ''; + $contact->client_id = $client->id; + $contact->saveQuietly(); + + if (! isset($client->number) || empty($client->number)) { + $x = 1; + + do { + try { + $client->number = $this->getNextClientNumber($client); + $client->saveQuietly(); + + $this->completed = false; + } catch (QueryException $e) { + $x++; + + if ($x > 10) { + $this->completed = false; + } + } + } while ($this->completed); + } else { + $client->saveQuietly(); + } + + return $client; + + } + + public function importCustomers() + { + $customers = $this->init()->gateway->customer()->all(); + + foreach($customers as $c){ + + $customer = $this->find($c->id); + + // nlog(count($customer->creditCards). " Exists for {$c->id}"); + + if(!$customer) + continue; + + $client = $this->findClient($customer->email); + + if(!$this->findTokens($c->id) && !$client) { + //customer is not referenced in the system - create client + $client = $this->createNinjaClient($customer); + // nlog("Creating new Client"); + } + + $this->addClientCards($client, $customer->creditCards); + + // nlog("Adding Braintree Client: {$c->id} => {$client->id}"); + + } + } } diff --git a/app/PaymentDrivers/CheckoutComPaymentDriver.php b/app/PaymentDrivers/CheckoutComPaymentDriver.php index 176a763507b9..774bf6af64b5 100644 --- a/app/PaymentDrivers/CheckoutComPaymentDriver.php +++ b/app/PaymentDrivers/CheckoutComPaymentDriver.php @@ -12,37 +12,38 @@ namespace App\PaymentDrivers; -use App\Exceptions\PaymentFailed; -use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; -use App\Http\Requests\Gateways\Checkout3ds\Checkout3dsRequest; -use App\Http\Requests\Payments\PaymentWebhookRequest; -use App\Jobs\Util\SystemLogger; -use App\Models\ClientGatewayToken; +use Exception; use App\Models\Company; -use App\Models\GatewayType; use App\Models\Invoice; use App\Models\Payment; +use App\Models\SystemLog; +use Checkout\CheckoutSdk; +use Checkout\Environment; +use Checkout\Common\Phone; +use App\Models\GatewayType; use App\Models\PaymentHash; use App\Models\PaymentType; -use App\Models\SystemLog; -use App\PaymentDrivers\CheckoutCom\CheckoutWebhook; -use App\PaymentDrivers\CheckoutCom\CreditCard; -use App\PaymentDrivers\CheckoutCom\Utilities; -use App\Utils\Traits\SystemLogTrait; +use Illuminate\Support\Carbon; +use App\Jobs\Util\SystemLogger; +use App\Exceptions\PaymentFailed; +use App\Models\ClientGatewayToken; use Checkout\CheckoutApiException; +use App\Utils\Traits\SystemLogTrait; +use Checkout\Payments\RefundRequest; +use Illuminate\Support\Facades\Auth; use Checkout\CheckoutArgumentException; -use Checkout\CheckoutAuthorizationException; -use Checkout\CheckoutSdk; -use Checkout\Common\Phone; use Checkout\Customers\CustomerRequest; -use Checkout\Environment; +use Checkout\CheckoutAuthorizationException; +use App\PaymentDrivers\CheckoutCom\Utilities; +use Checkout\Payments\Request\PaymentRequest; +use App\PaymentDrivers\CheckoutCom\CreditCard; +use App\PaymentDrivers\CheckoutCom\CheckoutWebhook; +use App\Http\Requests\Payments\PaymentWebhookRequest; +use Checkout\Payments\Request\Source\RequestIdSource; +use App\Http\Requests\Gateways\Checkout3ds\Checkout3dsRequest; +use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use Checkout\Payments\Previous\PaymentRequest as PreviousPaymentRequest; use Checkout\Payments\Previous\Source\RequestIdSource as SourceRequestIdSource; -use Checkout\Payments\RefundRequest; -use Checkout\Payments\Request\PaymentRequest; -use Checkout\Payments\Request\Source\RequestIdSource; -use Exception; -use Illuminate\Support\Facades\Auth; class CheckoutComPaymentDriver extends BaseDriver { @@ -534,4 +535,87 @@ class CheckoutComPaymentDriver extends BaseDriver { // Gateway doesn't support this feature. } + + public function auth(): bool + { + try{ + $this->init()->gateway->getCustomersClient('x'); + return true; + } + catch(\Exception $e){ + + } + return false; + } + + private function getToken(string $token, $gateway_customer_reference) + { + return ClientGatewayToken::query() + ->where('company_id', $this->company_gateway->company_id) + ->where('gateway_customer_reference', $gateway_customer_reference) + ->where('token', $token) + ->first(); + } + + /** + * ImportCustomers + * + * Only their methods because checkout.com + * does not have a list route for customers + * + * @return void + */ + public function importCustomers() + { + $this->init(); + + $this->company_gateway + ->company + ->clients() + ->cursor() + ->each(function ($client){ + + if(!str_contains($client->present()->email(), "@")) + return; + + try{ + $customer = $this->gateway->getCustomersClient()->get($client->present()->email()); + } + catch(\Exception $e) { + nlog("Checkout: Customer not found"); + return; + } + + $this->client = $client; + + nlog($customer['instruments']); + + foreach($customer['instruments'] as $card) + { + if( + $card['type'] != 'card' || + Carbon::createFromDate($card['expiry_year'], $card['expiry_month'], '1')->lt(now()) || + $this->getToken($card['id'], $customer['id']) + ) + continue; + + $payment_meta = new \stdClass(); + $payment_meta->exp_month = (string) $card['expiry_month']; + $payment_meta->exp_year = (string) $card['expiry_year']; + $payment_meta->brand = (string) $card['scheme']; + $payment_meta->last4 = (string) $card['last4']; + $payment_meta->type = (int) GatewayType::CREDIT_CARD; + + $data = [ + 'payment_meta' => $payment_meta, + 'token' => $card['id'], + 'payment_method_id' => GatewayType::CREDIT_CARD, + ]; + + $this->storeGatewayToken($data, ['gateway_customer_reference' => $customer['id']]); + + } + + }); + } } diff --git a/app/PaymentDrivers/EwayPaymentDriver.php b/app/PaymentDrivers/EwayPaymentDriver.php index 92459d460d32..0a772fb71758 100644 --- a/app/PaymentDrivers/EwayPaymentDriver.php +++ b/app/PaymentDrivers/EwayPaymentDriver.php @@ -211,4 +211,24 @@ class EwayPaymentDriver extends BaseDriver return $fields; } + + public function auth(): bool + { + + $response =$this->init()->eway->queryTransaction('xx'); + + return (bool) count($response->getErrors()) == 0; + + } + + /** + * importCustomers + * + * No support + * @return void + */ + public function importCustomers() + { + return true; + } } diff --git a/app/PaymentDrivers/Factory/ForteCustomerFactory.php b/app/PaymentDrivers/Factory/ForteCustomerFactory.php new file mode 100644 index 000000000000..eb6336d778fb --- /dev/null +++ b/app/PaymentDrivers/Factory/ForteCustomerFactory.php @@ -0,0 +1,137 @@ + $customer['company_name'] ?? $customer['first_name'], + 'contacts' => [ + [ + 'first_name' => $customer['first_name'], + 'last_name' => $customer['last_name'], + 'email' => $this->getBillingAddress($customer)['email'], + 'phone' => $this->getBillingAddress($customer)['phone'], + ] + ], + 'settings' => [ + 'currency_id' => $company->settings->currency_id, + ], + ])->merge($this->getShippingAddress($customer)) + ->merge($this->getBillingAddress($customer)) + ->toArray(); + + } + + // public function convertToGateway(Client $client): array + // { + + // } + + private function getBillingAddress(array $customer): array + { + if(isset($customer['default_billing_address_token'])) { + + foreach($customer['addresses'] as $address) { + + if($address['address_token'] != $customer['default_billing_address_token']) + continue; + + return [ + 'address1' => $address['physical_address']['street_line1'], + 'address2' => $address['physical_address']['street_line2'], + 'city' => $address['physical_address']['locality'], + 'state' => $address['physical_address']['region'], + 'postal_code' => $address['physical_address']['postal_code'], + 'country_id' => '840', + 'email' => $address['email'], + 'phone' => $address['phone'], + ]; + + } + + } + + if(isset($customer['addresses'][0])) { + + $address = $customer['addresses'][0]; + + return [ + 'address1' => $address['physical_address']['street_line1'], + 'address2' => $address['physical_address']['street_line2'], + 'city' => $address['physical_address']['locality'], + 'state' => $address['physical_address']['region'], + 'postal_code' => $address['physical_address']['postal_code'], + 'email' => $address['email'], + 'phone' => $address['phone'], + 'country_id' => '840', + ]; + + } + + return ['email' => '', 'phone' => '']; + + } + + private function getShippingAddress(array $customer): array + { + + if(isset($customer['default_shipping_address_token'])) { + + foreach($customer['addresses'] as $address) { + + if($address['address_token'] != $customer['default_shipping_address_token']) { + continue; + } + + return [ + 'shipping_address1' => $address['physical_address']['street_line1'], + 'shipping_address2' => $address['physical_address']['street_line2'], + 'shipping_city' => $address['physical_address']['locality'], + 'shipping_state' => $address['physical_address']['region'], + 'shipping_postal_code' => $address['physical_address']['postal_code'], + 'shipping_country_id' => '840', + ]; + + } + + } + + if(isset($customer['addresses'][1])){ + + $address = $customer['addresses'][1]; + + return [ + 'shipping_address1' => $address['physical_address']['street_line1'], + 'shipping_address2' => $address['physical_address']['street_line2'], + 'shipping_city' => $address['physical_address']['locality'], + 'shipping_state' => $address['physical_address']['region'], + 'shipping_postal_code' => $address['physical_address']['postal_code'], + 'shipping_country_id' => '840', + 'email' => $address['email'], + 'phone' => $address['phone'], + ]; + + } + + return ['email' => '', 'phone' => '']; + + } +} diff --git a/app/PaymentDrivers/Factory/PaytraceCustomerFactory.php b/app/PaymentDrivers/Factory/PaytraceCustomerFactory.php new file mode 100644 index 000000000000..731a6c7085f6 --- /dev/null +++ b/app/PaymentDrivers/Factory/PaytraceCustomerFactory.php @@ -0,0 +1,59 @@ + $customer->billing_address->name ?? $customer->shipping_address->name, + 'contacts' => [ + [ + 'first_name' => $customer->billing_address->name ?? $customer->shipping_address->name, + 'last_name' => '', + 'email' => $customer->email, + 'phone' => $customer->phone, + ] + ], + 'currency_id' => $company->settings->currency_id, + 'address1' => $customer->billing_address->street_address, + 'address2' => $customer->billing_address->street_address2, + 'city' => $customer->billing_address->city, + 'state' => $customer->billing_address->state, + 'postal_code' => $customer->billing_address->zip, + 'country_id' => '840', + 'shipping_address1' => $customer->shipping_address->street_address, + 'shipping_address2' => $customer->shipping_address->street_address2, + 'shipping_city' => $customer->shipping_address->city, + 'shipping_state' => $customer->shipping_address->state, + 'shipping_postal_code' => $customer->shipping_address->zip, + 'shipping_country_id' => '840', + 'settings' => [ + 'currency_id' => $company->settings->currency_id, + ], + 'card' => [ + 'token' => $customer->customer_id, + 'last4' => $customer->credit_card->masked_number, + 'expiry_month' => $customer->credit_card->expiration_month, + 'expiry_year' => $customer->credit_card->expiration_year, + ], + ]) + ->toArray(); + + } + +} diff --git a/app/PaymentDrivers/Factory/SquareCustomerFactory.php b/app/PaymentDrivers/Factory/SquareCustomerFactory.php new file mode 100644 index 000000000000..fa3d9ad71bfd --- /dev/null +++ b/app/PaymentDrivers/Factory/SquareCustomerFactory.php @@ -0,0 +1,132 @@ +getCards() ?? [] as $card){ + + $meta = new \stdClass; + $meta->exp_month = $card->getExpMonth(); + $meta->exp_year = $card->getExpYear(); + $meta->last4 = $card->getLast4(); + $meta->brand = $card->getCardBrand(); + $meta->type = GatewayType::CREDIT_CARD; + + $cards[] = [ + 'token' => $card->getId(), + 'payment_meta' => $meta, + 'payment_method_id' => GatewayType::CREDIT_CARD, + 'gateway_customer_reference' => $customer->getId(), + ]; + } + + $address = $customer->getAddress(); + + return + collect([ + 'name' => $customer->getCompanyName() ?? ($customer->getGivenName() ?? '' ." " . $customer->getFamilyName() ?? ''), + 'contacts' => [ + [ + 'first_name' => $customer->getGivenName(), + 'last_name' => $customer->getFamilyName(), + 'email' => $customer->getEmailAddress(), + 'phone' => $customer->getPhoneNumber(), + ] + ], + 'currency_id' => $company->settings->currency_id, + 'address1' => $address->getAddressLine1(), + 'address2' => $address->getAddressLine2(), + 'city' => $address->getLocality(), + 'state' => $address->getAdministrativeDistrictLevel1(), + 'postal_code' => $address->getPostalCode(), + 'country_id' => '840', + 'settings' => [ + 'currency_id' => $company->settings->currency_id, + ], + 'cards' => $cards, + ]) + ->toArray(); + + } + +} diff --git a/app/PaymentDrivers/FortePaymentDriver.php b/app/PaymentDrivers/FortePaymentDriver.php index 2a9ebb440ad3..0cbf1e3883df 100644 --- a/app/PaymentDrivers/FortePaymentDriver.php +++ b/app/PaymentDrivers/FortePaymentDriver.php @@ -11,13 +11,19 @@ namespace App\PaymentDrivers; -use App\Jobs\Util\SystemLogger; -use App\Models\GatewayType; use App\Models\Payment; use App\Models\SystemLog; -use App\PaymentDrivers\Forte\ACH; -use App\PaymentDrivers\Forte\CreditCard; +use App\Models\GatewayType; +use App\Models\ClientContact; +use App\Factory\ClientFactory; +use App\Jobs\Util\SystemLogger; use App\Utils\Traits\MakesHash; +use App\PaymentDrivers\Forte\ACH; +use Illuminate\Support\Facades\Http; +use App\Repositories\ClientRepository; +use App\PaymentDrivers\Forte\CreditCard; +use App\Repositories\ClientContactRepository; +use App\PaymentDrivers\Factory\ForteCustomerFactory; class FortePaymentDriver extends BaseDriver { @@ -183,8 +189,140 @@ class FortePaymentDriver extends BaseDriver ]; } - // public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) - // { - // return $this->payment_method->yourTokenBillingImplmentation(); - // } + //////////////////////////////////////////// + // DB + /////////////////////////////////////////// + public function auth(): bool + { + + $forte_base_uri = "https://sandbox.forte.net/api/v3/"; + if ($this->company_gateway->getConfigField('testMode') == false) { + $forte_base_uri = "https://api.forte.net/v3/"; + } + $forte_api_access_id = $this->company_gateway->getConfigField('apiAccessId'); + $forte_secure_key = $this->company_gateway->getConfigField('secureKey'); + $forte_auth_organization_id = $this->company_gateway->getConfigField('authOrganizationId'); + $forte_organization_id = $this->company_gateway->getConfigField('organizationId'); + $forte_location_id = $this->company_gateway->getConfigField('locationId'); + + $response = Http::withBasicAuth($forte_api_access_id, $forte_secure_key) + ->withHeaders(['X-Forte-Auth-Organization-Id' => $forte_organization_id]) + ->get("{$forte_base_uri}/organizations/{$forte_organization_id}/locations/{$forte_location_id}/customers/"); + + return $response->successful(); + + } + + public function baseUri(): string + { + + $forte_base_uri = "https://sandbox.forte.net/api/v3/"; + if ($this->company_gateway->getConfigField('testMode') == false) { + $forte_base_uri = "https://api.forte.net/v3/"; + } + + return $forte_base_uri; + } + + private function getOrganisationId(): string + { + return $this->company_gateway->getConfigField('organizationId'); + } + + public function getLocationId(): string + { + return $this->company_gateway->getConfigField('locationId'); + } + + public function stubRequest() + { + + $forte_api_access_id = $this->company_gateway->getConfigField('apiAccessId'); + $forte_secure_key = $this->company_gateway->getConfigField('secureKey'); + $forte_auth_organization_id = $this->company_gateway->getConfigField('authOrganizationId'); + + return Http::withBasicAuth($forte_api_access_id, $forte_secure_key) + ->withHeaders(['X-Forte-Auth-Organization-Id' => $this->getOrganisationId()]); + } + + private function getClient(?string $email) + { + return ClientContact::query() + ->where('company_id', $this->company_gateway->company_id) + ->where('email', $email) + ->first(); + } + + public function getLocation() + { + + $response = $this->stubRequest() + ->withQueryParameters(['page_size' => 10000]) + ->get("{$this->baseUri()}/organizations/{$this->getOrganisationId()}/locations/{$this->getLocationId()}"); + + if($response->successful()) + return $response->json(); + + return false; + } + + public function updateFees() + { + $response = $this->getLocation(); + + if($response) + { + $body = $response['services']; + + $fees_and_limits = $this->company_gateway->fees_and_limits; + + if($body['card']['service_fee_percentage'] > 0 || $body['card']['service_fee_additional_amount'] > 0){ + + $fees_and_limits->{1}->fee_amount = $body['card']['service_fee_additional_amount']; + $fees_and_limits->{1}->fee_percent = $body['card']['service_fee_percentage']; + } + + if($body['debit']['service_fee_percentage'] > 0 || $body['debit']['service_fee_additional_amount'] > 0) { + + $fees_and_limits->{2}->fee_amount = $body['debit']['service_fee_additional_amount']; + $fees_and_limits->{2}->fee_percent = $body['debit']['service_fee_percentage']; + } + + $this->company_gateway->fees_and_limits = $fees_and_limits; + $this->company_gateway->save(); + + } + + return false; + + } + + public function importCustomers() + { + + $response = $this->stubRequest() + ->withQueryParameters(['page_size' => 10000]) + ->get("{$this->baseUri()}/organizations/{$this->getOrganisationId()}/locations/{$this->getLocationId()}/customers"); + + if($response->successful()){ + + foreach($response->json()['results'] as $customer) + { + + $client_repo = new ClientRepository(new ClientContactRepository()); + $factory = new ForteCustomerFactory(); + + $data = $factory->convertToNinja($customer, $this->company_gateway->company); + + if(strlen($data['email']) == 0 || $this->getClient($data['email'])) + continue; + + $client_repo->save($data, ClientFactory::create($this->company_gateway->company_id, $this->company_gateway->user_id)); + + //persist any payment methods here! + } + } + + } + } diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index 0ec94d1032c5..723a190f00be 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -84,11 +84,11 @@ class GoCardlessPaymentDriver extends BaseDriver $types[] = GatewayType::DIRECT_DEBIT; } - if (in_array($this->client->currency()->code, ['EUR', 'GBP'])) { + if ($this->client && in_array($this->client->currency()->code, ['EUR', 'GBP'])) { $types[] = GatewayType::SEPA; } - if ($this->client->currency()->code === 'GBP') { + if ($this->client && $this->client->currency()->code === 'GBP') { $types[] = GatewayType::INSTANT_BANK_PAY; } @@ -558,4 +558,17 @@ class GoCardlessPaymentDriver extends BaseDriver { return render('gateways.gocardless.verification'); } + + public function auth(): bool + { + try { + $customers = $this->init()->gateway->customers()->list(); + return true; + } + catch(\Exception $e){ + + } + + return false; + } } diff --git a/app/PaymentDrivers/MolliePaymentDriver.php b/app/PaymentDrivers/MolliePaymentDriver.php index 57adb0ca2698..ed02ba31a835 100644 --- a/app/PaymentDrivers/MolliePaymentDriver.php +++ b/app/PaymentDrivers/MolliePaymentDriver.php @@ -420,4 +420,20 @@ class MolliePaymentDriver extends BaseDriver { return \number_format((float) $amount, 2, '.', ''); } + + public function auth(): bool + { + $this->init(); + + try { + $p = $this->gateway->payments->page(); + return true; + } + catch(\Exception $e){ + + } + + return false; + + } } diff --git a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php index 5b757d907600..5c86fae72acc 100644 --- a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php +++ b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php @@ -560,5 +560,28 @@ class PayPalPPCPPaymentDriver extends BaseDriver PayPalWebhook::dispatch($request->all(), $request->headers->all(), $this->access_token); } + + public function auth(): bool + { + try { + $this->init()->getClientToken(); + return true; + } + catch(\Exception $e) { + + } + + return false; + } + + public function importCustomers() + { + + // $response = $this->gatewayRequest('/v1/reporting/transactions', 'get', ['fields' => 'all','page_size' => 500,'start_date' => '2024-02-01T00:00:00-0000', 'end_date' => '2024-03-01T00:00:00-0000']); + + // nlog($response->json()); + + return true; + } } diff --git a/app/PaymentDrivers/PayPalRestPaymentDriver.php b/app/PaymentDrivers/PayPalRestPaymentDriver.php index f08decd67501..1d2bbd90bac3 100644 --- a/app/PaymentDrivers/PayPalRestPaymentDriver.php +++ b/app/PaymentDrivers/PayPalRestPaymentDriver.php @@ -81,11 +81,7 @@ class PayPalRestPaymentDriver extends BaseDriver public function init() { - // $this->omnipay_gateway = Omnipay::create( - // $this->company_gateway->gateway->provider - // ); - // $this->omnipay_gateway->initialize((array) $this->company_gateway->getConfig()); $this->api_endpoint_url = $this->company_gateway->getConfigField('testMode') ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com'; $secret = $this->company_gateway->getConfigField('secret'); @@ -380,61 +376,6 @@ class PayPalRestPaymentDriver extends BaseDriver return $r->json()['id']; - - - // $_invoice = collect($this->payment_hash->data->invoices)->first(); - - // $invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id)); - - // $order = [ - // "intent" => "CAPTURE", - // "payer" => [ - // "name" => [ - // "given_name" => $this->client->present()->first_name(), - // "surname" => $this->client->present()->last_name(), - // ], - // "email_address" => $this->client->present()->email(), - // "address" => [ - // "address_line_1" => $this->client->address1, - // "address_line_2" => $this->client->address2, - // "admin_area_1" => $this->client->city, - // "admin_area_2" => $this->client->state, - // "postal_code" => $this->client->postal_code, - // "country_code" => $this->client->country->iso_3166_2, - // ] - // ], - // "purchase_units" => [ - // [ - // "description" => ctrans('texts.invoice_number').'# '.$invoice->number, - // "invoice_id" => $invoice->number, - // "amount" => [ - // "value" => (string)$data['amount_with_fee'], - // "currency_code" => $this->client->currency()->code, - // "breakdown" => [ - // "item_total" => [ - // "currency_code" => $this->client->currency()->code, - // "value" => (string)$data['amount_with_fee'] - // ] - // ] - // ], - // "items" => [ - // [ - // "name" => ctrans('texts.invoice_number').'# '.$invoice->number, - // "quantity" => "1", - // "unit_amount" => [ - // "currency_code" => $this->client->currency()->code, - // "value" => (string)$data['amount_with_fee'] - // ], - // ], - // ], - // ] - // ] - // ]; - - // $r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order); - - // return $r->json()['id']; - } private function getShippingAddress(): ?array @@ -483,7 +424,7 @@ class PayPalRestPaymentDriver extends BaseDriver SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_PAYPAL, $this->client, - $this->client->company, + $this->client->company ?? $this->company_gateway->company, ); throw new PaymentFailed("Gateway failure - {$r->body()}", 401); @@ -520,5 +461,23 @@ class PayPalRestPaymentDriver extends BaseDriver return 0; } + public function auth(): bool + { + + try { + $this->init()->getClientToken(); + return true; + } + catch(\Exception $e) { + + } + + return false; + } + + public function importCustomers() + { + return true; + } } diff --git a/app/PaymentDrivers/PayTrace/CreditCard.php b/app/PaymentDrivers/PayTrace/CreditCard.php index 0fa5f37991d1..b08a27088d1e 100644 --- a/app/PaymentDrivers/PayTrace/CreditCard.php +++ b/app/PaymentDrivers/PayTrace/CreditCard.php @@ -59,6 +59,9 @@ class CreditCard 'enc_key' => $data['enc_key'], 'integrator_id' => $this->paytrace->company_gateway->getConfigField('integratorId'), 'billing_address' => $this->buildBillingAddress(), + 'email' => $this->paytrace->client->present()->email(), + 'phone' => $this->paytrace->client->present()->phone(), + ]; $response = $this->paytrace->gatewayRequest('/v1/customer/pt_protect_create', $post_data); diff --git a/app/PaymentDrivers/PaytracePaymentDriver.php b/app/PaymentDrivers/PaytracePaymentDriver.php index 4f76b95645a2..05278300bc7c 100644 --- a/app/PaymentDrivers/PaytracePaymentDriver.php +++ b/app/PaymentDrivers/PaytracePaymentDriver.php @@ -11,19 +11,24 @@ namespace App\PaymentDrivers; -use App\Exceptions\SystemError; -use App\Http\Requests\Payments\PaymentWebhookRequest; -use App\Jobs\Util\SystemLogger; -use App\Models\ClientGatewayToken; -use App\Models\GatewayType; use App\Models\Invoice; use App\Models\Payment; +use App\Utils\CurlUtils; +use App\Models\SystemLog; +use App\Models\GatewayType; use App\Models\PaymentHash; use App\Models\PaymentType; -use App\Models\SystemLog; -use App\PaymentDrivers\PayTrace\CreditCard; -use App\Utils\CurlUtils; +use App\Factory\ClientFactory; +use App\Exceptions\SystemError; +use App\Jobs\Util\SystemLogger; use App\Utils\Traits\MakesHash; +use App\Models\ClientGatewayToken; +use App\Repositories\ClientRepository; +use App\PaymentDrivers\PayTrace\CreditCard; +use App\Repositories\ClientContactRepository; +use App\Http\Requests\Payments\PaymentWebhookRequest; +use App\Models\ClientContact; +use App\PaymentDrivers\Factory\PaytraceCustomerFactory; class PaytracePaymentDriver extends BaseDriver { @@ -246,4 +251,76 @@ class PaytracePaymentDriver extends BaseDriver return false; } + + public function auth(): bool + { + try { + $this->init()->generateAuthHeaders() && strlen($this->company_gateway->getConfigField('integratorId')) > 2; + return true; + } + catch(\Exception $e){ + + } + + return false; + + } + + public function importCustomers() + { + + $data = [ + 'integrator_id' => $this->company_gateway->getConfigField('integratorId'), + ]; + + $response = $this->gatewayRequest('/v1/customer/export', $data); + + nlog($response); + + if ($response && $response->success) { + + $client_repo = new ClientRepository(new ClientContactRepository()); + $factory = new PaytraceCustomerFactory(); + + foreach($response->customers as $customer) + { + $data = $factory->convertToNinja($customer, $this->company_gateway->company); + + $client = false; + + if(str_contains($data['contacts'][0]['email'], "@")) + { + $client = ClientContact::query() + ->where('company_id', $this->company_gateway->company_id) + ->where('email', $data['contacts'][0]['email']) + ->first()->client ?? false; + } + + if(!$client) + $client = $client_repo->save($data, ClientFactory::create($this->company_gateway->company_id, $this->company_gateway->user_id)); + + $this->client = $client; + + if(ClientGatewayToken::query()->where('client_id', $client->id)->where('token',$data['card']['token'])->exists()) + continue; + + $cgt = []; + $cgt['token'] = $data['card']['token']; + $cgt['payment_method_id'] = GatewayType::CREDIT_CARD; + + $payment_meta = new \stdClass(); + $payment_meta->exp_month = $data['card']['expiry_month']; + $payment_meta->exp_year = $data['card']['expiry_year']; + $payment_meta->brand = 'CC'; + $payment_meta->last4 = $data['card']['last4']; + $payment_meta->type = GatewayType::CREDIT_CARD; + + $cgt['payment_meta'] = $payment_meta; + + $token = $this->storeGatewayToken($cgt, []); + + } + } + + } } diff --git a/app/PaymentDrivers/Square/CreditCard.php b/app/PaymentDrivers/Square/CreditCard.php index da53d36a2610..80b48fb5a15a 100644 --- a/app/PaymentDrivers/Square/CreditCard.php +++ b/app/PaymentDrivers/Square/CreditCard.php @@ -197,7 +197,7 @@ class CreditCard implements MethodInterface { $square_card = new \Square\Models\Card(); - $square_card->setCustomerId($this->findOrCreateClient()); + $square_card->setCustomerId($this->square_driver->findOrCreateClient()); $body = new \Square\Models\CreateCardRequest(uniqid("st", true), $source_id, $square_card); @@ -238,82 +238,5 @@ class CreditCard implements MethodInterface return false; } - private function findOrCreateClient() - { - $email_address = new \Square\Models\CustomerTextFilter(); - $email_address->setExact($this->square_driver->client->present()->email()); - - $filter = new \Square\Models\CustomerFilter(); - $filter->setEmailAddress($email_address); - - $query = new \Square\Models\CustomerQuery(); - $query->setFilter($filter); - - $body = new \Square\Models\SearchCustomersRequest(); - $body->setQuery($query); - - $api_response = $this->square_driver - ->init() - ->square - ->getCustomersApi() - ->searchCustomers($body); - - $customers = false; - - if ($api_response->isSuccess()) { - $customers = $api_response->getBody(); - $customers = json_decode($customers); - - if (count([$api_response->getBody(), 1]) == 0) { - $customers = false; - } - } else { - $errors = $api_response->getErrors(); - } - - if ($customers && property_exists($customers, 'customers')) { - return $customers->customers[0]->id; - } - - return $this->createClient(); - } - - private function createClient() - { - $country = $this->square_driver->client->country ? $this->square_driver->client->country->iso_3166_2 : $this->square_driver->client->company->country()->iso_3166_2; - - /* Step two - create the customer */ - $billing_address = new \Square\Models\Address(); - $billing_address->setAddressLine1($this->square_driver->client->address1); - $billing_address->setAddressLine2($this->square_driver->client->address2); - $billing_address->setLocality($this->square_driver->client->city); - $billing_address->setAdministrativeDistrictLevel1($this->square_driver->client->state); - $billing_address->setPostalCode($this->square_driver->client->postal_code); - $billing_address->setCountry($country); - - $body = new \Square\Models\CreateCustomerRequest(); - $body->setGivenName($this->square_driver->client->present()->name()); - $body->setFamilyName(''); - $body->setEmailAddress($this->square_driver->client->present()->email()); - $body->setAddress($billing_address); - // $body->setPhoneNumber($this->square_driver->client->phone); - $body->setReferenceId($this->square_driver->client->number); - $body->setNote('Created by Invoice Ninja.'); - - $api_response = $this->square_driver - ->init() - ->square - ->getCustomersApi() - ->createCustomer($body); - - if ($api_response->isSuccess()) { - $result = $api_response->getResult(); - - return $result->getCustomer()->getId(); - } else { - $errors = $api_response->getErrors(); - nlog($errors); - return $this->processUnsuccessfulPayment($api_response); - } - } + } diff --git a/app/PaymentDrivers/SquarePaymentDriver.php b/app/PaymentDrivers/SquarePaymentDriver.php index d58aa4f7cf9a..079ef5c3626b 100644 --- a/app/PaymentDrivers/SquarePaymentDriver.php +++ b/app/PaymentDrivers/SquarePaymentDriver.php @@ -11,22 +11,27 @@ namespace App\PaymentDrivers; -use App\Http\Requests\Payments\PaymentWebhookRequest; -use App\Jobs\Util\SystemLogger; -use App\Models\ClientGatewayToken; -use App\Models\GatewayType; use App\Models\Invoice; use App\Models\Payment; +use App\Models\SystemLog; +use App\Models\GatewayType; use App\Models\PaymentHash; use App\Models\PaymentType; -use App\Models\SystemLog; +use App\Models\ClientContact; +use App\Factory\ClientFactory; +use App\Jobs\Util\SystemLogger; +use App\Utils\Traits\MakesHash; +use Square\Utils\WebhooksHelper; +use App\Models\ClientGatewayToken; +use App\Repositories\ClientRepository; +use Square\Models\WebhookSubscription; use App\PaymentDrivers\Square\CreditCard; use App\PaymentDrivers\Square\SquareWebhook; -use App\Utils\Traits\MakesHash; -use Square\Models\Builders\RefundPaymentRequestBuilder; +use App\Repositories\ClientContactRepository; use Square\Models\CreateWebhookSubscriptionRequest; -use Square\Models\WebhookSubscription; -use Square\Utils\WebhooksHelper; +use App\Http\Requests\Payments\PaymentWebhookRequest; +use App\PaymentDrivers\Factory\SquareCustomerFactory; +use Square\Models\Builders\RefundPaymentRequestBuilder; class SquarePaymentDriver extends BaseDriver { @@ -429,4 +434,205 @@ class SquarePaymentDriver extends BaseDriver return $amount; } + + public function auth(): bool + { + + $api_response = $this->init() + ->square + ->getCustomersApi() + ->listCustomers(); + + + return (bool) count($api_response->getErrors()) == 0; + + } + + public function importCustomers() + { + + $limit = 100; + + $api_response = $this->init() + ->square + ->getCustomersApi() + ->listCustomers(null, + $limit, + 'DEFAULT', + 'DESC' + ); + + if ($api_response->isSuccess()) { + + while ($api_response->getResult()->getCustomers()) { + + $customers = $api_response->getResult()->getCustomers(); + + $client_repo = new ClientRepository(new ClientContactRepository()); + + foreach($customers as $customer) + { + + $data = (new SquareCustomerFactory())->convertToNinja($customer, $this->company_gateway->company); + $client = ClientContact::where('company_id', $this->company_gateway->company_id)->where('email', $customer->getEmailAddress())->first()->client ?? false; + + if(!$client) + $client = $client_repo->save($data, ClientFactory::create($this->company_gateway->company_id, $this->company_gateway->user_id)); + + $this->client = $client; + + foreach($data['cards'] as $card) { + + if(ClientGatewayToken::where('company_id', $this->company_gateway->company_id)->where('token', $card['token'])->exists()) + continue; + + $this->storeGatewayToken($card); + + } + } + + $c = $api_response->getCursor(); + if ($c) { + + $api_response = $this->init() + ->square + ->getCustomersApi() + ->listCustomers( + $c, + $limit, + 'DEFAULT', + 'DESC' + ); + } else { + break; + } + + + } + + } + } + + private function findClient($email = null) + { + + $email_address_string = $email ?? $this->client->present()->email(); + + $email_address = new \Square\Models\CustomerTextFilter(); + $email_address->setExact($email_address_string); + + $filter = new \Square\Models\CustomerFilter(); + $filter->setEmailAddress($email_address); + + $query = new \Square\Models\CustomerQuery(); + $query->setFilter($filter); + + $body = new \Square\Models\SearchCustomersRequest(); + $body->setQuery($query); + + $api_response = $this->init() + ->square + ->getCustomersApi() + ->searchCustomers($body); + + $customers = false; + + if ($api_response->isSuccess()) { + $customers = $api_response->getBody(); + $customers = json_decode($customers); + + if (count([$api_response->getBody(), 1]) == 0) { + $customers = false; + } + } else { + $errors = $api_response->getErrors(); + } + + if ($customers && property_exists($customers, 'customers')) { + return $customers->customers[0]->id; + } + + return false; + + } + + public function findOrCreateClient() + { + if($customer_id = $this->findClient()) + return $customer_id; + + return $this->createClient(); + } + + private function createClient() + { + $country = $this->client->country ? $this->client->country->iso_3166_2 : $this->client->company->country()->iso_3166_2; + + /* Step two - create the customer */ + $billing_address = new \Square\Models\Address(); + $billing_address->setAddressLine1($this->client->address1); + $billing_address->setAddressLine2($this->client->address2); + $billing_address->setLocality($this->client->city); + $billing_address->setAdministrativeDistrictLevel1($this->client->state); + $billing_address->setPostalCode($this->client->postal_code); + $billing_address->setCountry($country); + + $body = new \Square\Models\CreateCustomerRequest(); + $body->setGivenName($this->client->present()->name()); + $body->setFamilyName(''); + $body->setEmailAddress($this->client->present()->email()); + $body->setAddress($billing_address); + $body->setReferenceId($this->client->number); + $body->setNote('Created by Invoice Ninja.'); + + $api_response = $this->init() + ->square + ->getCustomersApi() + ->createCustomer($body); + + if ($api_response->isSuccess()) { + $result = $api_response->getResult(); + + return $result->getCustomer()->getId(); + } else { + $errors = $api_response->getErrors(); + nlog($errors); + + $error = end($errors); + + $data = [ + 'response' => $error->getDetail(), + 'error' => $error->getDetail(), + 'error_code' => $error->getCode(), + ]; + + return $this->processUnsuccessfulTransaction($data); + + } + } + + + + + + + + + + + + + + + + + + + + + + + + + } diff --git a/app/PaymentDrivers/Stripe/ImportCustomers.php b/app/PaymentDrivers/Stripe/ImportCustomers.php index 6001ed84298e..aa874444398d 100644 --- a/app/PaymentDrivers/Stripe/ImportCustomers.php +++ b/app/PaymentDrivers/Stripe/ImportCustomers.php @@ -62,12 +62,6 @@ class ImportCustomers $this->addCustomer($customer); } - //handle - // if(is_array($customers->data) && end($customers->data) && array_key_exists('id', end($customers->data))) - // $starting_after = end($customers->data)['id']; - // else - // break; - $starting_after = isset(end($customers->data)['id']) ? end($customers->data)['id'] : false; if (!$starting_after) { diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index b128b6baa68b..f440debb0232 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -990,4 +990,20 @@ class StripePaymentDriver extends BaseDriver return mb_convert_encoding(pack('H*', $matches[1]), 'UTF-8', 'UCS-2BE'); }, $string); } + + public function auth(): bool + { + $this->init(); + + try { + $this->verifyConnect(); + return true; + } + catch(\Exception $e) { + + } + + return false; + + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 53f49f4f854c..8ab1e78c91f6 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -29,6 +29,8 @@ use App\Http\Middleware\SetDomainNameDb; use Illuminate\Queue\Events\JobProcessing; use App\Helpers\Mail\Office365MailTransport; use Illuminate\Database\Eloquent\Relations\Relation; +use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; class AppServiceProvider extends ServiceProvider { @@ -55,7 +57,7 @@ class AppServiceProvider extends ServiceProvider /* Defines the name used in polymorphic tables */ Relation::morphMap([ - 'invoices' => Invoice::class, + 'invoices' => Invoice::class, 'proposals' => Proposal::class, ]); @@ -119,6 +121,30 @@ class AppServiceProvider extends ServiceProvider return $this; }); + Mail::extend('brevo', function () { + return (new BrevoTransportFactory)->create( + new Dsn( + 'brevo+api', + 'default', + config('services.brevo.key') + ) + ); + }); + Mailer::macro('brevo_config', function (string $brevo_key) { + // @phpstan-ignore /** @phpstan-ignore-next-line **/ + Mailer::setSymfonyTransport( + (new BrevoTransportFactory)->create( + new Dsn( + 'brevo+api', + 'default', + $brevo_key + ) + ) + ); + + return $this; + }); + } public function register(): void diff --git a/app/Services/Client/Statement.php b/app/Services/Client/Statement.php index bedcacb34af4..163718ea6dbf 100644 --- a/app/Services/Client/Statement.php +++ b/app/Services/Client/Statement.php @@ -162,6 +162,7 @@ class Statement $ts->addGlobal(['show_credits' => $this->options['show_credits_table']]); $ts->addGlobal(['show_aging' => $this->options['show_aging_table']]); $ts->addGlobal(['show_payments' => $this->options['show_payments_table']]); + $ts->addGlobal(['currency_code' => $this->client->company->currency()->code]); $ts->build([ 'variables' => collect([$variables]), diff --git a/app/Services/Email/AdminEmail.php b/app/Services/Email/AdminEmail.php index a2a874ca2308..15af006de861 100644 --- a/app/Services/Email/AdminEmail.php +++ b/app/Services/Email/AdminEmail.php @@ -59,6 +59,8 @@ class AdminEmail implements ShouldQueue protected ?string $client_mailgun_endpoint = null; + protected ?string $client_brevo_secret = null; + private string $mailer = 'default'; public Mailable $mailable; @@ -82,7 +84,7 @@ class AdminEmail implements ShouldQueue MultiDB::setDb($this->company->db); $this->setOverride() - ->buildMailable(); + ->buildMailable(); if ($this->preFlightChecksFail()) { return; @@ -137,24 +139,28 @@ class AdminEmail implements ShouldQueue $mailer->mailgun_config($this->client_mailgun_secret, $this->client_mailgun_domain, $this->client_mailgun_endpoint); } + if ($this->client_brevo_secret) { + $mailer->brevo_config($this->client_brevo_secret); + } + /* Attempt the send! */ try { - nlog("Using mailer => ". $this->mailer. " ". now()->toDateTimeString()); + nlog("Using mailer => " . $this->mailer . " " . now()->toDateTimeString()); $mailer->send($this->mailable); - Cache::increment("email_quota".$this->company->account->key); + Cache::increment("email_quota" . $this->company->account->key); LightLogs::create(new EmailSuccess($this->company->company_key, $this->mailable->subject)) - ->send(); + ->send(); - } catch(\Symfony\Component\Mime\Exception\RfcComplianceException $e) { + } catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) { nlog("Mailer failed with a Logic Exception {$e->getMessage()}"); $this->fail(); $this->cleanUpMailers(); $this->logMailError($e->getMessage(), $this->company->clients()->first()); return; - } catch(\Symfony\Component\Mime\Exception\LogicException $e) { + } catch (\Symfony\Component\Mime\Exception\LogicException $e) { nlog("Mailer failed with a Logic Exception {$e->getMessage()}"); $this->fail(); $this->cleanUpMailers(); @@ -215,16 +221,16 @@ class AdminEmail implements ShouldQueue } /** - * On the hosted platform we scan all outbound email for - * spam. This sequence processes the filters we use on all - * emails. - * - * @return bool - */ + * On the hosted platform we scan all outbound email for + * spam. This sequence processes the filters we use on all + * emails. + * + * @return bool + */ public function preFlightChecksFail(): bool { /* Always send if disabled */ - if($this->override) { + if ($this->override) { return false; } @@ -248,7 +254,7 @@ class AdminEmail implements ShouldQueue } /* GMail users are uncapped */ - if (in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun'])) { + if (in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun', 'client_brevo'])) { return false; } @@ -337,6 +343,10 @@ class AdminEmail implements ShouldQueue $this->mailer = 'mailgun'; $this->setMailgunMailer(); return $this; + case 'client_brevo': + $this->mailer = 'brevo'; + $this->setBrevoMailer(); + return $this; default: $this->mailer = config('mail.default'); @@ -369,7 +379,7 @@ class AdminEmail implements ShouldQueue if (env($this->company->id . '_MAIL_FROM_ADDRESS')) { $this->mailable - ->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME'))); + ->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME'))); } } } @@ -390,6 +400,8 @@ class AdminEmail implements ShouldQueue $this->client_mailgun_endpoint = null; + $this->client_brevo_secret = null; + //always dump the drivers to prevent reuse app('mail.manager')->forgetMailers(); } @@ -452,7 +464,29 @@ class AdminEmail implements ShouldQueue $sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); $this->mailable - ->from($sending_email, $sending_user); + ->from($sending_email, $sending_user); + } + /** + * Configures Brevo using client supplied secret + * as the Mailer + */ + private function setBrevoMailer() + { + if (strlen($this->email_object->settings->brevo_secret) > 2) { + $this->client_brevo_secret = $this->email_object->settings->brevo_secret; + + } else { + $this->email_object->settings->email_sending_method = 'default'; + return $this->setMailDriver(); + } + + $user = $this->resolveSendingUser(); + + $sending_email = (isset($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email; + $sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); + + $this->mailable + ->from($sending_email, $sending_user); } /** @@ -474,7 +508,7 @@ class AdminEmail implements ShouldQueue $sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); $this->mailable - ->from($sending_email, $sending_user); + ->from($sending_email, $sending_user); } /** @@ -500,10 +534,10 @@ class AdminEmail implements ShouldQueue } $this->mailable - ->from($user->email, $user->name()) - ->withSymfonyMessage(function ($message) use ($token) { - $message->getHeaders()->addTextHeader('gmailtoken', $token); - }); + ->from($user->email, $user->name()) + ->withSymfonyMessage(function ($message) use ($token) { + $message->getHeaders()->addTextHeader('gmailtoken', $token); + }); } /** @@ -527,7 +561,7 @@ class AdminEmail implements ShouldQueue } $google->getClient()->setAccessToken(json_encode($user->oauth_user_token)); - } catch(\Exception $e) { + } catch (\Exception $e) { $this->logMailError('Gmail Token Invalid', $this->company->clients()->first()); $this->email_object->settings->email_sending_method = 'default'; return $this->setMailDriver(); @@ -547,7 +581,7 @@ class AdminEmail implements ShouldQueue * Now that our token is refreshed and valid we can boot the * mail driver at runtime and also set the token which will persist * just for this request. - */ + */ $token = $user->oauth_user_token->access_token; @@ -558,10 +592,10 @@ class AdminEmail implements ShouldQueue } $this->mailable - ->from($user->email, $user->name()) - ->withSymfonyMessage(function ($message) use ($token) { - $message->getHeaders()->addTextHeader('gmailtoken', $token); - }); + ->from($user->email, $user->name()) + ->withSymfonyMessage(function ($message) use ($token) { + $message->getHeaders()->addTextHeader('gmailtoken', $token); + }); } /** @@ -573,21 +607,23 @@ class AdminEmail implements ShouldQueue */ private function logMailError($errors, $recipient_object): void { - (new SystemLogger( - $errors, - SystemLog::CATEGORY_MAIL, - SystemLog::EVENT_MAIL_SEND, - SystemLog::TYPE_FAILURE, - $recipient_object, - $this->company - ))->handle(); + ( + new SystemLogger( + $errors, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_SEND, + SystemLog::TYPE_FAILURE, + $recipient_object, + $this->company + ) + )->handle(); $job_failure = new EmailFailure($this->company->company_key); $job_failure->string_metric5 = 'failed_email'; $job_failure->string_metric6 = substr($errors, 0, 150); LightLogs::create($job_failure) - ->send(); + ->send(); $job_failure = null; } @@ -608,8 +644,8 @@ class AdminEmail implements ShouldQueue $token = json_decode($guzzle->post($url, [ 'form_params' => [ - 'client_id' => config('ninja.o365.client_id') , - 'client_secret' => config('ninja.o365.client_secret') , + 'client_id' => config('ninja.o365.client_id'), + 'client_secret' => config('ninja.o365.client_secret'), 'scope' => 'email Mail.Send offline_access profile User.Read openid', 'grant_type' => 'refresh_token', 'refresh_token' => $user->oauth_user_refresh_token diff --git a/app/Services/Email/Email.php b/app/Services/Email/Email.php index f47189009b3f..c07d34515a0f 100644 --- a/app/Services/Email/Email.php +++ b/app/Services/Email/Email.php @@ -41,6 +41,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Mail; +use Log; use Turbo124\Beacon\Facades\LightLogs; class Email implements ShouldQueue @@ -72,6 +73,9 @@ class Email implements ShouldQueue /** MailGun endpoint */ protected ?string $client_mailgun_endpoint = null; + /** Brevo endpoint */ + protected ?string $client_brevo_secret = null; + /** Default mailer */ private string $mailer = 'default'; @@ -100,9 +104,9 @@ class Email implements ShouldQueue MultiDB::setDb($this->company->db); $this->setOverride() - ->initModels() - ->setDefaults() - ->buildMailable(); + ->initModels() + ->setDefaults() + ->buildMailable(); /** Ensure quota's on hosted platform are respected. :) */ $this->setMailDriver(); @@ -246,10 +250,10 @@ class Email implements ShouldQueue private function incrementEmailCounter(): void { - if(in_array($this->mailer, ['default','mailgun'])) + if(in_array($this->mailer, ['default','mailgun','postmark'])) Cache::increment("email_quota".$this->company->account->key); } - + /** * Attempts to send the email * @@ -261,6 +265,7 @@ class Email implements ShouldQueue /* Init the mailer*/ $mailer = Mail::mailer($this->mailer); + /* Additional configuration if using a client third party mailer */ if ($this->client_postmark_secret) { $mailer->postmark_config($this->client_postmark_secret); @@ -270,24 +275,28 @@ class Email implements ShouldQueue $mailer->mailgun_config($this->client_mailgun_secret, $this->client_mailgun_domain, $this->client_mailgun_endpoint); } + if ($this->client_brevo_secret) { + $mailer->brevo_config($this->client_brevo_secret); + } + /* Attempt the send! */ try { - nlog("Using mailer => ". $this->mailer. " ". now()->toDateTimeString()); + nlog("Using mailer => " . $this->mailer . " " . now()->toDateTimeString()); $mailer->send($this->mailable); $this->incrementEmailCounter(); LightLogs::create(new EmailSuccess($this->company->company_key, $this->mailable->subject)) - ->send(); + ->send(); - } catch(\Symfony\Component\Mime\Exception\RfcComplianceException $e) { + } catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) { nlog("Mailer failed with a Logic Exception {$e->getMessage()}"); $this->fail(); $this->cleanUpMailers(); $this->logMailError($e->getMessage(), $this->company->clients()->first()); return; - } catch(\Symfony\Component\Mime\Exception\LogicException $e) { + } catch (\Symfony\Component\Mime\Exception\LogicException $e) { nlog("Mailer failed with a Logic Exception {$e->getMessage()}"); $this->fail(); $this->cleanUpMailers(); @@ -367,16 +376,16 @@ class Email implements ShouldQueue } /** - * On the hosted platform we scan all outbound email for - * spam. This sequence processes the filters we use on all - * emails. - * - * @return bool - */ + * On the hosted platform we scan all outbound email for + * spam. This sequence processes the filters we use on all + * emails. + * + * @return bool + */ public function preFlightChecksFail(): bool { /* Always send if disabled */ - if($this->override) { + if ($this->override) { return false; } @@ -400,7 +409,7 @@ class Email implements ShouldQueue } /* GMail users are uncapped */ - if (in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun'])) { + if (in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun', 'client_brevo'])) { return false; } @@ -461,7 +470,7 @@ class Email implements ShouldQueue return true; } - if($address_object->name == " " || $address_object->name == "") { + if ($address_object->name == " " || $address_object->name == "") { return true; } } @@ -479,7 +488,7 @@ class Email implements ShouldQueue } $this->mailable - ->from(config('services.mailgun.from.address'), $email_from_name); + ->from(config('services.mailgun.from.address'), $email_from_name); } @@ -498,7 +507,7 @@ class Email implements ShouldQueue // return $this; // } - if(Ninja::isHosted() && $this->company->account->isPaid() && $this->email_object->settings->email_sending_method == 'default') { + if (Ninja::isHosted() && $this->company->account->isPaid() && $this->email_object->settings->email_sending_method == 'default') { try { @@ -507,7 +516,7 @@ class Email implements ShouldQueue $domain = explode("@", $email)[1] ?? ""; $dns = dns_get_record($domain, DNS_MX); $server = $dns[0]["target"]; - if(stripos($server, "outlook.com") !== false) { + if (stripos($server, "outlook.com") !== false) { if (property_exists($this->email_object->settings, 'email_from_name') && strlen($this->email_object->settings->email_from_name) > 1) { $email_from_name = $this->email_object->settings->email_from_name; @@ -518,12 +527,12 @@ class Email implements ShouldQueue $this->mailer = 'postmark'; $this->client_postmark_secret = config('services.postmark-outlook.token'); $this->mailable - ->from(config('services.postmark-outlook.from.address'), $email_from_name); + ->from(config('services.postmark-outlook.from.address'), $email_from_name); return $this; - + } - } catch(\Exception $e) { + } catch (\Exception $e) { nlog("problem switching outlook driver - hosted"); nlog($e->getMessage()); } @@ -555,6 +564,10 @@ class Email implements ShouldQueue $this->mailer = 'mailgun'; $this->setMailgunMailer(); return $this; + case 'client_brevo': + $this->mailer = 'brevo'; + $this->setBrevoMailer(); + return $this; case 'smtp': $this->mailer = 'smtp'; $this->configureSmtpMailer(); @@ -600,11 +613,11 @@ class Email implements ShouldQueue $user = $this->resolveSendingUser(); - $sending_email = (isset($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email; - $sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); + $sending_email = (isset ($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email; + $sending_user = (isset ($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); $this->mailable - ->from($sending_email, $sending_user); + ->from($sending_email, $sending_user); } @@ -627,7 +640,7 @@ class Email implements ShouldQueue if (env($this->company->id . '_MAIL_FROM_ADDRESS')) { $this->mailable - ->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME'))); + ->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME'))); } } } @@ -648,6 +661,8 @@ class Email implements ShouldQueue $this->client_mailgun_endpoint = null; + $this->client_brevo_secret = null; + //always dump the drivers to prevent reuse app('mail.manager')->forgetMailers(); } @@ -706,11 +721,33 @@ class Email implements ShouldQueue $user = $this->resolveSendingUser(); - $sending_email = (isset($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email; - $sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); + $sending_email = (isset ($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email; + $sending_user = (isset ($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); $this->mailable - ->from($sending_email, $sending_user); + ->from($sending_email, $sending_user); + } + /** + * Configures Brevo using client supplied secret + * as the Mailer + */ + private function setBrevoMailer() + { + if (strlen($this->email_object->settings->brevo_secret) > 2) { + $this->client_brevo_secret = $this->email_object->settings->brevo_secret; + + } else { + $this->email_object->settings->email_sending_method = 'default'; + return $this->setMailDriver(); + } + + $user = $this->resolveSendingUser(); + + $sending_email = (isset ($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email; + $sending_user = (isset ($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); + + $this->mailable + ->from($sending_email, $sending_user); } /** @@ -728,11 +765,11 @@ class Email implements ShouldQueue $user = $this->resolveSendingUser(); - $sending_email = (isset($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email; - $sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); + $sending_email = (isset ($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email; + $sending_user = (isset ($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); $this->mailable - ->from($sending_email, $sending_user); + ->from($sending_email, $sending_user); } /** @@ -758,10 +795,10 @@ class Email implements ShouldQueue } $this->mailable - ->from($user->email, $user->name()) - ->withSymfonyMessage(function ($message) use ($token) { - $message->getHeaders()->addTextHeader('gmailtoken', $token); - }); + ->from($user->email, $user->name()) + ->withSymfonyMessage(function ($message) use ($token) { + $message->getHeaders()->addTextHeader('gmailtoken', $token); + }); } /** @@ -785,7 +822,7 @@ class Email implements ShouldQueue } $google->getClient()->setAccessToken(json_encode($user->oauth_user_token)); - } catch(\Exception $e) { + } catch (\Exception $e) { $this->logMailError('Gmail Token Invalid', $this->company->clients()->first()); $this->email_object->settings->email_sending_method = 'default'; return $this->setMailDriver(); @@ -805,7 +842,7 @@ class Email implements ShouldQueue * Now that our token is refreshed and valid we can boot the * mail driver at runtime and also set the token which will persist * just for this request. - */ + */ $token = $user->oauth_user_token->access_token; @@ -816,10 +853,10 @@ class Email implements ShouldQueue } $this->mailable - ->from($user->email, $user->name()) - ->withSymfonyMessage(function ($message) use ($token) { - $message->getHeaders()->addTextHeader('gmailtoken', $token); - }); + ->from($user->email, $user->name()) + ->withSymfonyMessage(function ($message) use ($token) { + $message->getHeaders()->addTextHeader('gmailtoken', $token); + }); } /** @@ -831,21 +868,23 @@ class Email implements ShouldQueue */ private function logMailError($errors, $recipient_object): void { - (new SystemLogger( - $errors, - SystemLog::CATEGORY_MAIL, - SystemLog::EVENT_MAIL_SEND, - SystemLog::TYPE_FAILURE, - $recipient_object, - $this->company - ))->handle(); + ( + new SystemLogger( + $errors, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_SEND, + SystemLog::TYPE_FAILURE, + $recipient_object, + $this->company + ) + )->handle(); $job_failure = new EmailFailure($this->company->company_key); $job_failure->string_metric5 = 'failed_email'; $job_failure->string_metric6 = substr($errors, 0, 150); LightLogs::create($job_failure) - ->send(); + ->send(); $job_failure = null; } @@ -866,8 +905,8 @@ class Email implements ShouldQueue $token = json_decode($guzzle->post($url, [ 'form_params' => [ - 'client_id' => config('ninja.o365.client_id') , - 'client_secret' => config('ninja.o365.client_secret') , + 'client_id' => config('ninja.o365.client_id'), + 'client_secret' => config('ninja.o365.client_secret'), 'scope' => 'email Mail.Send offline_access profile User.Read openid', 'grant_type' => 'refresh_token', 'refresh_token' => $user->oauth_user_refresh_token diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index effe34daceba..dbe57e361b3c 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -544,7 +544,7 @@ class InvoiceService return $this; } - public function fillDefaults() + public function fillDefaults(bool $is_recurring = false) { $this->invoice->load('client.company'); @@ -571,7 +571,7 @@ class InvoiceService $this->invoice->exchange_rate = $this->invoice->client->setExchangeRate(); } - if ($this->invoice->client->getSetting('auto_bill_standard_invoices')) { + if ($is_recurring && $this->invoice->client->getSetting('auto_bill_standard_invoices')) { $this->invoice->auto_bill_enabled = true; } diff --git a/app/Services/Pdf/PdfMock.php b/app/Services/Pdf/PdfMock.php index d6fb4e5aa53c..1afc3ef8a49b 100644 --- a/app/Services/Pdf/PdfMock.php +++ b/app/Services/Pdf/PdfMock.php @@ -113,24 +113,30 @@ class PdfMock /** @var \App\Models\Invoice | \App\Models\Credit | \App\Models\Quote $entity */ $entity = Invoice::factory()->make(); $entity->client = Client::factory()->make(['settings' => $settings]); + $entity->client->setRelation('company', $this->company); $entity->invitation = InvoiceInvitation::factory()->make(); break; case 'quote': /** @var \App\Models\Invoice | \App\Models\Credit | \App\Models\Quote $entity */ $entity = Quote::factory()->make(); $entity->client = Client::factory()->make(['settings' => $settings]); + $entity->client->setRelation('company', $this->company); $entity->invitation = QuoteInvitation::factory()->make(); break; case 'credit': /** @var \App\Models\Invoice | \App\Models\Credit | \App\Models\Quote $entity */ $entity = Credit::factory()->make(); $entity->client = Client::factory()->make(['settings' => $settings]); + $entity->client->setRelation('company', $this->company); $entity->invitation = CreditInvitation::factory()->make(); break; case 'purchase_order': - /** @var \App\Models\Invoice | \App\Models\Credit | \App\Models\Quote $entity */ + + /** @var \App\Models\PurchaseOrder $entity */ $entity = PurchaseOrder::factory()->make(); - $entity->client = Client::factory()->make(['settings' => $settings]); + // $entity->client = Client::factory()->make(['settings' => $settings]); + $entity->vendor = Vendor::factory()->make(); + $entity->vendor->setRelation('company', $this->company); $entity->invitation = PurchaseOrderInvitation::factory()->make(); break; case PurchaseOrder::class: @@ -138,17 +144,17 @@ class PdfMock $entity = PurchaseOrder::factory()->make(); $entity->invitation = PurchaseOrderInvitation::factory()->make(); $entity->vendor = Vendor::factory()->make(); + $entity->invitation->setRelation('company', $this->company); break; default: $entity = false; break; } - $entity->tax_map = $this->getTaxMap(); $entity->total_tax_map = $this->getTotalTaxMap(); $entity->invitation->company = $this->company; - + return $entity; } diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index e9fcced20a5e..d3710ba722b0 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -613,8 +613,6 @@ class TemplateService $this->payment = $payment; - $this->addGlobal(['currency_code' => $payment->currency->code ?? $this->company->currency()->code]); - $credits = $payment->credits->map(function ($credit) use ($payment) { return [ 'credit' => $credit->number, diff --git a/composer.json b/composer.json index 0336c1ad12d0..21ea79984be9 100644 --- a/composer.json +++ b/composer.json @@ -48,6 +48,7 @@ "doctrine/dbal": "^3.0", "eway/eway-rapid-php": "^1.3", "fakerphp/faker": "^1.14", + "getbrevo/brevo-php": "^1.0", "gocardless/gocardless-pro": "^4.12", "google/apiclient": "^2.7", "guzzlehttp/guzzle": "^7.2", @@ -95,7 +96,8 @@ "sprain/swiss-qr-bill": "^4.3", "square/square": "30.0.0.*", "stripe/stripe-php": "^12", - "symfony/http-client": "^7.0", + "symfony/brevo-mailer": "6.4", + "symfony/http-client": "^6.0", "symfony/mailgun-mailer": "^6.1", "symfony/postmark-mailer": "^6.1", "turbo124/beacon": "^1.5", diff --git a/composer.lock b/composer.lock index 7f7b39bbd5d0..70f2ca5f20c4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "63d177f80df79731528c8669d192ded6", + "content-hash": "fdea921aefca562c17db327acd8df062", "packages": [ { "name": "afosto/yaac", @@ -1343,16 +1343,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.301.1", + "version": "3.301.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "0a910d2b35e7087337cdf3569dc9b6ce232aafba" + "reference": "7f8180275e624cb566d8af77d2f1c958bf5be35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0a910d2b35e7087337cdf3569dc9b6ce232aafba", - "reference": "0a910d2b35e7087337cdf3569dc9b6ce232aafba", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7f8180275e624cb566d8af77d2f1c958bf5be35b", + "reference": "7f8180275e624cb566d8af77d2f1c958bf5be35b", "shasum": "" }, "require": { @@ -1432,9 +1432,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.301.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.301.2" }, - "time": "2024-03-15T18:14:42+00:00" + "time": "2024-03-18T18:06:18+00:00" }, { "name": "bacon/bacon-qr-code", @@ -3269,6 +3269,69 @@ ], "time": "2023-10-12T05:21:21+00:00" }, + { + "name": "getbrevo/brevo-php", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/getbrevo/brevo-php.git", + "reference": "6c3286e62327277fd8445cddb057d44e850722c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getbrevo/brevo-php/zipball/6c3286e62327277fd8445cddb057d44e850722c0", + "reference": "6c3286e62327277fd8445cddb057d44e850722c0", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/guzzle": "^7.4.0", + "php": ">=5.6" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~1.12", + "phpunit/phpunit": "^4.8", + "squizlabs/php_codesniffer": "~2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x.x-dev" + } + }, + "autoload": { + "psr-4": { + "Brevo\\Client\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brevo Developers", + "email": "contact@brevo.com", + "homepage": "https://www.brevo.com/" + } + ], + "description": "Official Brevo provided RESTFul API V3 php library", + "homepage": "https://github.com/getbrevo/brevo-php", + "keywords": [ + "api", + "brevo", + "php", + "sdk", + "swagger" + ], + "support": { + "issues": "https://github.com/getbrevo/brevo-php/issues", + "source": "https://github.com/getbrevo/brevo-php/tree/v1.0.2" + }, + "time": "2023-07-14T10:00:50+00:00" + }, { "name": "gocardless/gocardless-pro", "version": "4.28.0", @@ -3455,16 +3518,16 @@ }, { "name": "google/apiclient-services", - "version": "v0.339.0", + "version": "v0.340.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-api-php-client-services.git", - "reference": "5662d2ab3da41ac0e0e99db221a8c22c511c8f9c" + "reference": "c89999ea477da2b0803b2b4f14c9e7fc23b6344a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/5662d2ab3da41ac0e0e99db221a8c22c511c8f9c", - "reference": "5662d2ab3da41ac0e0e99db221a8c22c511c8f9c", + "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/c89999ea477da2b0803b2b4f14c9e7fc23b6344a", + "reference": "c89999ea477da2b0803b2b4f14c9e7fc23b6344a", "shasum": "" }, "require": { @@ -3493,9 +3556,9 @@ ], "support": { "issues": "https://github.com/googleapis/google-api-php-client-services/issues", - "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.339.0" + "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.340.0" }, - "time": "2024-03-10T01:06:17+00:00" + "time": "2024-03-17T00:56:17+00:00" }, { "name": "google/auth", @@ -12228,6 +12291,75 @@ }, "time": "2023-10-16T18:04:12+00:00" }, + { + "name": "symfony/brevo-mailer", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/brevo-mailer.git", + "reference": "83db87e0f44653cd40aeef54a2f57ab6bfccadfe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/brevo-mailer/zipball/83db87e0f44653cd40aeef54a2f57ab6bfccadfe", + "reference": "83db87e0f44653cd40aeef54a2f57ab6bfccadfe", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/mailer": "^5.4.21|^6.2.7|^7.0" + }, + "conflict": { + "symfony/mime": "<6.2" + }, + "require-dev": { + "symfony/http-client": "^6.3|^7.0", + "symfony/webhook": "^6.3|^7.0" + }, + "type": "symfony-mailer-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\Bridge\\Brevo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Pierre Tanguy", + "homepage": "https://github.com/petanguy" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Brevo Mailer Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/brevo-mailer/tree/v7.0.0-RC1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-06T17:20:05+00:00" + }, { "name": "symfony/console", "version": "v6.4.4", @@ -12814,27 +12946,28 @@ }, { "name": "symfony/http-client", - "version": "v7.0.5", + "version": "v6.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "425f462a59d8030703ee04a9e1c666575ed5db3b" + "reference": "f3c86a60a3615f466333a11fd42010d4382a82c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/425f462a59d8030703ee04a9e1c666575ed5db3b", - "reference": "425f462a59d8030703ee04a9e1c666575ed5db3b", + "url": "https://api.github.com/repos/symfony/http-client/zipball/f3c86a60a3615f466333a11fd42010d4382a82c7", + "reference": "f3c86a60a3615f466333a11fd42010d4382a82c7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.1", "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "^3", "symfony/service-contracts": "^2.5|^3" }, "conflict": { "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.4" + "symfony/http-foundation": "<6.3" }, "provide": { "php-http/async-client-implementation": "*", @@ -12851,11 +12984,11 @@ "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -12886,7 +13019,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.0.5" + "source": "https://github.com/symfony/http-client/tree/v6.4.5" }, "funding": [ { @@ -12902,7 +13035,7 @@ "type": "tidelift" } ], - "time": "2024-03-02T12:46:12+00:00" + "time": "2024-03-02T12:45:30+00:00" }, { "name": "symfony/http-client-contracts", @@ -16807,16 +16940,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.51.0", + "version": "v3.52.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "127fa74f010da99053e3f5b62672615b72dd6efd" + "reference": "a3564bd66f4bce9bc871ef18b690e2dc67a7f969" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/127fa74f010da99053e3f5b62672615b72dd6efd", - "reference": "127fa74f010da99053e3f5b62672615b72dd6efd", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a3564bd66f4bce9bc871ef18b690e2dc67a7f969", + "reference": "a3564bd66f4bce9bc871ef18b690e2dc67a7f969", "shasum": "" }, "require": { @@ -16887,7 +17020,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.51.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.52.0" }, "funding": [ { @@ -16895,7 +17028,7 @@ "type": "github" } ], - "time": "2024-02-28T19:50:06+00:00" + "time": "2024-03-18T18:40:11+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -17621,16 +17754,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.62", + "version": "1.10.63", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "cd5c8a1660ed3540b211407c77abf4af193a6af9" + "reference": "ad12836d9ca227301f5fb9960979574ed8628339" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd5c8a1660ed3540b211407c77abf4af193a6af9", - "reference": "cd5c8a1660ed3540b211407c77abf4af193a6af9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ad12836d9ca227301f5fb9960979574ed8628339", + "reference": "ad12836d9ca227301f5fb9960979574ed8628339", "shasum": "" }, "require": { @@ -17679,7 +17812,7 @@ "type": "tidelift" } ], - "time": "2024-03-13T12:27:20+00:00" + "time": "2024-03-18T16:53:53+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/config/mail.php b/config/mail.php index 0cd235b4048f..a9a86b08acc3 100644 --- a/config/mail.php +++ b/config/mail.php @@ -28,7 +28,7 @@ return [ | sending an e-mail. You will specify which one you are using for your | mailers below. You are free to add additional mailers as required. | - | Supported: "smtp", "sendmail", "mailgun", "ses", + | Supported: "smtp", "sendmail", "mailgun", "brevo", "ses", | "postmark", "log", "array", "failover" | */ @@ -54,6 +54,10 @@ return [ 'transport' => 'mailgun', ], + 'brevo' => [ + 'transport' => 'brevo', + ], + 'postmark' => [ 'transport' => 'postmark', ], diff --git a/config/ninja.php b/config/ninja.php index 9004e8d22d3f..a7ac185eb8d5 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -84,9 +84,15 @@ return [ 'username' => 'user@example.com', 'clientname' => 'client@example.com', 'password' => 'password', + 'gocardless' => env('GOCARDLESS_KEYS',''), + 'square' => env('SQUARE_KEYS',''), + 'eway' => env('EWAY_KEYS',''), + 'mollie', env('MOLLIE_KEYS',''), + 'paytrace' => env('PAYTRACE_KEYS',''), 'stripe' => env('STRIPE_KEYS', ''), 'paypal' => env('PAYPAL_KEYS', ''), 'ppcp' => env('PPCP_KEYS', ''), + 'forte' => env('FORTE_KEYS', ''), 'paypal_rest' => env('PAYPAL_REST_KEYS', ''), 'authorize' => env('AUTHORIZE_KEYS', ''), 'checkout' => env('CHECKOUT_KEYS', ''), @@ -94,11 +100,6 @@ return [ 'test_email' => env('TEST_EMAIL', 'test@example.com'), 'wepay' => env('WEPAY_KEYS', ''), 'braintree' => env('BRAINTREE_KEYS', ''), - 'paytrace' => [ - 'username' => env('PAYTRACE_U', ''), - 'password' => env('PAYTRACE_P', ''), - 'decrypted' => env('PAYTRACE_KEYS', ''), - ], 'mollie' => env('MOLLIE_KEYS', ''), 'square' => env('SQUARE_KEYS', ''), ], @@ -254,4 +255,6 @@ return [ 'public_key' => env('NINJA_PUBLIC_KEY', false), 'private_key' => env('NINJA_PRIVATE_KEY', false), ], + 'upload_extensions' => env('ADDITIONAL_UPLOAD_EXTENSIONS', false), + ]; diff --git a/config/services.php b/config/services.php index e8f12e34cb4d..ae434aba6575 100644 --- a/config/services.php +++ b/config/services.php @@ -12,7 +12,7 @@ return [ |-------------------------------------------------------------------------- | | This file is for storing the credentials for third party services such - | as Mailgun, Postmark, AWS and more. This file provides the de facto + | as Mailgun, Brevo, Postmark, AWS and more. This file provides the de facto | location for this type of information, allowing packages to have | a conventional file to locate the various service credentials. | @@ -30,6 +30,10 @@ return [ ], ], + 'brevo' => [ + 'key' => env('BREVO_SECRET', ''), + ], + 'postmark' => [ 'token' => env('POSTMARK_SECRET', ''), ], @@ -67,8 +71,8 @@ return [ ], 'stripe' => [ - 'model' => App\Models\User::class, - 'key' => env('STRIPE_KEY'), + 'model' => App\Models\User::class, + 'key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), ], diff --git a/database/migrations/2024_03_14_201844_adjust_discount_column_max_resolution.php b/database/migrations/2024_03_14_201844_adjust_discount_column_max_resolution.php index 3aa0099e3cf0..ad36915c62d2 100644 --- a/database/migrations/2024_03_14_201844_adjust_discount_column_max_resolution.php +++ b/database/migrations/2024_03_14_201844_adjust_discount_column_max_resolution.php @@ -16,7 +16,6 @@ return new class extends Migration $table->decimal('discount', 20, 6)->default(0)->change(); }); - Schema::table('credits', function (Blueprint $table) { $table->decimal('discount', 20, 6)->default(0)->change(); }); diff --git a/lang/en/texts.php b/lang/en/texts.php index e6a226b2c595..b721758bb774 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -2197,6 +2197,8 @@ $lang = array( 'encryption' => 'Encryption', 'mailgun_domain' => 'Mailgun Domain', 'mailgun_private_key' => 'Mailgun Private Key', + 'brevo_domain' => 'Brevo Domain', + 'brevo_private_key' => 'Brevo Private Key', 'send_test_email' => 'Send test email', 'select_label' => 'Select Label', 'label' => 'Label', @@ -4849,6 +4851,7 @@ $lang = array( 'email_alignment' => 'Email Alignment', 'pdf_preview_location' => 'PDF Preview Location', 'mailgun' => 'Mailgun', + 'brevo' => 'Brevo', 'postmark' => 'Postmark', 'microsoft' => 'Microsoft', 'click_plus_to_create_record' => 'Click + to create a record', @@ -5258,6 +5261,9 @@ $lang = array( 'select_email_provider' => 'Set your email as the sending user', 'purchase_order_items' => 'Purchase Order Items', 'csv_rows_length' => 'No data found in this CSV file', + 'accept_payments_online' => 'Accept Payments Online', + 'all_payment_gateways' => 'View all payment gateways', + 'product_cost' => 'Product cost', ); return $lang; diff --git a/lang/fr_CA/texts.php b/lang/fr_CA/texts.php index 9faef9cfe54c..b69b5fca52d9 100644 --- a/lang/fr_CA/texts.php +++ b/lang/fr_CA/texts.php @@ -460,7 +460,7 @@ $lang = array( 'edit_token' => 'Éditer le jeton', 'delete_token' => 'Supprimer le jeton', 'token' => 'Jeton', - 'add_gateway' => 'Add Payment Gateway', + 'add_gateway' => 'Ajouter une passerelle de paiement', 'delete_gateway' => 'Supprimer la passerelle', 'edit_gateway' => 'Éditer la passerelle', 'updated_gateway' => 'La passerelle a été mise à jour', @@ -5248,9 +5248,11 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'payment_type_help' => 'Définit le type de paiement manuel par défaut.', 'quote_valid_until_help' => 'Le nombre de jours pour lesquels la soumission est valide', 'expense_payment_type_help' => 'Le type de paiement de dépenses par défaut à utiliser', - 'paylater' => 'Pay in 4', - 'payment_provider' => 'Payment Provider', - + 'paylater' => 'Payer en 4', + 'payment_provider' => 'Fournisseur de paiement', + 'select_email_provider' => 'Définir le courriel pour l\'envoi', + 'purchase_order_items' => 'Articles du bon d\'achat', + 'csv_rows_length' => 'Aucune donnée dans ce fichier CSV', ); return $lang; diff --git a/openapi/api-docs.yaml b/openapi/api-docs.yaml index 3d95b07ea714..593cf67846a0 100644 --- a/openapi/api-docs.yaml +++ b/openapi/api-docs.yaml @@ -6,7 +6,7 @@ info:
The Invoice Ninja API is organized around REST and returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs. -
+
termsOfService: 'https://invoiceninja.github.io/docs/legal/terms_of_service/#page-content' @@ -17,10 +17,9 @@ info: url: 'https://www.elastic.co/licensing/elastic-license' version: 5.8.34 servers: - - - url: 'https://demo.invoiceninja.com' - description: | - ## Demo API Server InvoiceNinja. + - url: "https://demo.invoiceninja.com" + description: | + ## Demo API Server InvoiceNinja. You can use the demo API key `TOKEN` to test the endpoints from within this API spec paths: /api/v1/activities: @@ -49,10 +48,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Activity' @@ -114,9 +113,9 @@ paths: - login summary: "Attempts authentication" description: | - After authenticating with the API, the returned object is a CompanyUser object which is a bridge linking the user to the company. + After authenticating with the API, the returned object is a CompanyUser object which is a bridge linking the user to the company. - The company user object itself contains the users permissions (admin/owner or fine grained permissions) You will most likely want to + The company user object itself contains the users permissions (admin/owner or fine grained permissions) You will most likely want to also include in the response of this object both the company and the user object, this can be done by using the include parameter. /api/v1/login?include=company,user @@ -186,7 +185,7 @@ paths: - refresh summary: "Refresh data by timestamp" description: | - Refreshes the dataset. + Refreshes the dataset. This endpoint can be used if you only need to access the most recent data from a certain point in time. For example, if you only want to retrieve The most recent data from the last time you accessed the system, you would pass the query parameter ?updated_at=1676173763. (unix timestamp) @@ -299,10 +298,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/BankIntegration' @@ -755,10 +754,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/BankTransaction' @@ -1140,10 +1139,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/BankTransactionRule' @@ -1516,10 +1515,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/ClientGatewayToken' @@ -1809,10 +1808,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Company' @@ -2208,10 +2207,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/CompanyGateway' @@ -2530,10 +2529,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/CompanyLedger' @@ -2617,7 +2616,7 @@ paths: $ref: "#/components/responses/422" default: $ref: "#/components/responses/default" - + /api/v1/designs: get: tags: @@ -2642,10 +2641,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Design' @@ -2958,10 +2957,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Document' @@ -3059,10 +3058,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/ExpenseCategory' @@ -3363,10 +3362,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Expense' @@ -3756,10 +3755,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/GroupSetting' @@ -4217,7 +4216,7 @@ paths: $ref: "#/components/responses/422" default: $ref: "#/components/responses/default" - + /api/v1/claim_license: get: tags: @@ -4427,7 +4426,7 @@ paths: $ref: "#/components/responses/422" default: $ref: "#/components/responses/default" - + /api/v1/payment_terms: get: tags: @@ -4452,10 +4451,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/PaymentTerm' @@ -4894,10 +4893,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/RecurringExpense' @@ -5237,7 +5236,7 @@ paths: $ref: "#/components/responses/422" default: $ref: "#/components/responses/default" - + /api/v1/recurring_quotes: get: tags: @@ -5261,10 +5260,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/RecurringQuote' @@ -6165,10 +6164,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Subscription' @@ -6514,10 +6513,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/SystemLog' @@ -6573,7 +6572,7 @@ paths: $ref: "#/components/responses/422" default: $ref: "#/components/responses/default" - + /api/v1/task_schedulers/: get: tags: @@ -6827,10 +6826,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/TaskStatus' @@ -7140,10 +7139,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/TaxRate' @@ -7457,10 +7456,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/CompanyToken' @@ -7765,10 +7764,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/User' @@ -8169,7 +8168,7 @@ paths: $ref: "#/components/responses/422" default: $ref: "#/components/responses/default" - + /api/v1/webcron: get: tags: @@ -8221,10 +8220,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Webhook' @@ -8513,7 +8512,7 @@ paths: - products summary: "List products" description: | - Lists products, search and filters allow fine grained lists to be generated. + Lists products, search and filters allow fine grained lists to be generated. Query parameters can be added to perform fine grained filtering of the products list, these are handled by the ProductFilters class which defines the methods available operationId: getProducts @@ -8548,7 +8547,7 @@ paths: required: false schema: type: string - example: id|desc product_key|desc + example: id|desc product_key|desc responses: 200: description: "A list of products" @@ -8561,10 +8560,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Product' @@ -8974,10 +8973,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Task' @@ -9410,10 +9409,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Project' @@ -9796,31 +9795,31 @@ paths: summary: 'List clients' description: | When retrieving a list of clients you can also chain query parameters in order to filter the dataset that is returned. For example, you can send a request to the following URL to retrieve clients that have a balance greater than 1000:\ - + ``` /api/v1/clients?balance=gt:1000 - ``` - + ``` + You can also sort the results by adding a sort parameter. The following example will sort the results by the client name in descending order:\ - + ``` /api/v1/clients?sort=name|desc ``` You can also combine multiple filters together. The following example will return clients that have a balance greater than 1000 and are not deleted and have a name that starts with "Bob":\ - + ``` /api/v1/clients?balance=gt:1000&name=Bob* ``` If you wish to retrieve child relations, you can also combine the query parameter `?include=` with a comma separated list of relationships:\ - + ``` /api/v1/clients?include=activities,ledger,system_logs' ``` The per_page and page variables allow pagination of the list of clients. The following example will return the second page of clients with 15 clients per page:\ - + ``` /api/v1/clients?per_page=15&page=2 ``` @@ -9894,7 +9893,7 @@ paths: required: false schema: type: string - example: id|desc name|desc balance|asc + example: id|desc name|desc balance|asc responses: 200: @@ -9908,10 +9907,10 @@ paths: $ref: '#/components/headers/X-RateLimit-Limit' content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Client' @@ -9938,7 +9937,7 @@ paths: summary: 'Create client' description: | Adds a client to a company - + When creating (or updating) a client you must include the child contacts with all mutating requests. Client contacts cannot be modified in isolation. operationId: storeClient @@ -10203,7 +10202,7 @@ paths: application/json: schema: $ref: '#/components/schemas/GenericBulkAction' - + responses: 200: description: 'The Client listresponse' @@ -10338,7 +10337,7 @@ paths: description: | Handles merging 2 clients - The id parameter is the client that will be the primary client after the merge has completed. + The id parameter is the client that will be the primary client after the merge has completed. The mergeable_client_hashed_id is the client that will be merged into the primary client, this clients records will be updated and associated with the primary client. operationId: mergeClient @@ -10555,10 +10554,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Credit' @@ -10614,7 +10613,7 @@ paths: description: 'Server error' default: $ref: "#/components/responses/default" - + "/api/v1/credits/{id}": get: tags: @@ -11001,12 +11000,12 @@ paths: - name: filter in: query description: | - Searches across a range of columns including: - - amount - - date - - custom_value1 - - custom_value2 - - custom_value3 + Searches across a range of columns including: + - amount + - date + - custom_value1 + - custom_value2 + - custom_value3 - custom_value4 required: false schema: @@ -11015,7 +11014,7 @@ paths: - name: number in: query description: | - Search payments by payment number + Search payments by payment number required: false schema: type: string @@ -11026,7 +11025,7 @@ paths: required: false schema: type: string - example: id|desc number|desc balance|asc + example: id|desc number|desc balance|asc responses: 200: description: "A list of payments" @@ -11039,10 +11038,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Payment' @@ -11104,7 +11103,7 @@ paths: description: 'Server error' default: $ref: "#/components/responses/default" - + "/api/v1/payments/{id}": get: tags: @@ -11462,7 +11461,7 @@ paths: description: 'Server error' default: $ref: "#/components/responses/default" - + "/api/v1/payments/{id}/upload": post: tags: @@ -11531,8 +11530,8 @@ paths: - invoices summary: "List invoices" description: | - Lists invoices with the option to chain multiple query parameters allowing fine grained filtering of the list. - + Lists invoices with the option to chain multiple query parameters allowing fine grained filtering of the list. + operationId: getInvoices parameters: - $ref: "#/components/parameters/X-API-TOKEN" @@ -11548,11 +11547,11 @@ paths: - name: client_status in: query description: | - A comma separated list of invoice status strings. Valid options include: + A comma separated list of invoice status strings. Valid options include: - all - - paid - - unpaid - - overdue + - paid + - unpaid + - overdue required: false schema: type: string @@ -11560,7 +11559,7 @@ paths: - name: number in: query description: | - Search invoices by invoice number + Search invoices by invoice number required: false schema: type: string @@ -11568,15 +11567,15 @@ paths: - name: filter in: query description: | - Searches across a range of columns including: - - number - - po_number - - date - - amount - - balance - - custom_value1 - - custom_value2 - - custom_value3 + Searches across a range of columns including: + - number + - po_number + - date + - amount + - balance + - custom_value1 + - custom_value2 + - custom_value3 - custom_value4 required: false schema: @@ -11612,7 +11611,7 @@ paths: required: false schema: type: string - example: id|desc number|desc balance|asc + example: id|desc number|desc balance|asc - name: private_notes in: query description: | @@ -11633,10 +11632,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Invoice' @@ -11659,10 +11658,10 @@ paths: tags: - invoices summary: "Create invoice" - description: | + description: | Adds a invoice to a company - Triggered actions are available when updating or creating an invoice. + Triggered actions are available when updating or creating an invoice. These are query parameters that can be chained in order to perform additional actions on the entity, these include: ``` @@ -11766,7 +11765,7 @@ paths: description: | Handles the updating of an invoice by id. - Triggered actions are available when updating or creating an invoice. + Triggered actions are available when updating or creating an invoice. These are query parameters that can be chained in order to perform additional actions on the entity, these include: ``` @@ -11817,7 +11816,7 @@ paths: 5XX: description: 'Server error' default: - $ref: "#/components/responses/default" + $ref: "#/components/responses/default" delete: tags: - invoices @@ -11948,7 +11947,7 @@ paths: - invoices summary: "Bulk invoice actions" description: | - There are multiple actions that are available including: + There are multiple actions that are available including: operationId: bulkInvoices parameters: @@ -11966,35 +11965,35 @@ paths: action: type: string description: | - The action to be performed, options include: - - `bulk_download` - Bulk download an array of invoice PDFs (These are sent to the admin via email.) - - `download` - Download a single PDF. (Returns a single PDF object) - - `bulk_print` - Merges an array of Invoice PDFs for easy one click printing. - - `auto_bill` - Attempts to automatically bill the invoices with the payment method on file. - - `clone_to_invoice` - Returns a clone of the invoice. - - `clone_to_quote` - Returns a quote cloned using the properties of the given invoice. - - `mark_paid` - Marks an array of invoices as paid. - - `mark_sent` - Marks an array of invoices as sent. - - `restore` - Restores an array of invoices - - `delete` - Deletes an array of invoices - - `archive` - Archives an array of invoices - - `cancel` - Cancels an array of invoices - - `email` - Emails an array of invoices - - `send_email` - Emails an array of invoices. Requires additional properties to be sent. `email_type` + The action to be performed, options include: + - `bulk_download` + Bulk download an array of invoice PDFs (These are sent to the admin via email.) + - `download` + Download a single PDF. (Returns a single PDF object) + - `bulk_print` + Merges an array of Invoice PDFs for easy one click printing. + - `auto_bill` + Attempts to automatically bill the invoices with the payment method on file. + - `clone_to_invoice` + Returns a clone of the invoice. + - `clone_to_quote` + Returns a quote cloned using the properties of the given invoice. + - `mark_paid` + Marks an array of invoices as paid. + - `mark_sent` + Marks an array of invoices as sent. + - `restore` + Restores an array of invoices + - `delete` + Deletes an array of invoices + - `archive` + Archives an array of invoices + - `cancel` + Cancels an array of invoices + - `email` + Emails an array of invoices + - `send_email` + Emails an array of invoices. Requires additional properties to be sent. `email_type` ids: type: array items: @@ -12003,7 +12002,7 @@ paths: example: action: bulk_download ids: "['D2J234DFA','D2J234DFA','D2J234DFA']" - + responses: 200: description: "The Bulk Action response" @@ -12034,17 +12033,17 @@ paths: - invoices summary: "Custom invoice action" description: | - Performs a custom action on an invoice. - The current range of actions are as follows - - clone_to_invoice - - clone_to_quote - - history - - delivery_note - - mark_paid - - download - - archive - - delete - - email + Performs a custom action on an invoice. + The current range of actions are as follows + - clone_to_invoice + - clone_to_quote + - history + - delivery_note + - mark_paid + - download + - archive + - delete + - email operationId: actionInvoice parameters: - $ref: "#/components/parameters/X-API-TOKEN" @@ -12242,7 +12241,7 @@ paths: - Recurring Invoices summary: "List recurring invoices" description: | - Lists invoices with the option to chain multiple query parameters allowing fine grained filtering of the list. + Lists invoices with the option to chain multiple query parameters allowing fine grained filtering of the list. operationId: getRecurringInvoices parameters: @@ -12258,10 +12257,10 @@ paths: - name: filter in: query description: | - Searches across a range of columns including: - - custom_value1 - - custom_value2 - - custom_value3 + Searches across a range of columns including: + - custom_value1 + - custom_value2 + - custom_value3 - custom_value4 required: false schema: @@ -12270,11 +12269,11 @@ paths: - name: client_status in: query description: | - A comma separated list of invoice status strings. Valid options include: + A comma separated list of invoice status strings. Valid options include: - all - - active - - paused - - completed + - active + - paused + - completed required: false schema: type: string @@ -12285,7 +12284,7 @@ paths: required: false schema: type: string - example: id|desc number|desc balance|asc + example: id|desc number|desc balance|asc responses: 200: description: "A list of recurring_invoices" @@ -12298,10 +12297,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/RecurringInvoice' @@ -12356,7 +12355,7 @@ paths: description: 'Server error' default: $ref: "#/components/responses/default" - + "/api/v1/recurring_invoices/{id}": get: tags: @@ -12531,7 +12530,7 @@ paths: description: 'Server error' default: $ref: "#/components/responses/default" - + /api/v1/recurring_invoices/create: get: tags: @@ -12593,20 +12592,20 @@ paths: action: type: string description: | - The action to be performed, options include: - - `start` - Starts (or restarts) the recurring invoice. **note** if the recurring invoice has been stopped for a long time, it will attempt to catch back up firing a new Invoice every hour per interval that has been missed. + The action to be performed, options include: + - `start` + Starts (or restarts) the recurring invoice. **note** if the recurring invoice has been stopped for a long time, it will attempt to catch back up firing a new Invoice every hour per interval that has been missed. If you do not wish to have the recurring invoice catch up, you should set the next_send_date to the correct date you wish the recurring invoice to commence from. - - `stop` - Stops the recurring invoice. - - `send_now` - Force sends the recurring invoice - this option is only available when the recurring invoice is in a draft state. - - `restore` + - `stop` + Stops the recurring invoice. + - `send_now` + Force sends the recurring invoice - this option is only available when the recurring invoice is in a draft state. + - `restore` Restores the recurring invoice from an archived or deleted state. - - `archive` + - `archive` Archives the recurring invoice. The recurring invoice will not fire in this state. - - `delete` - Deletes a recurring invoice. + - `delete` + Deletes a recurring invoice. ids: type: array items: @@ -12819,11 +12818,11 @@ paths: - name: filter in: query description: | - Searches across a range of columns including: - - number - - custom_value1 - - custom_value2 - - custom_value3 + Searches across a range of columns including: + - number + - custom_value1 + - custom_value2 + - custom_value3 - custom_value4 required: false schema: @@ -12832,13 +12831,13 @@ paths: - name: client_status in: query description: | - A comma separated list of quote status strings. Valid options include: + A comma separated list of quote status strings. Valid options include: - all - - draft - - sent + - draft + - sent - approved - expired - - upcoming + - upcoming required: false schema: type: string @@ -12846,7 +12845,7 @@ paths: - name: number in: query description: | - Search quote by quote number + Search quote by quote number required: false schema: type: string @@ -12857,7 +12856,7 @@ paths: required: false schema: type: string - example: id|desc number|desc balance|asc + example: id|desc number|desc balance|asc responses: 200: description: "A list of quotes" @@ -12870,10 +12869,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Quote' @@ -13367,10 +13366,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/PurchaseOrder' @@ -13425,7 +13424,7 @@ paths: description: 'Server error' default: $ref: "#/components/responses/default" - + "/api/v1/purchase_orders/{id}": get: tags: @@ -13601,7 +13600,7 @@ paths: description: 'Server error' default: $ref: "#/components/responses/default" - + /api/v1/purchase_orders/create: get: tags: @@ -13864,10 +13863,10 @@ paths: $ref: "#/components/headers/X-RateLimit-Limit" content: application/json: - schema: + schema: type: object properties: - data: + data: type: array items: $ref: '#/components/schemas/Vendor' @@ -14258,10 +14257,10 @@ components: schema: type: integer securitySchemes: - ApiKeyAuth: + ApiKeyAuth: type: apiKey - in: header - name: X-API-TOKEN + in: header + name: X-API-TOKEN #examples: # Client: # $ref: '#/components/schemas/Client' @@ -14362,13 +14361,13 @@ components: application/json: schema: $ref: '#/components/schemas/Error' - 400: + 400: description: 'Invalid user input' content: application/json: schema: $ref: '#/components/schemas/InvalidInputError' - 401: + 401: description: 'Authentication error' content: application/json: @@ -14411,7 +14410,7 @@ components: bank_integration_include: name: include in: query - description: Include child relations of the BankIntegration object. Format is comma separated. + description: Include child relations of the BankIntegration object. Format is comma separated. required: false schema: type: string @@ -14476,7 +14475,7 @@ components: summary: include=payment will include the payment object in the response expense: value: expense - summary: include=expense will include the expense object in the response + summary: include=expense will include the expense object in the response vendor_contact: value: vendor_contact summary: include=vendor_contact will include the vendor_contact object in the response @@ -14488,7 +14487,7 @@ components: summary: include=purchase_order will include the purchase_order object in the response task: value: task - summary: include=task will include the task object in the response + summary: include=task will include the task object in the response login_include: name: include in: query @@ -14565,16 +14564,16 @@ components: schema: type: number example: user - + ########################### Generic filters available across all filter ################################## status: name: status in: query description: | - Filter the entity based on their status. ie active / archived / deleted. Format is a comma separated string with any of the following options: + Filter the entity based on their status. ie active / archived / deleted. Format is a comma separated string with any of the following options: - active - archived - - deleted + - deleted required: false schema: type: string @@ -14702,11 +14701,151 @@ components: type: string example: '2' type: object - - + 422: + $ref: "#/components/responses/422" + default: + $ref: "#/components/responses/default" + "/api/v1/companies/{id}/edit": + get: + tags: + - companies + summary: "Shows an company for editting" + description: "Displays an company by id" + operationId: editCompany + parameters: + - $ref: "#/components/parameters/X-API-TOKEN" + - $ref: "#/components/parameters/X-Requested-With" + - $ref: "#/components/parameters/include" + - name: id + in: path + description: "The Company Hashed ID" + required: true + schema: + type: string + format: string + example: D2J234DFA + responses: + 200: + description: "Returns the company object" + headers: + X-MINIMUM-CLIENT-VERSION: + $ref: "#/components/headers/X-MINIMUM-CLIENT-VERSION" + X-RateLimit-Remaining: + $ref: "#/components/headers/X-RateLimit-Remaining" + X-RateLimit-Limit: + $ref: "#/components/headers/X-RateLimit-Limit" + content: + application/json: + schema: + $ref: "#/components/schemas/Company" + 401: + $ref: "#/components/responses/401" + 403: + $ref: "#/components/responses/403" + + 422: + $ref: "#/components/responses/422" + default: + $ref: "#/components/responses/default" + "/api/v1/companies/{id}/upload": + post: + tags: + - companies + summary: "Uploads a document to a company" + description: "Handles the uploading of a document to a company" + operationId: uploadCompanies + parameters: + - $ref: "#/components/parameters/X-API-TOKEN" + - $ref: "#/components/parameters/X-Requested-With" + - $ref: "#/components/parameters/include" + - name: id + in: path + description: "The Company Hashed ID" + required: true + schema: + type: string + format: string + example: D2J234DFA + requestBody: + description: "File Upload Body" + required: true + content: + multipart/form-data: + schema: + type: object + properties: + _method: + type: string + example: PUT + documents: + type: array + items: + description: "Array of binary documents for upload" + type: string + format: binary + responses: + 200: + description: "Returns the client object" + headers: + X-MINIMUM-CLIENT-VERSION: + $ref: "#/components/headers/X-MINIMUM-CLIENT-VERSION" + X-RateLimit-Remaining: + $ref: "#/components/headers/X-RateLimit-Remaining" + X-RateLimit-Limit: + $ref: "#/components/headers/X-RateLimit-Limit" + content: + application/json: + schema: + $ref: "#/components/schemas/Company" + 401: + $ref: "#/components/responses/401" + 403: + $ref: "#/components/responses/403" + + 422: + $ref: "#/components/responses/422" + default: + $ref: "#/components/responses/default" + "/api/v1/companies/{company}/default": + post: + tags: + - companies + summary: "Sets the company as the default company." + description: "Sets the company as the default company." + operationId: setDefaultCompany + parameters: + - $ref: "#/components/parameters/X-API-TOKEN" + - $ref: "#/components/parameters/X-Requested-With" + - $ref: "#/components/parameters/include" + - name: company + in: path + description: "The Company Hashed ID" + required: true + schema: + type: string + format: string + example: D2J234DFA + responses: + 200: + description: "Returns the company object" + headers: + X-MINIMUM-CLIENT-VERSION: + $ref: "#/components/headers/X-MINIMUM-CLIENT-VERSION" + X-RateLimit-Remaining: + $ref: "#/components/headers/X-RateLimit-Remaining" + X-RateLimit-Limit: + $ref: "#/components/headers/X-RateLimit-Limit" + content: + application/json: + schema: + $ref: "#/components/schemas/Company" + 401: + $ref: "#/components/responses/401" + 403: + $ref: "#/components/responses/403" TaskSchedulerSchema: properties: @@ -14791,7 +14930,7 @@ components: type: string example: create_client_report type: object - + TaskStatus: properties: id: @@ -14862,9 +15001,9 @@ components: type: string example: '' type: object - + AuthenticationError: - type: object + type: object properties: message: description: 'These credentials do not match our records / Invalid Token' @@ -15088,7 +15227,7 @@ components: The tax category id for this product.' The following constants are available (default = '1') - + ``` PRODUCT_TYPE_PHYSICAL = '1' PRODUCT_TYPE_SERVICE = '2' @@ -15238,8 +15377,8 @@ components: type: integer format: int32 description: | - The quantity of the product that is currently in stock. - + The quantity of the product that is currently in stock. + **note** this field is not mutable without passing an extra query parameter which will allow modification of this value. The query parameter ?update_in_stock_quantity=true **MUST** be passed if you wish to update this value manually. @@ -15275,7 +15414,7 @@ components: The tax category id for this product.' The following constants are available (default = '1') - + ``` PRODUCT_TYPE_PHYSICAL = '1' PRODUCT_TYPE_SERVICE = '2' @@ -15463,7 +15602,7 @@ components: type: string example: PAY_101 type: object - + BankTransactionRule: properties: id: @@ -15766,8 +15905,8 @@ components: items: $ref: '#/components/schemas/FeesAndLimits' type: object - - + + CompanySettings: required: - currency_id @@ -16700,7 +16839,7 @@ components: type: string example: Opnel5aKBz readOnly: true - client_contact_id: + client_contact_id: description: 'The client contact hashed id' type: string example: Opnel5aKBz @@ -17033,7 +17172,7 @@ components: type: object InvoiceRequest: required: - - client_id + - client_id properties: id: description: 'The invoice hashed id' @@ -18710,7 +18849,7 @@ components: self::ENTITY_RECURRING_TASK => 1024, self::ENTITY_RECURRING_QUOTE => 2048, ``` - + The default per_page value is 20. example: 2048 @@ -18725,8 +18864,8 @@ components: first_month_of_year: description: "The first month for the company financial year" type: string - example: '1' - enabled_item_tax_rates: + example: '1' + enabled_item_tax_rates: description: "The number of tax rates used per item" type: integer example: 2 @@ -18741,7 +18880,7 @@ components: A flag determining whether to auto-bill clients by default values: - + - always - Always auto bill - disabled - Never auto bill - optin - Allow the client to select their auto bill status with the default being disabled @@ -18910,12 +19049,12 @@ components: invoice_task_project_header: description: "A flag determining whether to include the project header on invoices by default" type: boolean - example: true + example: true invoice_task_item_description: description: "A flag determining whether to include the item description on invoices by default" type: boolean example: true - + settings: $ref: '#/components/schemas/CompanySettings' type: object @@ -19141,7 +19280,7 @@ components: type: boolean example: true type: object - + Quote: properties: id: @@ -19593,7 +19732,7 @@ components: description: 'The subscription associated with this invoice' type: string example: Opnel5aKBz - + type: object ClientRequest: required: @@ -20996,7 +21135,7 @@ components: project_id: description: 'The associated project_id' type: string - example: 'Opnel5aKBz' + example: 'Opnel5aKBz' client_id: description: 'The client hashed id' type: string @@ -21156,7 +21295,7 @@ components: type: string example: Opnel5aKBz readOnly: true - client_contact_id: + client_contact_id: description: 'The client contact hashed id' type: string example: Opnel5aKBz @@ -21231,7 +21370,7 @@ components: The tax rate id to set on the list of products The following constants are available (default = '1') - + ``` PRODUCT_TYPE_PHYSICAL = '1' PRODUCT_TYPE_SERVICE = '2' @@ -21293,7 +21432,7 @@ components: language_id: description: 'The language id of the user' type: string - example: 1 + example: 1 verified_phone_number: description: 'Boolean flag if the user has their phone verified. Required to settings up 2FA' type: boolean @@ -21631,7 +21770,7 @@ tags: description: | Endpoint definitions for interacting with reports. externalDocs: - description: "https://invoiceninja.github.io" - url: "https://invoiceninja.github.io" + description: "https://invoiceninja.github.io" + url: "https://invoiceninja.github.io" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] diff --git a/openapi/components/schemas/company_settings.yaml b/openapi/components/schemas/company_settings.yaml index 15dc5bf8cd38..2ee34f675f99 100644 --- a/openapi/components/schemas/company_settings.yaml +++ b/openapi/components/schemas/company_settings.yaml @@ -1,842 +1,842 @@ - CompanySettings: - required: +CompanySettings: + required: - currency_id - properties: + properties: currency_id: - description: 'The default currency id' - type: string - example: true + description: "The default currency id" + type: string + example: true timezone_id: - description: 'The timezone id' - type: string - example: '15' + description: "The timezone id" + type: string + example: "15" date_format_id: - description: 'The date format id' - type: string - example: '15' + description: "The date format id" + type: string + example: "15" military_time: - description: 'Toggles 12/24 hour time' - type: boolean - example: true + description: "Toggles 12/24 hour time" + type: boolean + example: true language_id: - description: 'The language id' - type: string - example: '1' + description: "The language id" + type: string + example: "1" show_currency_code: - description: 'Toggles whether the currency symbol or code is shown' - type: boolean - example: true + description: "Toggles whether the currency symbol or code is shown" + type: boolean + example: true payment_terms: - description: '-1 sets no payment term, 0 sets payment due immediately, positive integers indicates payment terms in days' - type: integer - example: '1' + description: "-1 sets no payment term, 0 sets payment due immediately, positive integers indicates payment terms in days" + type: integer + example: "1" company_gateway_ids: - description: 'A commad separate list of available gateways' - type: string - example: '1,2,3,4' + description: "A commad separate list of available gateways" + type: string + example: "1,2,3,4" custom_value1: - description: 'A Custom Label' - type: string - example: 'Custom Label' + description: "A Custom Label" + type: string + example: "Custom Label" custom_value2: - description: 'A Custom Label' - type: string - example: 'Custom Label' + description: "A Custom Label" + type: string + example: "Custom Label" custom_value3: - description: 'A Custom Label' - type: string - example: 'Custom Label' + description: "A Custom Label" + type: string + example: "Custom Label" custom_value4: - description: 'A Custom Label' - type: string - example: 'Custom Label' + description: "A Custom Label" + type: string + example: "Custom Label" default_task_rate: - description: 'The default task rate' - type: number - format: float - example: '10.00' + description: "The default task rate" + type: number + format: float + example: "10.00" send_reminders: - description: 'Toggles whether reminders are sent' - type: boolean - example: true + description: "Toggles whether reminders are sent" + type: boolean + example: true enable_client_portal_tasks: - description: 'Show/hide the tasks panel in the client portal' - type: boolean - example: true + description: "Show/hide the tasks panel in the client portal" + type: boolean + example: true email_style: - description: 'options include plain,light,dark,custom' - type: string - example: light + description: "options include plain,light,dark,custom" + type: string + example: light reply_to_email: - description: 'The reply to email address' - type: string - example: email@gmail.com + description: "The reply to email address" + type: string + example: email@gmail.com bcc_email: - description: 'A comma separate list of BCC emails' - type: string - example: 'email@gmail.com, contact@gmail.com' + description: "A comma separate list of BCC emails" + type: string + example: "email@gmail.com, contact@gmail.com" pdf_email_attachment: - description: 'Toggles whether to attach PDF as attachment' - type: boolean - example: true + description: "Toggles whether to attach PDF as attachment" + type: boolean + example: true ubl_email_attachment: - description: 'Toggles whether to attach UBL as attachment' - type: boolean - example: true + description: "Toggles whether to attach UBL as attachment" + type: boolean + example: true email_style_custom: - description: 'The custom template' - type: string - example: '' + description: "The custom template" + type: string + example: "" counter_number_applied: - description: 'enum when the invoice number counter is set, ie when_saved, when_sent, when_paid' - type: string - example: when_sent + description: "enum when the invoice number counter is set, ie when_saved, when_sent, when_paid" + type: string + example: when_sent quote_number_applied: - description: 'enum when the quote number counter is set, ie when_saved, when_sent' - type: string - example: when_sent + description: "enum when the quote number counter is set, ie when_saved, when_sent" + type: string + example: when_sent custom_message_dashboard: - description: 'A custom message which is displayed on the dashboard' - type: string - example: 'Please pay invoices immediately' + description: "A custom message which is displayed on the dashboard" + type: string + example: "Please pay invoices immediately" custom_message_unpaid_invoice: - description: 'A custom message which is displayed in the client portal when a client is viewing a unpaid invoice.' - type: string - example: 'Please pay invoices immediately' + description: "A custom message which is displayed in the client portal when a client is viewing a unpaid invoice." + type: string + example: "Please pay invoices immediately" custom_message_paid_invoice: - description: 'A custom message which is displayed in the client portal when a client is viewing a paid invoice.' - type: string - example: 'Thanks for paying this invoice!' + description: "A custom message which is displayed in the client portal when a client is viewing a paid invoice." + type: string + example: "Thanks for paying this invoice!" custom_message_unapproved_quote: - description: 'A custom message which is displayed in the client portal when a client is viewing a unapproved quote.' - type: string - example: 'Please approve quote' + description: "A custom message which is displayed in the client portal when a client is viewing a unapproved quote." + type: string + example: "Please approve quote" lock_invoices: - description: 'Toggles whether invoices are locked once sent and cannot be modified further' - type: boolean - example: true + description: "Toggles whether invoices are locked once sent and cannot be modified further" + type: boolean + example: true auto_archive_invoice: - description: 'Toggles whether a invoice is archived immediately following payment' - type: boolean - example: true + description: "Toggles whether a invoice is archived immediately following payment" + type: boolean + example: true auto_archive_quote: - description: 'Toggles whether a quote is archived after being converted to a invoice' - type: boolean - example: true + description: "Toggles whether a quote is archived after being converted to a invoice" + type: boolean + example: true auto_convert_quote: - description: 'Toggles whether a quote is converted to a invoice when approved' - type: boolean - example: true + description: "Toggles whether a quote is converted to a invoice when approved" + type: boolean + example: true inclusive_taxes: - description: 'Boolean flag determining whether inclusive or exclusive taxes are used' - type: boolean - example: true + description: "Boolean flag determining whether inclusive or exclusive taxes are used" + type: boolean + example: true translations: - description: 'JSON payload of customized translations' - type: object - example: '' + description: "JSON payload of customized translations" + type: object + example: "" task_number_pattern: - description: 'Allows customisation of the task number pattern' - type: string - example: '{$year}-{$counter}' + description: "Allows customisation of the task number pattern" + type: string + example: "{$year}-{$counter}" task_number_counter: - description: 'The incrementing counter for tasks' - type: integer - example: '1' + description: "The incrementing counter for tasks" + type: integer + example: "1" reminder_send_time: - description: 'Time from UTC +0 when the email will be sent to the client' - type: integer - example: '32400' + description: "Time from UTC +0 when the email will be sent to the client" + type: integer + example: "32400" expense_number_pattern: - description: 'Allows customisation of the expense number pattern' - type: string - example: '{$year}-{$counter}' + description: "Allows customisation of the expense number pattern" + type: string + example: "{$year}-{$counter}" expense_number_counter: - description: 'The incrementing counter for expenses' - type: integer - example: '1' + description: "The incrementing counter for expenses" + type: integer + example: "1" vendor_number_pattern: - description: 'Allows customisation of the vendor number pattern' - type: string - example: '{$year}-{$counter}' + description: "Allows customisation of the vendor number pattern" + type: string + example: "{$year}-{$counter}" vendor_number_counter: - description: 'The incrementing counter for vendors' - type: integer - example: '1' + description: "The incrementing counter for vendors" + type: integer + example: "1" ticket_number_pattern: - description: 'Allows customisation of the ticket number pattern' - type: string - example: '{$year}-{$counter}' + description: "Allows customisation of the ticket number pattern" + type: string + example: "{$year}-{$counter}" ticket_number_counter: - description: 'The incrementing counter for tickets' - type: integer - example: '1' + description: "The incrementing counter for tickets" + type: integer + example: "1" payment_number_pattern: - description: 'Allows customisation of the payment number pattern' - type: string - example: '{$year}-{$counter}' + description: "Allows customisation of the payment number pattern" + type: string + example: "{$year}-{$counter}" payment_number_counter: - description: 'The incrementing counter for payments' - type: integer - example: '1' + description: "The incrementing counter for payments" + type: integer + example: "1" invoice_number_pattern: - description: 'Allows customisation of the invoice number pattern' - type: string - example: '{$year}-{$counter}' + description: "Allows customisation of the invoice number pattern" + type: string + example: "{$year}-{$counter}" invoice_number_counter: - description: 'The incrementing counter for invoices' - type: integer - example: '1' + description: "The incrementing counter for invoices" + type: integer + example: "1" quote_number_pattern: - description: 'Allows customisation of the quote number pattern' - type: string - example: '{$year}-{$counter}' + description: "Allows customisation of the quote number pattern" + type: string + example: "{$year}-{$counter}" quote_number_counter: - description: 'The incrementing counter for quotes' - type: integer - example: '1' + description: "The incrementing counter for quotes" + type: integer + example: "1" client_number_pattern: - description: 'Allows customisation of the client number pattern' - type: string - example: '{$year}-{$counter}' + description: "Allows customisation of the client number pattern" + type: string + example: "{$year}-{$counter}" client_number_counter: - description: 'The incrementing counter for clients' - type: integer - example: '1' + description: "The incrementing counter for clients" + type: integer + example: "1" credit_number_pattern: - description: 'Allows customisation of the credit number pattern' - type: string - example: '{$year}-{$counter}' + description: "Allows customisation of the credit number pattern" + type: string + example: "{$year}-{$counter}" credit_number_counter: - description: 'The incrementing counter for credits' - type: integer - example: '1' + description: "The incrementing counter for credits" + type: integer + example: "1" recurring_invoice_number_prefix: - description: 'This string is prepended to the recurring invoice number' - type: string - example: R + description: "This string is prepended to the recurring invoice number" + type: string + example: R reset_counter_frequency_id: - description: 'CONSTANT which is used to apply the frequency which the counters are reset' - type: integer - example: '1' + description: "CONSTANT which is used to apply the frequency which the counters are reset" + type: integer + example: "1" reset_counter_date: - description: 'The explicit date which is used to reset counters' - type: string - example: '2019-01-01' + description: "The explicit date which is used to reset counters" + type: string + example: "2019-01-01" counter_padding: - description: 'Pads the counter with leading zeros' - type: integer - example: '1' + description: "Pads the counter with leading zeros" + type: integer + example: "1" shared_invoice_quote_counter: - description: 'Flags whether to share the counter for invoices and quotes' - type: boolean - example: true + description: "Flags whether to share the counter for invoices and quotes" + type: boolean + example: true update_products: - description: 'Determines if client fields are updated from third party APIs' - type: boolean - example: true + description: "Determines if client fields are updated from third party APIs" + type: boolean + example: true convert_products: - description: '' - type: boolean - example: true + description: "" + type: boolean + example: true fill_products: - description: 'Automatically fill products based on product_key' - type: boolean - example: true + description: "Automatically fill products based on product_key" + type: boolean + example: true invoice_terms: - description: 'The default invoice terms' - type: string - example: 'Invoice Terms are...' + description: "The default invoice terms" + type: string + example: "Invoice Terms are..." quote_terms: - description: 'The default quote terms' - type: string - example: 'Quote Terms are...' + description: "The default quote terms" + type: string + example: "Quote Terms are..." invoice_taxes: - description: 'Taxes can be applied to the invoice' - type: number - example: '1' + description: "Taxes can be applied to the invoice" + type: number + example: "1" invoice_design_id: - description: 'The default design id (invoice, quote etc)' - type: string - example: '1' + description: "The default design id (invoice, quote etc)" + type: string + example: "1" quote_design_id: - description: 'The default design id (invoice, quote etc)' - type: string - example: '1' + description: "The default design id (invoice, quote etc)" + type: string + example: "1" invoice_footer: - description: 'The default invoice footer' - type: string - example: '1' + description: "The default invoice footer" + type: string + example: "1" invoice_labels: - description: 'JSON string of invoice labels' - type: string - example: '1' + description: "JSON string of invoice labels" + type: string + example: "1" tax_rate1: - description: 'The tax rate (float)' - type: number - example: '10' + description: "The tax rate (float)" + type: number + example: "10" tax_name1: - description: 'The tax name' - type: string - example: GST + description: "The tax name" + type: string + example: GST tax_rate2: - description: 'The tax rate (float)' - type: number - example: '10' + description: "The tax rate (float)" + type: number + example: "10" tax_name2: - description: 'The tax name' - type: string - example: GST + description: "The tax name" + type: string + example: GST tax_rate3: - description: 'The tax rate (float)' - type: number - example: '10' + description: "The tax rate (float)" + type: number + example: "10" tax_name3: - description: 'The tax name' - type: string - example: GST + description: "The tax name" + type: string + example: GST payment_type_id: - description: 'The default payment type id' - type: string - example: '1' + description: "The default payment type id" + type: string + example: "1" custom_fields: - description: 'JSON string of custom fields' - type: string - example: '{}' + description: "JSON string of custom fields" + type: string + example: "{}" email_footer: - description: 'The default email footer' - type: string - example: 'A default email footer' + description: "The default email footer" + type: string + example: "A default email footer" email_sending_method: - description: 'The email driver to use to send email, options include default, gmail, client_postmark, client_mailgun, office365' - type: string - example: default + description: "The email driver to use to send email, options include default, gmail, client_postmark, client_mailgun, client_brevo, office365" + type: string + example: default gmail_sending_user_id: - description: 'The hashed_id of the user account to send email from' - type: string - example: F76sd34D + description: "The hashed_id of the user account to send email from" + type: string + example: F76sd34D email_subject_invoice: - description: '' - type: string - example: 'Your Invoice Subject' + description: "" + type: string + example: "Your Invoice Subject" email_subject_quote: - description: '' - type: string - example: 'Your Quote Subject' + description: "" + type: string + example: "Your Quote Subject" email_subject_payment: - description: '' - type: string - example: 'Your Payment Subject' + description: "" + type: string + example: "Your Payment Subject" email_template_invoice: - description: 'The full template for invoice emails' - type: string - example: '' + description: "The full template for invoice emails" + type: string + example: "" email_template_quote: - description: 'The full template for quote emails' - type: string - example: '' + description: "The full template for quote emails" + type: string + example: "" email_template_payment: - description: 'The full template for payment emails' - type: string - example: '' + description: "The full template for payment emails" + type: string + example: "" email_subject_reminder1: - description: 'Email subject for Reminder' - type: string - example: '' + description: "Email subject for Reminder" + type: string + example: "" email_subject_reminder2: - description: 'Email subject for Reminder' - type: string - example: '' + description: "Email subject for Reminder" + type: string + example: "" email_subject_reminder3: - description: 'Email subject for Reminder' - type: string - example: '' + description: "Email subject for Reminder" + type: string + example: "" email_subject_reminder_endless: - description: 'Email subject for endless reminders' - type: string - example: '' + description: "Email subject for endless reminders" + type: string + example: "" email_template_reminder1: - description: 'The full template for Reminder 1' - type: string - example: '' + description: "The full template for Reminder 1" + type: string + example: "" email_template_reminder2: - description: 'The full template for Reminder 2' - type: string - example: '' + description: "The full template for Reminder 2" + type: string + example: "" email_template_reminder3: - description: 'The full template for Reminder 3' - type: string - example: '' + description: "The full template for Reminder 3" + type: string + example: "" email_template_reminder_endless: - description: 'The full template for enless reminders' - type: string - example: '' + description: "The full template for enless reminders" + type: string + example: "" enable_portal_password: - description: 'Toggles whether a password is required to log into the client portal' - type: boolean - example: true + description: "Toggles whether a password is required to log into the client portal" + type: boolean + example: true show_accept_invoice_terms: - description: 'Toggles whether the terms dialogue is shown to the client' - type: boolean - example: true + description: "Toggles whether the terms dialogue is shown to the client" + type: boolean + example: true show_accept_quote_terms: - description: 'Toggles whether the terms dialogue is shown to the client' - type: boolean - example: true + description: "Toggles whether the terms dialogue is shown to the client" + type: boolean + example: true require_invoice_signature: - description: 'Toggles whether a invoice signature is required' - type: boolean - example: true + description: "Toggles whether a invoice signature is required" + type: boolean + example: true require_quote_signature: - description: 'Toggles whether a quote signature is required' - type: boolean - example: true + description: "Toggles whether a quote signature is required" + type: boolean + example: true name: - description: 'The company name' - type: string - example: 'Acme Co' + description: "The company name" + type: string + example: "Acme Co" company_logo: - description: 'The company logo file' - type: object - example: logo.png + description: "The company logo file" + type: object + example: logo.png website: - description: 'The company website URL' - type: string - example: www.acme.com + description: "The company website URL" + type: string + example: www.acme.com address1: - description: 'The company address line 1' - type: string - example: 'Suite 888' + description: "The company address line 1" + type: string + example: "Suite 888" address2: - description: 'The company address line 2' - type: string - example: '5 Jimbo Way' + description: "The company address line 2" + type: string + example: "5 Jimbo Way" city: - description: 'The company city' - type: string - example: Sydney + description: "The company city" + type: string + example: Sydney state: - description: 'The company state' - type: string - example: Florisa + description: "The company state" + type: string + example: Florisa postal_code: - description: 'The company zip/postal code' - type: string - example: '90210' + description: "The company zip/postal code" + type: string + example: "90210" phone: - description: 'The company phone' - type: string - example: 555-213-3948 + description: "The company phone" + type: string + example: 555-213-3948 email: - description: 'The company email' - type: string - example: joe@acme.co + description: "The company email" + type: string + example: joe@acme.co country_id: - description: 'The country ID' - type: string - example: '1' + description: "The country ID" + type: string + example: "1" vat_number: - description: 'The company VAT/TAX ID number' - type: string - example: '32 120 377 720' + description: "The company VAT/TAX ID number" + type: string + example: "32 120 377 720" page_size: - description: 'The default page size' - type: string - example: A4 + description: "The default page size" + type: string + example: A4 font_size: - description: 'The font size' - type: number - example: '9' + description: "The font size" + type: number + example: "9" primary_font: - description: 'The primary font' - type: string - example: roboto + description: "The primary font" + type: string + example: roboto secondary_font: - description: 'The secondary font' - type: string - example: roboto + description: "The secondary font" + type: string + example: roboto hide_paid_to_date: - description: 'Flags whether to hide the paid to date field' - type: boolean - example: false + description: "Flags whether to hide the paid to date field" + type: boolean + example: false embed_documents: - description: 'Toggled whether to embed documents in the PDF' - type: boolean - example: false + description: "Toggled whether to embed documents in the PDF" + type: boolean + example: false all_pages_header: - description: 'The header for the PDF' - type: boolean - example: false + description: "The header for the PDF" + type: boolean + example: false all_pages_footer: - description: 'The footer for the PDF' - type: boolean - example: false + description: "The footer for the PDF" + type: boolean + example: false document_email_attachment: - description: 'Toggles whether to attach documents in the email' - type: boolean - example: false + description: "Toggles whether to attach documents in the email" + type: boolean + example: false enable_client_portal_password: - description: 'Toggles password protection of the client portal' - type: boolean - example: false + description: "Toggles password protection of the client portal" + type: boolean + example: false enable_email_markup: - description: 'Toggles the use of markdown in emails' - type: boolean - example: false + description: "Toggles the use of markdown in emails" + type: boolean + example: false enable_client_portal_dashboard: - description: 'Toggles whether the client dashboard is shown in the client portal' - type: boolean - example: false + description: "Toggles whether the client dashboard is shown in the client portal" + type: boolean + example: false enable_client_portal: - description: 'Toggles whether the entire client portal is displayed to the client, or only the context' - type: boolean - example: false + description: "Toggles whether the entire client portal is displayed to the client, or only the context" + type: boolean + example: false email_template_statement: - description: 'The body of the email for statements' - type: string - example: 'template matter' + description: "The body of the email for statements" + type: string + example: "template matter" email_subject_statement: - description: 'The subject of the email for statements' - type: string - example: 'subject matter' + description: "The subject of the email for statements" + type: string + example: "subject matter" signature_on_pdf: - description: 'Toggles whether the signature (if available) is displayed on the PDF' - type: boolean - example: false + description: "Toggles whether the signature (if available) is displayed on the PDF" + type: boolean + example: false quote_footer: - description: 'The default quote footer' - type: string - example: 'the quote footer' + description: "The default quote footer" + type: string + example: "the quote footer" email_subject_custom1: - description: 'Custom reminder template subject' - type: string - example: 'Custom Subject 1' + description: "Custom reminder template subject" + type: string + example: "Custom Subject 1" email_subject_custom2: - description: 'Custom reminder template subject' - type: string - example: 'Custom Subject 2' + description: "Custom reminder template subject" + type: string + example: "Custom Subject 2" email_subject_custom3: - description: 'Custom reminder template subject' - type: string - example: 'Custom Subject 3' + description: "Custom reminder template subject" + type: string + example: "Custom Subject 3" email_template_custom1: - description: 'Custom reminder template body' - type: string - example: '' + description: "Custom reminder template body" + type: string + example: "" email_template_custom2: - description: 'Custom reminder template body' - type: string - example: '' + description: "Custom reminder template body" + type: string + example: "" email_template_custom3: - description: 'Custom reminder template body' - type: string - example: '' + description: "Custom reminder template body" + type: string + example: "" enable_reminder1: - description: 'Toggles whether this reminder is enabled' - type: boolean - example: false + description: "Toggles whether this reminder is enabled" + type: boolean + example: false enable_reminder2: - description: 'Toggles whether this reminder is enabled' - type: boolean - example: false + description: "Toggles whether this reminder is enabled" + type: boolean + example: false enable_reminder3: - description: 'Toggles whether this reminder is enabled' - type: boolean - example: false + description: "Toggles whether this reminder is enabled" + type: boolean + example: false num_days_reminder1: - description: 'The Reminder interval' - type: number - example: '9' + description: "The Reminder interval" + type: number + example: "9" num_days_reminder2: - description: 'The Reminder interval' - type: number - example: '9' + description: "The Reminder interval" + type: number + example: "9" num_days_reminder3: - description: 'The Reminder interval' - type: number - example: '9' + description: "The Reminder interval" + type: number + example: "9" schedule_reminder1: - description: '(enum: after_invoice_date, before_due_date, after_due_date)' - type: string - example: after_invoice_date + description: "(enum: after_invoice_date, before_due_date, after_due_date)" + type: string + example: after_invoice_date schedule_reminder2: - description: '(enum: after_invoice_date, before_due_date, after_due_date)' - type: string - example: after_invoice_date + description: "(enum: after_invoice_date, before_due_date, after_due_date)" + type: string + example: after_invoice_date schedule_reminder3: - description: '(enum: after_invoice_date, before_due_date, after_due_date)' - type: string - example: after_invoice_date + description: "(enum: after_invoice_date, before_due_date, after_due_date)" + type: string + example: after_invoice_date late_fee_amount1: - description: 'The late fee amount for reminder 1' - type: number - example: 10 + description: "The late fee amount for reminder 1" + type: number + example: 10 late_fee_amount2: - description: 'The late fee amount for reminder 2' - type: number - example: 20 + description: "The late fee amount for reminder 2" + type: number + example: 20 late_fee_amount3: - description: 'The late fee amount for reminder 2' - type: number - example: 100 + description: "The late fee amount for reminder 2" + type: number + example: 100 endless_reminder_frequency_id: - description: 'The frequency id of the endless reminder' - type: string - example: '1' + description: "The frequency id of the endless reminder" + type: string + example: "1" client_online_payment_notification: - description: 'Determines if a client should receive the notification for a online payment' - type: boolean - example: false + description: "Determines if a client should receive the notification for a online payment" + type: boolean + example: false client_manual_payment_notification: - description: 'Determines if a client should receive the notification for a manually entered payment' - type: boolean - example: false + description: "Determines if a client should receive the notification for a manually entered payment" + type: boolean + example: false enable_e_invoice: - description: 'Determines if e-invoicing is enabled' - type: boolean - example: false + description: "Determines if e-invoicing is enabled" + type: boolean + example: false default_expense_payment_type_id: - description: 'The default payment type for expenses' - type: string - example: '0' + description: "The default payment type for expenses" + type: string + example: "0" e_invoice_type: - description: 'The e-invoice type' - type: string - example: 'EN16931' + description: "The e-invoice type" + type: string + example: "EN16931" mailgun_endpoint: - description: 'The mailgun endpoint - used to determine whether US or EU endpoints are used' - type: string - example: 'api.mailgun.net or api.eu.mailgun.net' + description: "The mailgun endpoint - used to determine whether US or EU endpoints are used" + type: string + example: "api.mailgun.net or api.eu.mailgun.net" client_initiated_payments: - description: 'Determines if clients can initiate payments directly from the client portal' - type: boolean - example: false + description: "Determines if clients can initiate payments directly from the client portal" + type: boolean + example: false client_initiated_payments_minimum: - description: 'The minimum amount a client can pay' - type: number - example: 10 + description: "The minimum amount a client can pay" + type: number + example: 10 sync_invoice_quote_columns: - description: 'Determines if invoice and quote columns are synced for the PDF rendering, or if they use their own columns' - type: boolean - example: false + description: "Determines if invoice and quote columns are synced for the PDF rendering, or if they use their own columns" + type: boolean + example: false show_task_item_description: - description: 'Determines if the task item description is shown on the invoice' - type: boolean - example: false + description: "Determines if the task item description is shown on the invoice" + type: boolean + example: false allow_billable_task_items: - description: 'Determines if task items can be marked as billable' - type: boolean - example: false + description: "Determines if task items can be marked as billable" + type: boolean + example: false accept_client_input_quote_approval: - description: 'Determines if clients can approve quotes and also pass through a PO Number reference' - type: boolean - example: false + description: "Determines if clients can approve quotes and also pass through a PO Number reference" + type: boolean + example: false custom_sending_email: - description: 'When using Mailgun or Postmark, the FROM email address can be customized using this setting.' - type: string - example: 'bob@gmail.com' + description: "When using Mailgun or Postmark, the FROM email address can be customized using this setting." + type: string + example: "bob@gmail.com" show_paid_stamp: - description: 'Determines if the PAID stamp is shown on the invoice' - type: boolean - example: false + description: "Determines if the PAID stamp is shown on the invoice" + type: boolean + example: false show_shipping_address: - description: 'Determines if the shipping address is shown on the invoice' - type: boolean - example: false + description: "Determines if the shipping address is shown on the invoice" + type: boolean + example: false company_logo_size: - description: 'The size of the company logo on the PDF - percentage value between 0 and 100' - type: number - example: 100 + description: "The size of the company logo on the PDF - percentage value between 0 and 100" + type: number + example: 100 show_email_footer: - description: 'Determines if the email footer is shown on emails' - type: boolean - example: false + description: "Determines if the email footer is shown on emails" + type: boolean + example: false email_alignment: - description: 'The alignment of the email body text, options include left / center / right' - type: string - example: 'left' + description: "The alignment of the email body text, options include left / center / right" + type: string + example: "left" auto_bill_standard_invoices: - description: 'Determines if standard invoices are automatically billed when they are created or due' - type: boolean - example: false + description: "Determines if standard invoices are automatically billed when they are created or due" + type: boolean + example: false postmark_secret: - description: 'The Postmark secret API key' - type: string - example: '123456' + description: "The Postmark secret API key" + type: string + example: "123456" mailgun_secret: - description: 'The Mailgun secret API key' - type: string - example: '123456' + description: "The Mailgun secret API key" + type: string + example: "123456" mailgun_domain: - description: 'The Mailgun domain' - type: string - example: 'sandbox123456.mailgun.org' + description: "The Mailgun domain" + type: string + example: "sandbox123456.mailgun.org" send_email_on_mark_paid: - description: 'Determines if an email is sent when an invoice is marked as paid' - type: boolean - example: false + description: "Determines if an email is sent when an invoice is marked as paid" + type: boolean + example: false vendor_portal_enable_uploads: - description: 'Determines if vendors can upload files to the portal' - type: boolean - example: false + description: "Determines if vendors can upload files to the portal" + type: boolean + example: false besr_id: - description: 'The BESR ID' - type: string - example: '123456' + description: "The BESR ID" + type: string + example: "123456" qr_iban: - description: 'The IBAN for the QR code' - type: string - example: 'CH123456' + description: "The IBAN for the QR code" + type: string + example: "CH123456" email_subject_purchase_order: - description: 'The email subject for purchase orders' - type: string - example: 'Purchase Order' + description: "The email subject for purchase orders" + type: string + example: "Purchase Order" email_template_purchase_order: - description: 'The email template for purchase orders' - type: string - example: 'Please see attached your purchase order.' + description: "The email template for purchase orders" + type: string + example: "Please see attached your purchase order." require_purchase_order_signature: - description: 'Determines if a signature is required on purchase orders' - type: boolean - example: false + description: "Determines if a signature is required on purchase orders" + type: boolean + example: false purchase_order_public_notes: - description: 'The public notes for purchase orders' - type: string - example: 'Please see attached your purchase order.' + description: "The public notes for purchase orders" + type: string + example: "Please see attached your purchase order." purchase_order_terms: - description: 'The terms for purchase orders' - type: string - example: 'Please see attached your purchase order.' + description: "The terms for purchase orders" + type: string + example: "Please see attached your purchase order." purchase_order_footer: - description: 'The footer for purchase orders' - type: string - example: 'Please see attached your purchase order.' + description: "The footer for purchase orders" + type: string + example: "Please see attached your purchase order." purchase_order_design_id: - description: 'The design id for purchase orders' - type: string - example: 'hd677df' + description: "The design id for purchase orders" + type: string + example: "hd677df" purchase_order_number_pattern: - description: 'The pattern for purchase order numbers' - type: string - example: 'PO-000000' + description: "The pattern for purchase order numbers" + type: string + example: "PO-000000" purchase_order_number_counter: - description: 'The counter for purchase order numbers' - type: number - example: 1 + description: "The counter for purchase order numbers" + type: number + example: 1 page_numbering_alignment: - description: 'The alignment for page numbering: options include left / center / right' - type: string - example: 'left' + description: "The alignment for page numbering: options include left / center / right" + type: string + example: "left" page_numbering: - description: 'Determines if page numbering is enabled on Document PDFs' - type: boolean - example: false + description: "Determines if page numbering is enabled on Document PDFs" + type: boolean + example: false auto_archive_invoice_cancelled: - description: 'Determines if invoices are automatically archived when they are cancelled' - type: boolean - example: false + description: "Determines if invoices are automatically archived when they are cancelled" + type: boolean + example: false email_from_name: - description: 'The FROM name for emails when using Custom emailers' - type: string - example: 'Bob Smith' + description: "The FROM name for emails when using Custom emailers" + type: string + example: "Bob Smith" show_all_tasks_client_portal: - description: 'Determines if all tasks are shown on the client portal' - type: boolean - example: false + description: "Determines if all tasks are shown on the client portal" + type: boolean + example: false entity_send_time: - description: 'The time that emails are sent. The time is localized to the clients locale, integer values from 1 - 24' - type: integer - example: 9 + description: "The time that emails are sent. The time is localized to the clients locale, integer values from 1 - 24" + type: integer + example: 9 shared_invoice_credit_counter: - description: 'Determines if the invoice and credit counter are shared' - type: boolean - example: false + description: "Determines if the invoice and credit counter are shared" + type: boolean + example: false reply_to_name: - description: 'The reply to name for emails' - type: string - example: 'Bob Smith' + description: "The reply to name for emails" + type: string + example: "Bob Smith" hide_empty_columns_on_pdf: - description: 'Determines if empty columns are hidden on PDFs' - type: boolean - example: false + description: "Determines if empty columns are hidden on PDFs" + type: boolean + example: false enable_reminder_endless: - description: 'Determines if endless reminders are enabled' - type: boolean - example: false + description: "Determines if endless reminders are enabled" + type: boolean + example: false use_credits_payment: - description: 'Determines if credits can be used as a payment method' - type: boolean - example: false + description: "Determines if credits can be used as a payment method" + type: boolean + example: false recurring_invoice_number_pattern: - description: 'The pattern for recurring invoice numbers' - type: string - example: 'R-000000' + description: "The pattern for recurring invoice numbers" + type: string + example: "R-000000" recurring_invoice_number_counter: - description: 'The counter for recurring invoice numbers' - type: number - example: 1 + description: "The counter for recurring invoice numbers" + type: number + example: 1 client_portal_under_payment_minimum: - description: 'The minimum payment payment' - type: number - example: 10 + description: "The minimum payment payment" + type: number + example: 10 auto_bill_date: - description: 'Determines when the invoices are auto billed, options are on_send_date (when the invoice is sent) or on_due_date (when the invoice is due))' - type: string - example: 'on_send_date' + description: "Determines when the invoices are auto billed, options are on_send_date (when the invoice is sent) or on_due_date (when the invoice is due))" + type: string + example: "on_send_date" primary_color: - description: 'The primary color for the client portal / document highlights' - type: string - example: '#ffffff' + description: "The primary color for the client portal / document highlights" + type: string + example: "#ffffff" secondary_color: - description: 'The secondary color for the client portal / document highlights' - type: string - example: '#ffffff' + description: "The secondary color for the client portal / document highlights" + type: string + example: "#ffffff" client_portal_allow_under_payment: - description: 'Determines if clients can pay invoices under the invoice amount due' - type: boolean - example: false + description: "Determines if clients can pay invoices under the invoice amount due" + type: boolean + example: false client_portal_allow_over_payment: - description: 'Determines if clients can pay invoices over the invoice amount' - type: boolean - example: false + description: "Determines if clients can pay invoices over the invoice amount" + type: boolean + example: false auto_bill: - description: 'Determines how autobilling is applied for recurring invoices. off (no auto billed), always (always auto bill), optin (The user must opt in to auto billing), optout (The user must opt out of auto billing' - type: string - example: 'off' + description: "Determines how autobilling is applied for recurring invoices. off (no auto billed), always (always auto bill), optin (The user must opt in to auto billing), optout (The user must opt out of auto billing" + type: string + example: "off" client_portal_terms: - description: 'The terms which are displayed on the client portal' - type: string - example: 'Please see attached your invoice.' + description: "The terms which are displayed on the client portal" + type: string + example: "Please see attached your invoice." client_portal_privacy_policy: - description: 'The privacy policy which is displayed on the client portal' - type: string - example: 'These are the terms of use for using the client portal.' + description: "The privacy policy which is displayed on the client portal" + type: string + example: "These are the terms of use for using the client portal." client_can_register: - description: 'Determines if clients can register on the client portal' - type: boolean - example: false + description: "Determines if clients can register on the client portal" + type: boolean + example: false portal_design_id: - description: 'The design id for the client portal' - type: string - example: 'hd677df' + description: "The design id for the client portal" + type: string + example: "hd677df" late_fee_endless_percent: - description: 'The late fee percentage for endless late fees' - type: number - example: 10 + description: "The late fee percentage for endless late fees" + type: number + example: 10 late_fee_endless_amount: - description: 'The late fee amount for endless late fees' - type: number - example: 10 + description: "The late fee amount for endless late fees" + type: number + example: 10 auto_email_invoice: - description: 'Determines if invoices are automatically emailed when they are created' - type: boolean - example: false + description: "Determines if invoices are automatically emailed when they are created" + type: boolean + example: false email_signature: - description: 'The email signature for emails' - type: string - example: 'Bob Smith' + description: "The email signature for emails" + type: string + example: "Bob Smith" classification: - description: 'The classification for the company' - type: string - example: 'individual' - type: object \ No newline at end of file + description: "The classification for the company" + type: string + example: "individual" + type: object diff --git a/routes/api.php b/routes/api.php index 480f1ce87e6c..b404ef53b465 100644 --- a/routes/api.php +++ b/routes/api.php @@ -12,6 +12,7 @@ */ use Illuminate\Support\Facades\Route; use App\Http\Controllers\BaseController; +use App\Http\Controllers\BrevoController; use App\Http\Controllers\PingController; use App\Http\Controllers\SmtpController; use App\Http\Controllers\TaskController; @@ -194,7 +195,10 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::get('company_ledger', [CompanyLedgerController::class, 'index'])->name('company_ledger.index'); Route::resource('company_gateways', CompanyGatewayController::class); + Route::post('company_gateways/bulk', [CompanyGatewayController::class, 'bulk'])->name('company_gateways.bulk'); + Route::post('company_gateways/{company_gateway}/test', [CompanyGatewayController::class, 'test'])->name('company_gateways.test'); + Route::post('company_gateways/{company_gateway}/import_customers', [CompanyGatewayController::class, 'importCustomers'])->name('company_gateways.import_customers'); Route::put('company_users/{user}', [CompanyUserController::class, 'update']); Route::put('company_users/{user}/preferences', [CompanyUserController::class, 'updatePreferences']); @@ -426,6 +430,7 @@ Route::match(['get', 'post'], 'payment_notification_webhook/{company_key}/{compa Route::post('api/v1/postmark_webhook', [PostMarkController::class, 'webhook'])->middleware('throttle:1000,1'); Route::post('api/v1/mailgun_webhook', [MailgunController::class, 'webhook'])->middleware('throttle:1000,1'); Route::post('api/v1/mailgun_inbound_webhook', [MailgunController::class, 'inboundWebhook'])->middleware('throttle:1000,1'); +Route::post('api/v1/brevo_webhook', [BrevoController::class, 'webhook'])->middleware('throttle:1000,1'); Route::get('token_hash_router', [OneTimeTokenController::class, 'router'])->middleware('throttle:500,1'); Route::get('webcron', [WebCronController::class, 'index'])->middleware('throttle:100,1'); Route::post('api/v1/get_migration_account', [HostedMigrationController::class, 'getAccount'])->middleware('guest')->middleware('throttle:100,1'); diff --git a/tests/Unit/InvoiceTest.php b/tests/Unit/InvoiceTest.php index b87106079bb5..d9b9cab429e6 100644 --- a/tests/Unit/InvoiceTest.php +++ b/tests/Unit/InvoiceTest.php @@ -49,6 +49,88 @@ class InvoiceTest extends TestCase $this->invoice_calc = new InvoiceSum($this->invoice); } + public function testRappenRounding() + { + + $c_settings = $this->client->settings; + $c_settings->enable_rappen_rounding = true; + + $c = \App\Models\Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'settings' => $c_settings, + ]); + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 10.01; + $item->type_id = '1'; + $item->tax_id = '1'; + $line_items[] = $item; + + $i = Invoice::factory()->create([ + 'discount' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $c->id, + 'line_items' => $line_items, + 'status_id' => 1, + ]); + + $invoice_calc = new InvoiceSum($i); + $ii = $invoice_calc->build()->getInvoice(); + + $this->assertEquals(10, $ii->amount); + + } + + public function testRappenRoundingUp() + { + + $c_settings = $this->client->settings; + $c_settings->enable_rappen_rounding = true; + + $c = \App\Models\Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'settings' => $c_settings, + ]); + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 10.09; + $item->type_id = '1'; + $item->tax_id = '1'; + $line_items[] = $item; + + $i = Invoice::factory()->create([ + 'discount' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $c->id, + 'line_items' => $line_items, + 'status_id' => 1, + ]); + + $invoice_calc = new InvoiceSum($i); + $ii = $invoice_calc->build()->getInvoice(); + + $this->assertEquals(10.10, round($ii->amount,2)); + + } + public function testPartialDueDateCast() { $i = Invoice::factory() diff --git a/tests/Unit/SmsNumberTest.php b/tests/Unit/SmsNumberTest.php new file mode 100644 index 000000000000..496501e38764 --- /dev/null +++ b/tests/Unit/SmsNumberTest.php @@ -0,0 +1,36 @@ +assertTrue(SMSNumbers::hasNumber("+461614222")); + } + + public function testArrayMiss() + { + $this->assertFalse(SMSNumbers::hasNumber("+5485454")); + } + + public function testSmsArrayType() + { + $this->assertIsArray(SMSNumbers::getNumbers()); + } +} \ No newline at end of file