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/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index cbfaea8767cb..17249805bb5a 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -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 @@ -520,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', ]; /** @@ -851,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}); } } @@ -884,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/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/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 f39fe20001f0..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(); @@ -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/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/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/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 6631f0d2ae3d..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(); @@ -249,7 +253,7 @@ class Email implements ShouldQueue 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/composer.json b/composer.json index 1b9dc59adcc5..ea5fdc18ed6d 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,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", @@ -92,6 +93,7 @@ "sprain/swiss-qr-bill": "^4.3", "square/square": "30.0.0.*", "stripe/stripe-php": "^12", + "symfony/brevo-mailer": "6.4", "symfony/http-client": "^6.0", "symfony/mailgun-mailer": "^6.1", "symfony/postmark-mailer": "^6.1", diff --git a/composer.lock b/composer.lock index b7a547baa11e..94924c137001 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": "dc9142e4af116b98de0ac6310297dba6", + "content-hash": "ef338cb66991ec0e28b96643ac5a5c6f", "packages": [ { "name": "afosto/yaac", @@ -369,16 +369,16 @@ }, { "name": "amphp/parallel", - "version": "v2.2.6", + "version": "v2.2.7", "source": { "type": "git", "url": "https://github.com/amphp/parallel.git", - "reference": "5aeaad20297507cc754859236720501b54306eba" + "reference": "ffda869c33c30627b6eb5c25f096882d885681dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/5aeaad20297507cc754859236720501b54306eba", - "reference": "5aeaad20297507cc754859236720501b54306eba", + "url": "https://api.github.com/repos/amphp/parallel/zipball/ffda869c33c30627b6eb5c25f096882d885681dc", + "reference": "ffda869c33c30627b6eb5c25f096882d885681dc", "shasum": "" }, "require": { @@ -440,7 +440,7 @@ ], "support": { "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.2.6" + "source": "https://github.com/amphp/parallel/tree/v2.2.7" }, "funding": [ { @@ -448,7 +448,7 @@ "type": "github" } ], - "time": "2024-01-07T18:12:13+00:00" + "time": "2024-03-16T16:15:46+00:00" }, { "name": "amphp/parser", @@ -514,16 +514,16 @@ }, { "name": "amphp/pipeline", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/amphp/pipeline.git", - "reference": "8a0ecc281bb0932d6b4a786453aff18c55756e63" + "reference": "f1c2ce35d27ae86ead018adb803eccca7421dd9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/8a0ecc281bb0932d6b4a786453aff18c55756e63", - "reference": "8a0ecc281bb0932d6b4a786453aff18c55756e63", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/f1c2ce35d27ae86ead018adb803eccca7421dd9b", + "reference": "f1c2ce35d27ae86ead018adb803eccca7421dd9b", "shasum": "" }, "require": { @@ -569,7 +569,7 @@ ], "support": { "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.1.0" + "source": "https://github.com/amphp/pipeline/tree/v1.2.0" }, "funding": [ { @@ -577,7 +577,7 @@ "type": "github" } ], - "time": "2023-12-23T04:34:28+00:00" + "time": "2024-03-10T14:48:16+00:00" }, { "name": "amphp/process", @@ -791,16 +791,16 @@ }, { "name": "amphp/sync", - "version": "v2.1.0", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/amphp/sync.git", - "reference": "50ddc7392cc8034b3e4798cef3cc90d3f4c0441c" + "reference": "375ef5b54a0d12c38e12728dde05a55e30f2fbec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/sync/zipball/50ddc7392cc8034b3e4798cef3cc90d3f4c0441c", - "reference": "50ddc7392cc8034b3e4798cef3cc90d3f4c0441c", + "url": "https://api.github.com/repos/amphp/sync/zipball/375ef5b54a0d12c38e12728dde05a55e30f2fbec", + "reference": "375ef5b54a0d12c38e12728dde05a55e30f2fbec", "shasum": "" }, "require": { @@ -814,7 +814,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" + "psalm/phar": "5.23" }, "type": "library", "autoload": { @@ -854,7 +854,7 @@ ], "support": { "issues": "https://github.com/amphp/sync/issues", - "source": "https://github.com/amphp/sync/tree/v2.1.0" + "source": "https://github.com/amphp/sync/tree/v2.2.0" }, "funding": [ { @@ -862,7 +862,7 @@ "type": "github" } ], - "time": "2023-08-19T13:53:40+00:00" + "time": "2024-03-12T01:00:01+00:00" }, { "name": "amphp/windows-registry", @@ -1014,16 +1014,16 @@ }, { "name": "apimatic/jsonmapper", - "version": "3.1.2", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/apimatic/jsonmapper.git", - "reference": "6673a946c21f2ceeec0cb60d17541c11a22bc79d" + "reference": "5fe6ee7ed1857d6fed669dde935c6c6c70b637d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/apimatic/jsonmapper/zipball/6673a946c21f2ceeec0cb60d17541c11a22bc79d", - "reference": "6673a946c21f2ceeec0cb60d17541c11a22bc79d", + "url": "https://api.github.com/repos/apimatic/jsonmapper/zipball/5fe6ee7ed1857d6fed669dde935c6c6c70b637d2", + "reference": "5fe6ee7ed1857d6fed669dde935c6c6c70b637d2", "shasum": "" }, "require": { @@ -1062,9 +1062,9 @@ "support": { "email": "mehdi.jaffery@apimatic.io", "issues": "https://github.com/apimatic/jsonmapper/issues", - "source": "https://github.com/apimatic/jsonmapper/tree/3.1.2" + "source": "https://github.com/apimatic/jsonmapper/tree/3.1.3" }, - "time": "2023-06-08T04:27:10+00:00" + "time": "2024-03-15T06:02:44+00:00" }, { "name": "apimatic/unirest-php", @@ -1343,16 +1343,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.300.13", + "version": "3.301.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "b1eb7307d30ebcfa4e156971f658c2d177434db3" + "reference": "0a910d2b35e7087337cdf3569dc9b6ce232aafba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b1eb7307d30ebcfa4e156971f658c2d177434db3", - "reference": "b1eb7307d30ebcfa4e156971f658c2d177434db3", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0a910d2b35e7087337cdf3569dc9b6ce232aafba", + "reference": "0a910d2b35e7087337cdf3569dc9b6ce232aafba", "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.300.13" + "source": "https://github.com/aws/aws-sdk-php/tree/3.301.1" }, - "time": "2024-03-07T19:14:04+00:00" + "time": "2024-03-15T18:14:42+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1851,28 +1851,28 @@ }, { "name": "composer/ca-bundle", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd" + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", - "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", + "phpstan/phpstan": "^1.10", "psr/log": "^1.0", "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { @@ -1907,7 +1907,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.4.1" + "source": "https://github.com/composer/ca-bundle/tree/1.5.0" }, "funding": [ { @@ -1923,7 +1923,7 @@ "type": "tidelift" } ], - "time": "2024-02-23T10:16:52+00:00" + "time": "2024-03-15T14:00:32+00:00" }, { "name": "dasprid/enum", @@ -2806,16 +2806,16 @@ }, { "name": "endroid/qr-code", - "version": "5.0.6", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/endroid/qr-code.git", - "reference": "3a9cc61d2d34df93f6edc2140e7880966ee7860f" + "reference": "0cc00f0626b73bc71a1ea17af01387d0ac75e046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/endroid/qr-code/zipball/3a9cc61d2d34df93f6edc2140e7880966ee7860f", - "reference": "3a9cc61d2d34df93f6edc2140e7880966ee7860f", + "url": "https://api.github.com/repos/endroid/qr-code/zipball/0cc00f0626b73bc71a1ea17af01387d0ac75e046", + "reference": "0cc00f0626b73bc71a1ea17af01387d0ac75e046", "shasum": "" }, "require": { @@ -2869,7 +2869,7 @@ ], "support": { "issues": "https://github.com/endroid/qr-code/issues", - "source": "https://github.com/endroid/qr-code/tree/5.0.6" + "source": "https://github.com/endroid/qr-code/tree/5.0.7" }, "funding": [ { @@ -2877,7 +2877,7 @@ "type": "github" } ], - "time": "2024-03-06T22:34:02+00:00" + "time": "2024-03-08T11:24:40+00:00" }, { "name": "eway/eway-rapid-php", @@ -3193,6 +3193,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", @@ -3379,16 +3442,16 @@ }, { "name": "google/apiclient-services", - "version": "v0.338.0", + "version": "v0.339.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-api-php-client-services.git", - "reference": "52aeb042c8d30ac0f98f4051dd4bc523708b1306" + "reference": "5662d2ab3da41ac0e0e99db221a8c22c511c8f9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/52aeb042c8d30ac0f98f4051dd4bc523708b1306", - "reference": "52aeb042c8d30ac0f98f4051dd4bc523708b1306", + "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/5662d2ab3da41ac0e0e99db221a8c22c511c8f9c", + "reference": "5662d2ab3da41ac0e0e99db221a8c22c511c8f9c", "shasum": "" }, "require": { @@ -3417,9 +3480,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.338.0" + "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.339.0" }, - "time": "2024-03-03T00:56:15+00:00" + "time": "2024-03-10T01:06:17+00:00" }, { "name": "google/auth", @@ -4295,16 +4358,16 @@ }, { "name": "horstoeko/zugferd", - "version": "v1.0.34", + "version": "v1.0.36", "source": { "type": "git", "url": "https://github.com/horstoeko/zugferd.git", - "reference": "963b8ab88374e36c056f8ebceb834075be75f0a2" + "reference": "0d15c305328c137365648fe1c34a584d877127fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/horstoeko/zugferd/zipball/963b8ab88374e36c056f8ebceb834075be75f0a2", - "reference": "963b8ab88374e36c056f8ebceb834075be75f0a2", + "url": "https://api.github.com/repos/horstoeko/zugferd/zipball/0d15c305328c137365648fe1c34a584d877127fa", + "reference": "0d15c305328c137365648fe1c34a584d877127fa", "shasum": "" }, "require": { @@ -4362,9 +4425,9 @@ ], "support": { "issues": "https://github.com/horstoeko/zugferd/issues", - "source": "https://github.com/horstoeko/zugferd/tree/v1.0.34" + "source": "https://github.com/horstoeko/zugferd/tree/v1.0.36" }, - "time": "2024-01-27T09:14:13+00:00" + "time": "2024-03-11T04:34:59+00:00" }, { "name": "http-interop/http-factory-guzzle", @@ -4861,16 +4924,16 @@ }, { "name": "jean85/pretty-package-versions", - "version": "2.0.5", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", "shasum": "" }, "require": { @@ -4878,9 +4941,9 @@ "php": "^7.1|^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.17", + "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^0.12.66", + "phpstan/phpstan": "^1.4", "phpunit/phpunit": "^7.5|^8.5|^9.4", "vimeo/psalm": "^4.3" }, @@ -4914,9 +4977,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" }, - "time": "2021-10-08T21:21:46+00:00" + "time": "2024-03-08T09:58:59+00:00" }, { "name": "jms/metadata", @@ -5295,16 +5358,16 @@ }, { "name": "laravel/framework", - "version": "v10.47.0", + "version": "v10.48.3", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "fce29b8de62733cdecbe12e3bae801f83fff2ea4" + "reference": "5791c052b41c6b593556adc687076bfbdd13c501" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/fce29b8de62733cdecbe12e3bae801f83fff2ea4", - "reference": "fce29b8de62733cdecbe12e3bae801f83fff2ea4", + "url": "https://api.github.com/repos/laravel/framework/zipball/5791c052b41c6b593556adc687076bfbdd13c501", + "reference": "5791c052b41c6b593556adc687076bfbdd13c501", "shasum": "" }, "require": { @@ -5352,6 +5415,7 @@ "conflict": { "carbonphp/carbon-doctrine-types": ">=3.0", "doctrine/dbal": ">=4.0", + "mockery/mockery": "1.6.8", "phpunit/phpunit": ">=11.0.0", "tightenco/collect": "<5.5.33" }, @@ -5497,7 +5561,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-03-05T15:18:36+00:00" + "time": "2024-03-15T10:17:07+00:00" }, { "name": "laravel/prompts", @@ -5877,34 +5941,34 @@ }, { "name": "lcobucci/clock", - "version": "3.0.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/lcobucci/clock.git", - "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc" + "reference": "6f28b826ea01306b07980cb8320ab30b966cd715" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/039ef98c6b57b101d10bd11d8fdfda12cbd996dc", - "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/6f28b826ea01306b07980cb8320ab30b966cd715", + "reference": "6f28b826ea01306b07980cb8320ab30b966cd715", "shasum": "" }, "require": { - "php": "~8.1.0 || ~8.2.0", + "php": "~8.2.0 || ~8.3.0", "psr/clock": "^1.0" }, "provide": { "psr/clock-implementation": "1.0" }, "require-dev": { - "infection/infection": "^0.26", - "lcobucci/coding-standard": "^9.0", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-deprecation-rules": "^1.1.1", - "phpstan/phpstan-phpunit": "^1.3.2", - "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^9.5.27" + "infection/infection": "^0.27", + "lcobucci/coding-standard": "^11.0.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.10.25", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.13", + "phpstan/phpstan-strict-rules": "^1.5.1", + "phpunit/phpunit": "^10.2.3" }, "type": "library", "autoload": { @@ -5925,7 +5989,7 @@ "description": "Yet another clock abstraction", "support": { "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/3.0.0" + "source": "https://github.com/lcobucci/clock/tree/3.2.0" }, "funding": [ { @@ -5937,7 +6001,7 @@ "type": "patreon" } ], - "time": "2022-12-19T15:00:24+00:00" + "time": "2023-11-17T17:00:27+00:00" }, { "name": "lcobucci/jwt", @@ -6292,16 +6356,16 @@ }, { "name": "league/flysystem", - "version": "3.24.0", + "version": "3.25.1", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "b25a361508c407563b34fac6f64a8a17a8819675" + "reference": "abbd664eb4381102c559d358420989f835208f18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/b25a361508c407563b34fac6f64a8a17a8819675", - "reference": "b25a361508c407563b34fac6f64a8a17a8819675", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/abbd664eb4381102c559d358420989f835208f18", + "reference": "abbd664eb4381102c559d358420989f835208f18", "shasum": "" }, "require": { @@ -6329,7 +6393,7 @@ "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "microsoft/azure-storage-blob": "^1.1", - "phpseclib/phpseclib": "^3.0.34", + "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", "sabre/dav": "^4.6.0" @@ -6366,7 +6430,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.24.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.25.1" }, "funding": [ { @@ -6378,20 +6442,20 @@ "type": "github" } ], - "time": "2024-02-04T12:10:17+00:00" + "time": "2024-03-16T12:53:19+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "3.24.0", + "version": "3.25.1", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513" + "reference": "6a5be0e6d6a93574e80805c9cc108a4b63c824d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513", - "reference": "809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/6a5be0e6d6a93574e80805c9cc108a4b63c824d8", + "reference": "6a5be0e6d6a93574e80805c9cc108a4b63c824d8", "shasum": "" }, "require": { @@ -6431,7 +6495,7 @@ "storage" ], "support": { - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.24.0" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.25.1" }, "funding": [ { @@ -6443,20 +6507,20 @@ "type": "github" } ], - "time": "2024-01-26T18:43:21+00:00" + "time": "2024-03-15T19:58:44+00:00" }, { "name": "league/flysystem-local", - "version": "3.23.1", + "version": "3.25.1", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00" + "reference": "61a6a90d6e999e4ddd9ce5adb356de0939060b92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/b884d2bf9b53bb4804a56d2df4902bb51e253f00", - "reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/61a6a90d6e999e4ddd9ce5adb356de0939060b92", + "reference": "61a6a90d6e999e4ddd9ce5adb356de0939060b92", "shasum": "" }, "require": { @@ -6490,8 +6554,7 @@ "local" ], "support": { - "issues": "https://github.com/thephpleague/flysystem-local/issues", - "source": "https://github.com/thephpleague/flysystem-local/tree/3.23.1" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.25.1" }, "funding": [ { @@ -6503,7 +6566,7 @@ "type": "github" } ], - "time": "2024-01-26T18:25:23+00:00" + "time": "2024-03-15T19:58:44+00:00" }, { "name": "league/fractal", @@ -6946,16 +7009,16 @@ }, { "name": "livewire/livewire", - "version": "v3.4.7", + "version": "v3.4.9", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "05f25dab062cd6a1ec24d8df9e889f890c832cb0" + "reference": "c65b3f0798ab2c9338213ede3588c3cdf4e6fcc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/05f25dab062cd6a1ec24d8df9e889f890c832cb0", - "reference": "05f25dab062cd6a1ec24d8df9e889f890c832cb0", + "url": "https://api.github.com/repos/livewire/livewire/zipball/c65b3f0798ab2c9338213ede3588c3cdf4e6fcc0", + "reference": "c65b3f0798ab2c9338213ede3588c3cdf4e6fcc0", "shasum": "" }, "require": { @@ -7009,7 +7072,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.4.7" + "source": "https://github.com/livewire/livewire/tree/v3.4.9" }, "funding": [ { @@ -7017,7 +7080,7 @@ "type": "github" } ], - "time": "2024-03-05T15:54:03+00:00" + "time": "2024-03-14T14:03:32+00:00" }, { "name": "maennchen/zipstream-php", @@ -8298,16 +8361,16 @@ }, { "name": "omnipay/common", - "version": "v3.2.1", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-common.git", - "reference": "80545e9f4faab0efad36cc5f1e11a184dda22baf" + "reference": "2eca3823e9069e2c36b6007a090577d5584f9518" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/omnipay-common/zipball/80545e9f4faab0efad36cc5f1e11a184dda22baf", - "reference": "80545e9f4faab0efad36cc5f1e11a184dda22baf", + "url": "https://api.github.com/repos/thephpleague/omnipay-common/zipball/2eca3823e9069e2c36b6007a090577d5584f9518", + "reference": "2eca3823e9069e2c36b6007a090577d5584f9518", "shasum": "" }, "require": { @@ -8317,13 +8380,14 @@ "php-http/discovery": "^1.14", "php-http/message": "^1.5", "php-http/message-factory": "^1.1", - "symfony/http-foundation": "^2.1|^3|^4|^5|^6" + "symfony/http-foundation": "^2.1|^3|^4|^5|^6|^7" }, "require-dev": { + "http-interop/http-factory-guzzle": "^1.1", "omnipay/tests": "^4.1", "php-http/guzzle7-adapter": "^1", - "php-http/mock-client": "^1", - "squizlabs/php_codesniffer": "^3.5" + "php-http/mock-client": "^1.6", + "squizlabs/php_codesniffer": "^3.8.1" }, "suggest": { "league/omnipay": "The default Omnipay package provides a default HTTP Adapter." @@ -8379,7 +8443,7 @@ ], "support": { "issues": "https://github.com/thephpleague/omnipay-common/issues", - "source": "https://github.com/thephpleague/omnipay-common/tree/v3.2.1" + "source": "https://github.com/thephpleague/omnipay-common/tree/v3.3.0" }, "funding": [ { @@ -8387,7 +8451,7 @@ "type": "github" } ], - "time": "2023-05-30T12:44:03+00:00" + "time": "2024-03-08T11:56:40+00:00" }, { "name": "omnipay/paypal", @@ -9102,16 +9166,16 @@ }, { "name": "php-http/promise", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/php-http/promise.git", - "reference": "2916a606d3b390f4e9e8e2b8dd68581508be0f07" + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/promise/zipball/2916a606d3b390f4e9e8e2b8dd68581508be0f07", - "reference": "2916a606d3b390f4e9e8e2b8dd68581508be0f07", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", "shasum": "" }, "require": { @@ -9148,9 +9212,9 @@ ], "support": { "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.3.0" + "source": "https://github.com/php-http/promise/tree/1.3.1" }, - "time": "2024-01-04T18:49:48+00:00" + "time": "2024-03-15T13:55:21+00:00" }, { "name": "php-jsonpointer/php-jsonpointer", @@ -10232,16 +10296,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.0", + "version": "v0.12.2", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "750bf031a48fd07c673dbe3f11f72362ea306d0d" + "reference": "9185c66c2165bbf4d71de78a69dccf4974f9538d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/750bf031a48fd07c673dbe3f11f72362ea306d0d", - "reference": "750bf031a48fd07c673dbe3f11f72362ea306d0d", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/9185c66c2165bbf4d71de78a69dccf4974f9538d", + "reference": "9185c66c2165bbf4d71de78a69dccf4974f9538d", "shasum": "" }, "require": { @@ -10305,9 +10369,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.0" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.2" }, - "time": "2023-12-20T15:28:09+00:00" + "time": "2024-03-17T01:53:00+00:00" }, { "name": "pusher/pusher-php-server", @@ -11772,16 +11836,16 @@ }, { "name": "spatie/php-structure-discoverer", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/spatie/php-structure-discoverer.git", - "reference": "f5b3c935dda89d6c382b27e3caf348fa80bcfa88" + "reference": "24f5221641560ec0f7dce23dd814e7d555b0098b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/f5b3c935dda89d6c382b27e3caf348fa80bcfa88", - "reference": "f5b3c935dda89d6c382b27e3caf348fa80bcfa88", + "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/24f5221641560ec0f7dce23dd814e7d555b0098b", + "reference": "24f5221641560ec0f7dce23dd814e7d555b0098b", "shasum": "" }, "require": { @@ -11840,7 +11904,7 @@ ], "support": { "issues": "https://github.com/spatie/php-structure-discoverer/issues", - "source": "https://github.com/spatie/php-structure-discoverer/tree/2.1.0" + "source": "https://github.com/spatie/php-structure-discoverer/tree/2.1.1" }, "funding": [ { @@ -11848,7 +11912,7 @@ "type": "github" } ], - "time": "2024-02-16T12:42:24+00:00" + "time": "2024-03-13T16:08:30+00:00" }, { "name": "sprain/swiss-qr-bill", @@ -12034,6 +12098,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", @@ -12130,20 +12263,20 @@ }, { "name": "symfony/css-selector", - "version": "v6.4.3", + "version": "v7.0.3", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "ee0f7ed5cf298cc019431bb3b3977ebc52b86229" + "reference": "ec60a4edf94e63b0556b6a0888548bb400a3a3be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/ee0f7ed5cf298cc019431bb3b3977ebc52b86229", - "reference": "ee0f7ed5cf298cc019431bb3b3977ebc52b86229", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/ec60a4edf94e63b0556b6a0888548bb400a3a3be", + "reference": "ec60a4edf94e63b0556b6a0888548bb400a3a3be", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -12175,7 +12308,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.4.3" + "source": "https://github.com/symfony/css-selector/tree/v7.0.3" }, "funding": [ { @@ -12191,7 +12324,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-01-23T15:02:46+00:00" }, { "name": "symfony/deprecation-contracts", @@ -12337,24 +12470,24 @@ }, { "name": "symfony/event-dispatcher", - "version": "v6.4.3", + "version": "v7.0.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef" + "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae9d3a6f3003a6caf56acd7466d8d52378d44fef", - "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/834c28d533dd0636f910909d01b9ff45cc094b5e", + "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<5.4", + "symfony/dependency-injection": "<6.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -12363,13 +12496,13 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^5.4|^6.0|^7.0" + "symfony/stopwatch": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -12397,7 +12530,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.3" }, "funding": [ { @@ -12413,7 +12546,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-01-23T15:02:46+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -12981,25 +13114,25 @@ }, { "name": "symfony/intl", - "version": "v6.4.3", + "version": "v7.0.3", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "2628ded562ca132ed7cdea72f5ec6aaf65d94414" + "reference": "295995df4acf6790a35b9ce6ec32b313efb11ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/2628ded562ca132ed7cdea72f5ec6aaf65d94414", - "reference": "2628ded562ca132ed7cdea72f5ec6aaf65d94414", + "url": "https://api.github.com/repos/symfony/intl/zipball/295995df4acf6790a35b9ce6ec32b313efb11ff8", + "reference": "295995df4acf6790a35b9ce6ec32b313efb11ff8", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", - "symfony/var-exporter": "^5.4|^6.0|^7.0" + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -13043,7 +13176,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v6.4.3" + "source": "https://github.com/symfony/intl/tree/v7.0.3" }, "funding": [ { @@ -13059,7 +13192,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-01-23T15:02:46+00:00" }, { "name": "symfony/mailer", @@ -13296,20 +13429,20 @@ }, { "name": "symfony/options-resolver", - "version": "v6.4.0", + "version": "v7.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "22301f0e7fdeaacc14318928612dee79be99860e" + "reference": "700ff4096e346f54cb628ea650767c8130f1001f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22301f0e7fdeaacc14318928612dee79be99860e", - "reference": "22301f0e7fdeaacc14318928612dee79be99860e", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f", + "reference": "700ff4096e346f54cb628ea650767c8130f1001f", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -13343,7 +13476,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.4.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.0.0" }, "funding": [ { @@ -13359,7 +13492,7 @@ "type": "tidelift" } ], - "time": "2023-08-08T10:16:24+00:00" + "time": "2023-08-08T10:20:21+00:00" }, { "name": "symfony/polyfill-ctype", @@ -14543,20 +14676,20 @@ }, { "name": "symfony/string", - "version": "v6.4.4", + "version": "v7.0.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9" + "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", - "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", + "url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b", + "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", @@ -14566,11 +14699,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/intl": "^6.2|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0|^7.0" + "symfony/var-exporter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -14609,7 +14742,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.4" + "source": "https://github.com/symfony/string/tree/v7.0.4" }, "funding": [ { @@ -14625,7 +14758,7 @@ "type": "tidelift" } ], - "time": "2024-02-01T13:16:41+00:00" + "time": "2024-02-01T13:17:36+00:00" }, { "name": "symfony/translation", @@ -15383,7 +15516,7 @@ "version": "6.44.4", "source": { "type": "git", - "url": "git@github.com:twilio/twilio-php.git", + "url": "https://github.com/twilio/twilio-php.git", "reference": "08aad5f377e2245b9cd7508e7762d95e7392fa4d" }, "dist": { @@ -15425,6 +15558,10 @@ "sms", "twilio" ], + "support": { + "issues": "https://github.com/twilio/twilio-php/issues", + "source": "https://github.com/twilio/twilio-php/tree/6.44.4" + }, "time": "2023-02-22T19:59:53+00:00" }, { @@ -15803,23 +15940,23 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.10.6", + "version": "v3.12.2", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "1fcb37307ebb32207dce16fa160a92b14d8b671f" + "reference": "43555503052443964ce2c1c1f3b0378e58219eb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/1fcb37307ebb32207dce16fa160a92b14d8b671f", - "reference": "1fcb37307ebb32207dce16fa160a92b14d8b671f", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/43555503052443964ce2c1c1f3b0378e58219eb8", + "reference": "43555503052443964ce2c1c1f3b0378e58219eb8", "shasum": "" }, "require": { "illuminate/routing": "^9|^10|^11", "illuminate/session": "^9|^10|^11", "illuminate/support": "^9|^10|^11", - "maximebf/debugbar": "~1.20.1", + "maximebf/debugbar": "~1.21.0", "php": "^8.0", "symfony/finder": "^6|^7" }, @@ -15871,7 +16008,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.10.6" + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.12.2" }, "funding": [ { @@ -15883,7 +16020,7 @@ "type": "github" } ], - "time": "2024-03-01T14:41:13+00:00" + "time": "2024-03-13T09:50:34+00:00" }, { "name": "barryvdh/laravel-ide-helper", @@ -16093,16 +16230,16 @@ }, { "name": "brianium/paratest", - "version": "v7.3.1", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "551f46f52a93177d873f3be08a1649ae886b4a30" + "reference": "64fcfd0e28a6b8078a19dbf9127be2ee645b92ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/551f46f52a93177d873f3be08a1649ae886b4a30", - "reference": "551f46f52a93177d873f3be08a1649ae886b4a30", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/64fcfd0e28a6b8078a19dbf9127be2ee645b92ec", + "reference": "64fcfd0e28a6b8078a19dbf9127be2ee645b92ec", "shasum": "" }, "require": { @@ -16110,28 +16247,27 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-simplexml": "*", - "fidry/cpu-core-counter": "^0.5.1 || ^1.0.0", + "fidry/cpu-core-counter": "^1.1.0", "jean85/pretty-package-versions": "^2.0.5", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", - "phpunit/php-code-coverage": "^10.1.7", - "phpunit/php-file-iterator": "^4.1.0", - "phpunit/php-timer": "^6.0", - "phpunit/phpunit": "^10.4.2", - "sebastian/environment": "^6.0.1", - "symfony/console": "^6.3.4 || ^7.0.0", - "symfony/process": "^6.3.4 || ^7.0.0" + "php": "~8.2.0 || ~8.3.0", + "phpunit/php-code-coverage": "^10.1.11 || ^11.0.0", + "phpunit/php-file-iterator": "^4.1.0 || ^5.0.0", + "phpunit/php-timer": "^6.0.0 || ^7.0.0", + "phpunit/phpunit": "^10.5.9 || ^11.0.3", + "sebastian/environment": "^6.0.1 || ^7.0.0", + "symfony/console": "^6.4.3 || ^7.0.3", + "symfony/process": "^6.4.3 || ^7.0.3" }, "require-dev": { "doctrine/coding-standard": "^12.0.0", "ext-pcov": "*", "ext-posix": "*", - "infection/infection": "^0.27.6", - "phpstan/phpstan": "^1.10.40", + "phpstan/phpstan": "^1.10.58", "phpstan/phpstan-deprecation-rules": "^1.1.4", "phpstan/phpstan-phpunit": "^1.3.15", "phpstan/phpstan-strict-rules": "^1.5.2", - "squizlabs/php_codesniffer": "^3.7.2", - "symfony/filesystem": "^6.3.1 || ^7.0.0" + "squizlabs/php_codesniffer": "^3.9.0", + "symfony/filesystem": "^6.4.3 || ^7.0.3" }, "bin": [ "bin/paratest", @@ -16172,7 +16308,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.3.1" + "source": "https://github.com/paratestphp/paratest/tree/v7.4.3" }, "funding": [ { @@ -16184,20 +16320,20 @@ "type": "paypal" } ], - "time": "2023-10-31T09:24:17+00:00" + "time": "2024-02-20T07:24:02+00:00" }, { "name": "composer/class-map-generator", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9" + "reference": "8286a62d243312ed99b3eee20d5005c961adb311" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/953cc4ea32e0c31f2185549c7d216d7921f03da9", - "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8286a62d243312ed99b3eee20d5005c961adb311", + "reference": "8286a62d243312ed99b3eee20d5005c961adb311", "shasum": "" }, "require": { @@ -16241,7 +16377,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.1.0" + "source": "https://github.com/composer/class-map-generator/tree/1.1.1" }, "funding": [ { @@ -16257,7 +16393,7 @@ "type": "tidelift" } ], - "time": "2023-06-30T13:58:57+00:00" + "time": "2024-03-15T12:53:41+00:00" }, { "name": "composer/pcre", @@ -16915,16 +17051,16 @@ }, { "name": "maximebf/debugbar", - "version": "v1.20.2", + "version": "v1.21.3", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "484625c23a4fa4f303617f29fcacd42951c9c01d" + "reference": "0b407703b08ea0cf6ebc61e267cc96ff7000911b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/484625c23a4fa4f303617f29fcacd42951c9c01d", - "reference": "484625c23a4fa4f303617f29fcacd42951c9c01d", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/0b407703b08ea0cf6ebc61e267cc96ff7000911b", + "reference": "0b407703b08ea0cf6ebc61e267cc96ff7000911b", "shasum": "" }, "require": { @@ -16944,7 +17080,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.20-dev" + "dev-master": "1.21-dev" } }, "autoload": { @@ -16975,13 +17111,13 @@ ], "support": { "issues": "https://github.com/maximebf/php-debugbar/issues", - "source": "https://github.com/maximebf/php-debugbar/tree/v1.20.2" + "source": "https://github.com/maximebf/php-debugbar/tree/v1.21.3" }, - "time": "2024-02-15T10:49:09+00:00" + "time": "2024-03-12T14:23:07+00:00" }, { "name": "mockery/mockery", - "version": "1.6.7", + "version": "1.6.9", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", @@ -17425,16 +17561,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.60", + "version": "1.10.62", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe" + "reference": "cd5c8a1660ed3540b211407c77abf4af193a6af9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/95dcea7d6c628a3f2f56d091d8a0219485a86bbe", - "reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd5c8a1660ed3540b211407c77abf4af193a6af9", + "reference": "cd5c8a1660ed3540b211407c77abf4af193a6af9", "shasum": "" }, "require": { @@ -17483,20 +17619,20 @@ "type": "tidelift" } ], - "time": "2024-03-07T13:30:19+00:00" + "time": "2024-03-13T12:27:20+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "10.1.12", + "version": "10.1.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "842f72662d6b9edda84c4b6f13885fd9cd53dc63" + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/842f72662d6b9edda84c4b6f13885fd9cd53dc63", - "reference": "842f72662d6b9edda84c4b6f13885fd9cd53dc63", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", "shasum": "" }, "require": { @@ -17553,7 +17689,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.12" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.14" }, "funding": [ { @@ -17561,7 +17697,7 @@ "type": "github" } ], - "time": "2024-03-02T07:22:05+00:00" + "time": "2024-03-12T15:33:41+00:00" }, { "name": "phpunit/php-file-iterator", @@ -17808,16 +17944,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.11", + "version": "10.5.13", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0d968f6323deb3dbfeba5bfd4929b9415eb7a9a4" + "reference": "20a63fc1c6db29b15da3bd02d4b6cf59900088a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0d968f6323deb3dbfeba5bfd4929b9415eb7a9a4", - "reference": "0d968f6323deb3dbfeba5bfd4929b9415eb7a9a4", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/20a63fc1c6db29b15da3bd02d4b6cf59900088a7", + "reference": "20a63fc1c6db29b15da3bd02d4b6cf59900088a7", "shasum": "" }, "require": { @@ -17889,7 +18025,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.11" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.13" }, "funding": [ { @@ -17905,7 +18041,7 @@ "type": "tidelift" } ], - "time": "2024-02-25T14:05:00+00:00" + "time": "2024-03-12T15:37:41+00:00" }, { "name": "sebastian/cli-parser", @@ -19269,20 +19405,20 @@ }, { "name": "symfony/stopwatch", - "version": "v6.4.3", + "version": "v7.0.3", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "416596166641f1f728b0a64f5b9dd07cceb410c1" + "reference": "983900d6fddf2b0cbaacacbbad07610854bd8112" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/416596166641f1f728b0a64f5b9dd07cceb410c1", - "reference": "416596166641f1f728b0a64f5b9dd07cceb410c1", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/983900d6fddf2b0cbaacacbbad07610854bd8112", + "reference": "983900d6fddf2b0cbaacacbbad07610854bd8112", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/service-contracts": "^2.5|^3" }, "type": "library", @@ -19311,7 +19447,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.4.3" + "source": "https://github.com/symfony/stopwatch/tree/v7.0.3" }, "funding": [ { @@ -19327,7 +19463,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:35:58+00:00" + "time": "2024-01-23T15:02:46+00:00" }, { "name": "theseer/tokenizer", @@ -19397,5 +19533,5 @@ "platform-dev": { "php": "^8.1|^8.2" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } 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/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/lang/en/texts.php b/lang/en/texts.php index bb56e8675177..b3d101fc4542 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', @@ -4847,6 +4849,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', 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 1f770642aa3d..bf2be5dcbb0b 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; @@ -426,6 +427,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/brevo_webhook', [BrevoController::class, 'webhook'])->middleware('throttle:1000,1'); Route::post('api/v1/mailgun_webhook', [MailgunWebhookController::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');