diff --git a/_ide_helper_custom.php b/_ide_helper_custom.php index bfe7b673dd..747187a2d1 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 4c63369c33..855188dad4 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -26,7 +26,7 @@ class CompanySettings extends BaseSettings public $auto_archive_invoice = false; // @implemented public $qr_iban = ''; //@implemented - + public $besr_id = ''; //@implemented public $lock_invoices = 'off'; //off,when_sent,when_paid //@implemented @@ -229,7 +229,7 @@ class CompanySettings extends BaseSettings public $require_quote_signature = false; //@TODO ben to confirm //email settings - public $email_sending_method = 'default'; //enum 'default','gmail','office365' 'client_postmark', 'client_mailgun' //@implemented + public $email_sending_method = 'default'; //enum 'default','gmail','office365' 'client_postmark', 'client_mailgun' , 'client_brevo' //@implemented public $gmail_sending_user_id = '0'; //@implemented @@ -444,13 +444,19 @@ class CompanySettings extends BaseSettings public $postmark_secret = ''; public $custom_sending_email = ''; - + public $mailgun_secret = ''; - + public $mailgun_domain = ''; public $mailgun_endpoint = 'api.mailgun.net'; //api.eu.mailgun.net + public $brevo_secret = ''; + + public $brevo_domain = ''; + + public $brevo_endpoint = 'api.mailgun.net'; //api.eu.mailgun.net + public $auto_bill_standard_invoices = false; public $email_alignment = 'center'; // center , left, right @@ -482,9 +488,9 @@ class CompanySettings extends BaseSettings public $enable_e_invoice = false; public $delivery_note_design_id = ''; - + public $statement_design_id = ''; - + public $payment_receipt_design_id = ''; public $payment_refund_design_id = ''; @@ -494,279 +500,282 @@ class CompanySettings extends BaseSettings public $payment_email_all_contacts = false; public static $casts = [ - 'payment_email_all_contacts' => 'bool', - 'statement_design_id' => 'string', - 'delivery_note_design_id' => 'string', - 'payment_receipt_design_id' => 'string', - 'payment_refund_design_id' => 'string', - 'classification' => 'string', - 'enable_e_invoice' => 'bool', - 'classification' => 'string', - 'default_expense_payment_type_id' => 'string', - 'e_invoice_type' => 'string', - 'mailgun_endpoint' => 'string', - 'client_initiated_payments' => 'bool', - 'client_initiated_payments_minimum' => 'float', - 'sync_invoice_quote_columns' => 'bool', - 'show_task_item_description' => 'bool', - 'allow_billable_task_items' => 'bool', + 'payment_email_all_contacts' => 'bool', + 'statement_design_id' => 'string', + 'delivery_note_design_id' => 'string', + 'payment_receipt_design_id' => 'string', + 'payment_refund_design_id' => 'string', + 'classification' => 'string', + 'enable_e_invoice' => 'bool', + 'classification' => 'string', + 'default_expense_payment_type_id' => 'string', + 'e_invoice_type' => 'string', + 'mailgun_endpoint' => 'string', + 'brevo_endpoint' => 'string', + 'client_initiated_payments' => 'bool', + 'client_initiated_payments_minimum' => 'float', + 'sync_invoice_quote_columns' => 'bool', + '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', - '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', + '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', + 'brevo_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', + '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', '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', ]; /** @@ -840,9 +849,9 @@ class CompanySettings extends BaseSettings public static function setProperties($settings): stdClass { $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}); } } @@ -855,7 +864,7 @@ class CompanySettings extends BaseSettings * * @return stdClass */ - public static function notificationDefaults() :stdClass + public static function notificationDefaults(): stdClass { $notification = new stdClass; $notification->email = []; @@ -871,7 +880,7 @@ class CompanySettings extends BaseSettings * * @return stdClass */ - public static function notificationAdminDefaults() :stdClass + public static function notificationAdminDefaults(): stdClass { $notification = new stdClass; $notification->email = []; @@ -888,7 +897,7 @@ class CompanySettings extends BaseSettings * * @return stdClass The stdClass of PDF variables */ - public static function getEntityVariableDefaults() :stdClass + public static function getEntityVariableDefaults(): stdClass { $variables = [ 'client_details' => [ @@ -975,7 +984,7 @@ class CompanySettings extends BaseSettings '$product.tax', '$product.line_total', ], - 'task_columns' =>[ + 'task_columns' => [ '$task.service', '$task.description', '$task.rate', diff --git a/app/DataMapper/Settings/SettingsData.php b/app/DataMapper/Settings/SettingsData.php index 2584a56540..8f3419eb1f 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' , 'brevo_mailgun' //@implemented public string $gmail_sending_user_id = '0'; //@implemented @@ -433,6 +433,12 @@ class SettingsData public string $mailgun_endpoint = 'api.mailgun.net'; // api.eu.mailgun.net + public string $brevo_secret = ''; + + public string $brevo_domain = ''; + + public string $brevo_endpoint = 'api.mailgun.net'; // api.eu.mailgun.net + public bool $auto_bill_standard_invoices = false; public string $email_alignment = 'center'; // center, left, right @@ -464,13 +470,13 @@ class SettingsData public bool $enable_e_invoice = false; public string $classification = ''; - + private mixed $object; 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 +484,9 @@ class SettingsData try { settype($object->{$key}, gettype($this->{$key})); - } catch(\Exception | \Error | \Throwable $e) { - - if(property_exists($this, $key)) { + } catch (\Exception | \Error | \Throwable $e) { + + if (property_exists($this, $key)) { $object->{$key} = $this->{$key}; } else { unset($object->{$key}); @@ -506,11 +512,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/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index 60c3b7fbd8..8601350b02 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -62,6 +62,9 @@ class NinjaMailerJob implements ShouldQueue protected $client_mailgun_domain = false; + protected $client_brevo_secret = false; + + protected $client_brevo_domain = false; public function __construct(NinjaMailerObject $nmo, bool $override = false) { @@ -112,16 +115,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,23 +136,27 @@ 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, $this->client_brevo_domain, $this->nmo->settings->brevo_endpoint); + } + $mailer ->to($this->nmo->to_user->email) ->send($this->nmo->mailable); /* Count the amount of emails sent across all the users accounts */ - Cache::increment("email_quota".$this->company->account->key); + Cache::increment("email_quota" . $this->company->account->key); 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(); @@ -200,11 +207,11 @@ class NinjaMailerJob implements ShouldQueue app('sentry')->captureException($e); } } - + /* Releasing immediately does not add in the backoff */ sleep(rand(0, 3)); - $this->release($this->backoff()[$this->attempts()-1]); + $this->release($this->backoff()[$this->attempts() - 1]); } $this->nmo = null; @@ -272,6 +279,10 @@ class NinjaMailerJob implements ShouldQueue $this->mailer = 'mailgun'; $this->setMailgunMailer(); return $this; + case 'client_brevo': + $this->mailer = 'brevo'; + $this->setBrevoMailer(); + return $this; default: break; @@ -303,8 +314,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'))); } } } @@ -322,6 +333,10 @@ class NinjaMailerJob implements ShouldQueue $this->client_mailgun_domain = false; + $this->client_brevo_secret = false; + + $this->client_brevo_domain = false; + //always dump the drivers to prevent reuse app('mail.manager')->forgetMailers(); } @@ -381,8 +396,32 @@ class NinjaMailerJob implements ShouldQueue $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 && strlen($this->nmo->settings->brevo_domain) > 2) { + $this->client_brevo_secret = $this->nmo->settings->brevo_secret; + $this->client_brevo_domain = $this->nmo->settings->brevo_domain; + } 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); } /** @@ -404,8 +443,8 @@ class NinjaMailerJob implements ShouldQueue $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); } /** @@ -417,7 +456,7 @@ class NinjaMailerJob implements ShouldQueue $user = $this->resolveSendingUser(); $this->checkValidSendingUser($user); - + nlog("Sending via {$user->name()}"); $token = $this->refreshOfficeToken($user); @@ -431,11 +470,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); + }); } /** @@ -459,7 +498,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(); @@ -479,7 +518,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; @@ -490,11 +529,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); + }); } /** @@ -507,7 +546,7 @@ class NinjaMailerJob implements ShouldQueue private function preFlightChecksFail(): bool { /* Always send regardless */ - if($this->override) { + if ($this->override) { return false; } @@ -527,7 +566,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; } @@ -545,7 +584,7 @@ class NinjaMailerJob implements ShouldQueue if (!str_contains($this->nmo->to_user->email, "@")) { return true; } - + /* On the hosted platform if the user has not verified their account we fail here - but still check what they are trying to send! */ if (Ninja::isHosted() && $this->company->account && !$this->company->account->account_sms_verified) { if (class_exists(\Modules\Admin\Jobs\Account\EmailQuality::class)) { @@ -570,7 +609,7 @@ class NinjaMailerJob implements ShouldQueue * @param \App\Models\User | \App\Models\Client | null $recipient_object * @return void */ - private function logMailError($errors, $recipient_object) :void + private function logMailError($errors, $recipient_object): void { (new SystemLogger( $errors, @@ -586,7 +625,7 @@ class NinjaMailerJob implements ShouldQueue $job_failure->string_metric6 = substr($errors, 0, 150); LightLogs::create($job_failure) - ->send(); + ->send(); $job_failure = null; } @@ -611,14 +650,14 @@ 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 ], ])->getBody()->getContents()); - + if ($token) { $user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_token; $user->oauth_user_token = $token->access_token; diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0b4a78cc15..7bde51699e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -54,7 +54,7 @@ class AppServiceProvider extends ServiceProvider /* Defines the name used in polymorphic tables */ Relation::morphMap([ - 'invoices' => Invoice::class, + 'invoices' => Invoice::class, 'proposals' => Proposal::class, ]); @@ -96,11 +96,10 @@ class AppServiceProvider extends ServiceProvider 'transport' => 'postmark', 'token' => $postmark_key ])); - + return $this; }); - - + Mailer::macro('mailgun_config', function (string $secret, string $domain, string $endpoint = 'api.mailgun.net') { // @phpstan-ignore /** @phpstan-ignore-next-line **/ Mailer::setSymfonyTransport(app('mail.manager')->createSymfonyTransport([ @@ -110,7 +109,20 @@ class AppServiceProvider extends ServiceProvider 'endpoint' => $endpoint, 'scheme' => config('services.mailgun.scheme'), ])); - + + return $this; + }); + + Mailer::macro('brevo_config', function (string $secret, string $domain, string $endpoint = 'api.mailgun.net') { + // @phpstan-ignore /** @phpstan-ignore-next-line **/ + Mailer::setSymfonyTransport(app('mail.manager')->createSymfonyTransport([ + 'transport' => 'brevo', + 'secret' => $secret, + 'domain' => $domain, + 'endpoint' => $endpoint, + 'scheme' => config('services.brevo.scheme'), + ])); + return $this; }); diff --git a/app/Services/Email/AdminEmail.php b/app/Services/Email/AdminEmail.php index 0555cb8afc..a6f5dc6b78 100644 --- a/app/Services/Email/AdminEmail.php +++ b/app/Services/Email/AdminEmail.php @@ -55,6 +55,12 @@ class AdminEmail implements ShouldQueue protected ?string $client_mailgun_endpoint = null; + protected ?string $client_brevo_secret = null; + + protected ?string $client_brevo_domain = null; + + protected ?string $client_brevo_endpoint = null; + private string $mailer = 'default'; public Mailable $mailable; @@ -62,7 +68,7 @@ class AdminEmail implements ShouldQueue public function __construct(public EmailObject $email_object, public Company $company) { } - + /** * The backoff time between retries. * @@ -78,7 +84,7 @@ class AdminEmail implements ShouldQueue MultiDB::setDb($this->company->db); $this->setOverride() - ->buildMailable(); + ->buildMailable(); if ($this->preFlightChecksFail()) { return; @@ -87,7 +93,7 @@ class AdminEmail implements ShouldQueue $this->email(); } - + /** * Sets the override flag * @@ -99,7 +105,7 @@ class AdminEmail implements ShouldQueue return $this; } - + /** * Populates the mailable * @@ -108,10 +114,10 @@ class AdminEmail implements ShouldQueue public function buildMailable(): self { $this->mailable = new AdminEmailMailable($this->email_object); - + return $this; } - + /** * Attempts to send the email * @@ -133,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, $this->client_brevo_domain, $this->client_brevo_endpoint); + } + /* 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(); @@ -178,12 +188,12 @@ class AdminEmail implements ShouldQueue if ($e instanceof ClientException) { //postmark specific failure $response = $e->getResponse(); $message_body = json_decode($response->getBody()->getContents()); - + if ($message_body && property_exists($message_body, 'Message')) { $message = $message_body->Message; nlog($message); } - + $this->fail(); $this->cleanUpMailers(); return; @@ -202,7 +212,7 @@ class AdminEmail implements ShouldQueue sleep(rand(0, 3)); - $this->release($this->backoff()[$this->attempts()-1]); + $this->release($this->backoff()[$this->attempts() - 1]); $message = null; } @@ -211,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; } @@ -280,7 +290,7 @@ class AdminEmail implements ShouldQueue return false; } - + /** * hasInValidEmails * @@ -333,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'); @@ -365,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'))); } } } @@ -386,6 +400,12 @@ class AdminEmail implements ShouldQueue $this->client_mailgun_endpoint = null; + $this->client_brevo_secret = null; + + $this->client_brevo_domain = null; + + $this->client_brevo_endpoint = null; + //always dump the drivers to prevent reuse app('mail.manager')->forgetMailers(); } @@ -402,7 +422,7 @@ class AdminEmail implements ShouldQueue /* Always ensure the user is set on the correct account */ if ($user->account_id != $this->company->account_id) { $this->email_object->settings->email_sending_method = 'default'; - + return $this->setMailDriver(); } } @@ -448,7 +468,31 @@ 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 && strlen($this->email_object->settings->brevo_domain) > 2) { + $this->client_brevo_secret = $this->email_object->settings->brevo_secret; + $this->client_brevo_domain = $this->email_object->settings->brevo_domain; + $this->client_brevo_endpoint = $this->email_object->settings->brevo_endpoint; + + } 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); } /** @@ -468,9 +512,9 @@ class AdminEmail implements ShouldQueue $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); } /** @@ -480,9 +524,9 @@ class AdminEmail implements ShouldQueue private function setOfficeMailer() { $user = $this->resolveSendingUser(); - + $this->checkValidSendingUser($user); - + nlog("Sending via {$user->name()}"); $token = $this->refreshOfficeToken($user); @@ -496,10 +540,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); + }); } /** @@ -511,7 +555,7 @@ class AdminEmail implements ShouldQueue $user = $this->resolveSendingUser(); $this->checkValidSendingUser($user); - + nlog("Sending via {$user->name()}"); $google = (new Google())->init(); @@ -523,7 +567,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(); @@ -543,7 +587,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; @@ -554,10 +598,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); + }); } /** @@ -567,7 +611,7 @@ class AdminEmail implements ShouldQueue * @param null | \App\Models\Client $recipient_object * @return void */ - private function logMailError($errors, $recipient_object) :void + private function logMailError($errors, $recipient_object): void { (new SystemLogger( $errors, @@ -583,7 +627,7 @@ class AdminEmail implements ShouldQueue $job_failure->string_metric6 = substr($errors, 0, 150); LightLogs::create($job_failure) - ->send(); + ->send(); $job_failure = null; } @@ -604,14 +648,14 @@ 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 ], ])->getBody()->getContents()); - + if ($token) { $user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_token; $user->oauth_user_token = $token->access_token; diff --git a/app/Services/Email/Email.php b/app/Services/Email/Email.php index fa7cd008e3..caffd425e3 100644 --- a/app/Services/Email/Email.php +++ b/app/Services/Email/Email.php @@ -60,6 +60,12 @@ class Email implements ShouldQueue protected ?string $client_mailgun_endpoint = null; + protected ?string $client_brevo_secret = null; + + protected ?string $client_brevo_domain = null; + + protected ?string $client_brevo_endpoint = null; + private string $mailer = 'default'; public Mailable $mailable; @@ -67,7 +73,7 @@ class Email implements ShouldQueue public function __construct(public EmailObject $email_object, public Company $company) { } - + /** * The backoff time between retries. * @@ -83,9 +89,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(); @@ -98,7 +104,7 @@ class Email implements ShouldQueue $this->tearDown(); } - + /** * Sets the override flag * @@ -110,7 +116,7 @@ class Email implements ShouldQueue return $this; } - + /** * Initilializes the models * @@ -122,19 +128,19 @@ class Email implements ShouldQueue $this->email_object->invitation_id ? $this->email_object->invitation = $this->email_object->entity->invitations()->where('id', $this->email_object->invitation_id)->first() : $this->email_object->invitation = null; - $this->email_object->invitation_id ? $this->email_object->contact = $this->email_object->invitation->contact : $this->email_object->contact = null; + $this->email_object->invitation_id ? $this->email_object->contact = $this->email_object->invitation->contact : $this->email_object->contact = null; $this->email_object->client_id ? $this->email_object->client = Client::withTrashed()->find($this->email_object->client_id) : $this->email_object->client = null; - - $this->email_object->vendor_id ? $this->email_object->vendor = Vendor::withTrashed()->find($this->email_object->vendor_id) : $this->email_object->vendor = null; - - if (!$this->email_object->contact) { - $this->email_object->vendor_contact_id ? $this->email_object->contact = VendorContact::withTrashed()->find($this->email_object->vendor_contact_id) : null; - $this->email_object->client_contact_id ? $this->email_object->contact = ClientContact::withTrashed()->find($this->email_object->client_contact_id) : null; + $this->email_object->vendor_id ? $this->email_object->vendor = Vendor::withTrashed()->find($this->email_object->vendor_id) : $this->email_object->vendor = null; + + if (!$this->email_object->contact) { + $this->email_object->vendor_contact_id ? $this->email_object->contact = VendorContact::withTrashed()->find($this->email_object->vendor_contact_id) : null; + + $this->email_object->client_contact_id ? $this->email_object->contact = ClientContact::withTrashed()->find($this->email_object->client_contact_id) : null; } - $this->email_object->user_id ? $this->email_object->user = User::withTrashed()->find($this->email_object->user_id) : $this->email_object->user = $this->company->owner(); + $this->email_object->user_id ? $this->email_object->user = User::withTrashed()->find($this->email_object->user_id) : $this->email_object->user = $this->company->owner(); $this->email_object->company_key = $this->company->company_key; @@ -151,12 +157,12 @@ class Email implements ShouldQueue $this->email_object->signature = $this->email_object->settings->email_signature; $this->email_object->invitation_key = $this->email_object->invitation ? $this->email_object->invitation->key : null; - + $this->resolveVariables(); return $this; } - + /** * Generates the correct set of variables * @todo handle payment engine here also @@ -165,7 +171,7 @@ class Email implements ShouldQueue private function resolveVariables(): self { $_variables = $this->email_object->variables; - + match (class_basename($this->email_object->entity)) { "Invoice" => $this->email_object->variables = (new HtmlEngine($this->email_object->invitation))->makeValues(), "Quote" => $this->email_object->variables = (new HtmlEngine($this->email_object->invitation))->makeValues(), @@ -181,7 +187,7 @@ class Email implements ShouldQueue return $this; } - + /** * tearDown * @@ -199,7 +205,7 @@ class Email implements ShouldQueue return $this; } - + /** * Builds the email defaults * @@ -211,7 +217,7 @@ class Email implements ShouldQueue return $this; } - + /** * Populates the mailable * @@ -220,10 +226,10 @@ class Email implements ShouldQueue public function buildMailable(): self { $this->mailable = new EmailMailable($this->email_object); - + return $this; } - + /** * Attempts to send the email * @@ -245,24 +251,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, $this->client_brevo_domain, $this->client_brevo_endpoint); + } + /* 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(); @@ -288,7 +298,7 @@ class Email implements ShouldQueue $address_object = reset($this->email_object->to); $email = $address_object->address ?? ''; - + $message = "Recipient {$email} has been suppressed and cannot receive emails from you."; $this->fail(); @@ -306,12 +316,12 @@ class Email implements ShouldQueue if ($e instanceof ClientException) { //postmark specific failure $response = $e->getResponse(); $message_body = json_decode($response->getBody()->getContents()); - + if ($message_body && property_exists($message_body, 'Message')) { $message = $message_body->Message; nlog($message); } - + $this->fail(); $this->cleanUpMailers(); return; @@ -333,7 +343,7 @@ class Email implements ShouldQueue sleep(rand(0, 3)); - $this->release($this->backoff()[$this->attempts()-1]); + $this->release($this->backoff()[$this->attempts() - 1]); $message = null; } @@ -342,16 +352,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; } @@ -411,7 +421,7 @@ class Email implements ShouldQueue return false; } - + /** * hasInValidEmails * @@ -436,7 +446,7 @@ class Email implements ShouldQueue return true; } - if($address_object->name == " " || $address_object->name == "") { + if ($address_object->name == " " || $address_object->name == "") { return true; } } @@ -472,6 +482,10 @@ class Email 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'); @@ -504,7 +518,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'))); } } } @@ -525,6 +539,12 @@ class Email implements ShouldQueue $this->client_mailgun_endpoint = null; + $this->client_brevo_secret = null; + + $this->client_brevo_domain = null; + + $this->client_brevo_endpoint = null; + //always dump the drivers to prevent reuse app('mail.manager')->forgetMailers(); } @@ -541,7 +561,7 @@ class Email implements ShouldQueue /* Always ensure the user is set on the correct account */ if ($user->account_id != $this->company->account_id) { $this->email_object->settings->email_sending_method = 'default'; - + return $this->setMailDriver(); } } @@ -587,7 +607,31 @@ class Email 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 && strlen($this->email_object->settings->brevo_domain) > 2) { + $this->client_brevo_secret = $this->email_object->settings->brevo_secret; + $this->client_brevo_domain = $this->email_object->settings->brevo_domain; + $this->client_brevo_endpoint = $this->email_object->settings->brevo_endpoint; + + } 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); } /** @@ -607,9 +651,9 @@ class Email implements ShouldQueue $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); } /** @@ -619,9 +663,9 @@ class Email implements ShouldQueue private function setOfficeMailer() { $user = $this->resolveSendingUser(); - + $this->checkValidSendingUser($user); - + nlog("Sending via {$user->name()}"); $token = $this->refreshOfficeToken($user); @@ -635,10 +679,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); + }); } /** @@ -650,7 +694,7 @@ class Email implements ShouldQueue $user = $this->resolveSendingUser(); $this->checkValidSendingUser($user); - + nlog("Sending via {$user->name()}"); $google = (new Google())->init(); @@ -662,7 +706,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(); @@ -682,7 +726,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; @@ -693,10 +737,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); + }); } /** @@ -706,7 +750,7 @@ class Email implements ShouldQueue * @param null | \App\Models\Client $recipient_object * @return void */ - private function logMailError($errors, $recipient_object) :void + private function logMailError($errors, $recipient_object): void { (new SystemLogger( $errors, @@ -722,7 +766,7 @@ class Email implements ShouldQueue $job_failure->string_metric6 = substr($errors, 0, 150); LightLogs::create($job_failure) - ->send(); + ->send(); $job_failure = null; } @@ -743,14 +787,14 @@ 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 ], ])->getBody()->getContents()); - + if ($token) { $user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_token; $user->oauth_user_token = $token->access_token; diff --git a/composer.json b/composer.json index 1ef66cdd17..24f9adb861 100644 --- a/composer.json +++ b/composer.json @@ -91,6 +91,7 @@ "sprain/swiss-qr-bill": "^4.3", "square/square": "30.0.0.*", "stripe/stripe-php": "^12", + "symfony/brevo-mailer": "^7.0", "symfony/http-client": "^6.0", "symfony/mailgun-mailer": "^6.1", "symfony/postmark-mailer": "^6.1", @@ -179,4 +180,4 @@ ], "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 27dc537efd..4bafa42c02 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": "28b57fe6eac3d71c607125cda9a6a537", + "content-hash": "ba57ee16621201bac10def4422ba9161", "packages": [ { "name": "afosto/yaac", @@ -10716,6 +10716,75 @@ }, "time": "2023-10-16T18:04:12+00:00" }, + { + "name": "symfony/brevo-mailer", + "version": "v7.0.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" + }, + "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.1", @@ -18046,5 +18115,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 0cd235b404..a9a86b08ac 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 e252ef9a4b..aa1f51e03e 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. | @@ -25,6 +25,13 @@ return [ 'scheme' => 'https', ], + 'brevo' => [ + 'domain' => env('BREVO_DOMAIN', ''), + 'secret' => env('BREVO_SECRET', ''), + 'endpoint' => env('BREVO_ENDPOINT', 'api.mailgun.net'), + 'scheme' => 'https', + ], + 'postmark' => [ 'token' => env('POSTMARK_SECRET', ''), ], @@ -55,8 +62,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 a4bc7734fc..db98c6da32 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -2237,6 +2237,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', @@ -3857,308 +3859,308 @@ $lang = array( 'registration_url' => 'Registration URL', 'show_product_cost' => 'Show Product Cost', 'complete' => 'Complete', - 'next' => 'Next', - 'next_step' => 'Next step', - 'notification_credit_sent_subject' => 'Credit :invoice was sent to :client', - 'notification_credit_viewed_subject' => 'Credit :invoice was viewed by :client', - 'notification_credit_sent' => 'The following client :client was emailed Credit :invoice for :amount.', - 'notification_credit_viewed' => 'The following client :client viewed Credit :credit for :amount.', - 'reset_password_text' => 'Enter your email to reset your password.', - 'password_reset' => 'Password reset', - 'account_login_text' => 'Welcome! Glad to see you.', - 'request_cancellation' => 'Request cancellation', - 'delete_payment_method' => 'Delete Payment Method', - 'about_to_delete_payment_method' => 'You are about to delete the payment method.', - 'action_cant_be_reversed' => 'Action can\'t be reversed', - 'profile_updated_successfully' => 'The profile has been updated successfully.', - 'currency_ethiopian_birr' => 'Ethiopian Birr', - 'client_information_text' => 'Use a permanent address where you can receive mail.', - 'status_id' => 'Invoice Status', - 'email_already_register' => 'This email is already linked to an account', - 'locations' => 'Locations', - 'freq_indefinitely' => 'Indefinitely', - 'cycles_remaining' => 'Cycles remaining', - 'i_understand_delete' => 'I understand, delete', - 'download_files' => 'Download Files', - 'download_timeframe' => 'Use this link to download your files, the link will expire in 1 hour.', - 'new_signup' => 'New Signup', - 'new_signup_text' => 'A new account has been created by :user - :email - from IP address: :ip', - 'notification_payment_paid_subject' => 'Payment was made by :client', - 'notification_partial_payment_paid_subject' => 'Partial payment was made by :client', - 'notification_payment_paid' => 'A payment of :amount was made by client :client towards :invoice', - 'notification_partial_payment_paid' => 'A partial payment of :amount was made by client :client towards :invoice', - 'notification_bot' => 'Notification Bot', - 'invoice_number_placeholder' => 'Invoice # :invoice', - 'entity_number_placeholder' => ':entity # :entity_number', - 'email_link_not_working' => 'If the button above isn\'t working for you, please click on the link', - 'display_log' => 'Display Log', - 'send_fail_logs_to_our_server' => 'Report errors in realtime', - 'setup' => 'Setup', - 'quick_overview_statistics' => 'Quick overview & statistics', - 'update_your_personal_info' => 'Update your personal information', - 'name_website_logo' => 'Name, website & logo', - 'make_sure_use_full_link' => 'Make sure you use full link to your site', - 'personal_address' => 'Personal address', - 'enter_your_personal_address' => 'Enter your personal address', - 'enter_your_shipping_address' => 'Enter your shipping address', - 'list_of_invoices' => 'List of invoices', - 'with_selected' => 'With selected', - 'invoice_still_unpaid' => 'This invoice is still not paid. Click the button to complete the payment', - 'list_of_recurring_invoices' => 'List of recurring invoices', - 'details_of_recurring_invoice' => 'Here are some details about recurring invoice', - 'cancellation' => 'Cancellation', - 'about_cancellation' => 'In case you want to stop the recurring invoice, please click to request the cancellation.', - 'cancellation_warning' => 'Warning! You are requesting a cancellation of this service. Your service may be cancelled with no further notification to you.', - 'cancellation_pending' => 'Cancellation pending, we\'ll be in touch!', - 'list_of_payments' => 'List of payments', - 'payment_details' => 'Details of the payment', - 'list_of_payment_invoices' => 'List of invoices affected by the payment', - 'list_of_payment_methods' => 'List of payment methods', - 'payment_method_details' => 'Details of payment method', - 'permanently_remove_payment_method' => 'Permanently remove this payment method.', - 'warning_action_cannot_be_reversed' => 'Warning! This action can not be reversed!', - 'confirmation' => 'Confirmation', - 'list_of_quotes' => 'Quotes', - 'waiting_for_approval' => 'Waiting for approval', - 'quote_still_not_approved' => 'This quote is still not approved', - 'list_of_credits' => 'Credits', - 'required_extensions' => 'Required extensions', - 'php_version' => 'PHP version', - 'writable_env_file' => 'Writable .env file', - 'env_not_writable' => '.env file is not writable by the current user.', - 'minumum_php_version' => 'Minimum PHP version', - 'satisfy_requirements' => 'Make sure all requirements are satisfied.', - 'oops_issues' => 'Oops, something does not look right!', - 'open_in_new_tab' => 'Open in new tab', - 'complete_your_payment' => 'Complete payment', - 'authorize_for_future_use' => 'Authorize payment method for future use', - 'page' => 'Page', - 'per_page' => 'Per page', - 'of' => 'Of', - 'view_credit' => 'View Credit', - 'to_view_entity_password' => 'To view the :entity you need to enter password.', - 'showing_x_of' => 'Showing :first to :last out of :total results', - 'no_results' => 'No results found.', - 'payment_failed_subject' => 'Payment failed for Client :client', - 'payment_failed_body' => 'A payment made by client :client failed with message :message', - 'register' => 'Register', - 'register_label' => 'Create your account in seconds', - 'password_confirmation' => 'Confirm your password', - 'verification' => 'Verification', - 'complete_your_bank_account_verification' => 'Before using a bank account it must be verified.', - 'checkout_com' => 'Checkout.com', - 'footer_label' => 'Copyright © :year :company.', - 'credit_card_invalid' => 'Provided credit card number is not valid.', - 'month_invalid' => 'Provided month is not valid.', - 'year_invalid' => 'Provided year is not valid.', - 'https_required' => 'HTTPS is required, form will fail', - 'if_you_need_help' => 'If you need help you can post to our', - 'update_password_on_confirm' => 'After updating password, your account will be confirmed.', - 'bank_account_not_linked' => 'To pay with a bank account, first you have to add it as payment method.', - 'application_settings_label' => 'Let\'s store basic information about your Invoice Ninja!', - 'recommended_in_production' => 'Highly recommended in production', - 'enable_only_for_development' => 'Enable only for development', - 'test_pdf' => 'Test PDF', - 'checkout_authorize_label' => 'Checkout.com can be can saved as payment method for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.', - 'sofort_authorize_label' => 'Bank account (SOFORT) can be can saved as payment method for future use, once you complete your first transaction. Don\'t forget to check "Store payment details" during payment process.', - 'node_status' => 'Node status', - 'npm_status' => 'NPM status', - 'node_status_not_found' => 'I could not find Node anywhere. Is it installed?', - 'npm_status_not_found' => 'I could not find NPM anywhere. Is it installed?', - 'locked_invoice' => 'This invoice is locked and unable to be modified', - 'downloads' => 'Downloads', - 'resource' => 'Resource', - 'document_details' => 'Details about the document', - 'hash' => 'Hash', - 'resources' => 'Resources', - 'allowed_file_types' => 'Allowed file types:', - 'common_codes' => 'Common codes and their meanings', - 'payment_error_code_20087' => '20087: Bad Track Data (invalid CVV and/or expiry date)', - 'download_selected' => 'Download selected', - 'to_pay_invoices' => 'To pay invoices, you have to', - 'add_payment_method_first' => 'add payment method', - 'no_items_selected' => 'No items selected.', - 'payment_due' => 'Payment due', - 'account_balance' => 'Account Balance', - 'thanks' => 'Thanks', - 'minimum_required_payment' => 'Minimum required payment is :amount', - 'under_payments_disabled' => 'Company doesn\'t support underpayments.', - 'over_payments_disabled' => 'Company doesn\'t support overpayments.', - 'saved_at' => 'Saved at :time', - 'credit_payment' => 'Credit applied to Invoice :invoice_number', - 'credit_subject' => 'New credit :number from :account', - 'credit_message' => 'To view your credit for :amount, click the link below.', - 'payment_type_Crypto' => 'Cryptocurrency', - 'payment_type_Credit' => 'Credit', - 'store_for_future_use' => 'Store for future use', - 'pay_with_credit' => 'Pay with credit', - 'payment_method_saving_failed' => 'Payment method can\'t be saved for future use.', - 'pay_with' => 'Pay with', - 'n/a' => 'N/A', - 'by_clicking_next_you_accept_terms' => 'By clicking "Next step" you accept terms.', - 'not_specified' => 'Not specified', - 'before_proceeding_with_payment_warning' => 'Before proceeding with payment, you have to fill following fields', - 'after_completing_go_back_to_previous_page' => 'After completing, go back to previous page.', - 'pay' => 'Pay', - 'instructions' => 'Instructions', - 'notification_invoice_reminder1_sent_subject' => 'Reminder 1 for Invoice :invoice was sent to :client', - 'notification_invoice_reminder2_sent_subject' => 'Reminder 2 for Invoice :invoice was sent to :client', - 'notification_invoice_reminder3_sent_subject' => 'Reminder 3 for Invoice :invoice was sent to :client', - 'notification_invoice_custom_sent_subject' => 'Custom reminder for Invoice :invoice was sent to :client', - 'notification_invoice_reminder_endless_sent_subject' => 'Endless reminder for Invoice :invoice was sent to :client', - 'assigned_user' => 'Assigned User', - 'setup_steps_notice' => 'To proceed to next step, make sure you test each section.', - 'setup_phantomjs_note' => 'Note about Phantom JS. Read more.', - 'minimum_payment' => 'Minimum Payment', - 'no_action_provided' => 'No action provided. If you believe this is wrong, please contact the support.', - 'no_payable_invoices_selected' => 'No payable invoices selected. Make sure you are not trying to pay draft invoice or invoice with zero balance due.', - 'required_payment_information' => 'Required payment details', - 'required_payment_information_more' => 'To complete a payment we need more details about you.', - 'required_client_info_save_label' => 'We will save this, so you don\'t have to enter it next time.', - 'notification_credit_bounced' => 'We were unable to deliver Credit :invoice to :contact. \n :error', - 'notification_credit_bounced_subject' => 'Unable to deliver Credit :invoice', - 'save_payment_method_details' => 'Save payment method details', - 'new_card' => 'New card', - 'new_bank_account' => 'New bank account', - 'company_limit_reached' => 'Limit of :limit companies per account.', - 'credits_applied_validation' => 'Total credits applied cannot be MORE than total of invoices', - 'credit_number_taken' => 'Credit number already taken', - 'credit_not_found' => 'Credit not found', - 'invoices_dont_match_client' => 'Selected invoices are not from a single client', - 'duplicate_credits_submitted' => 'Duplicate credits submitted.', - 'duplicate_invoices_submitted' => 'Duplicate invoices submitted.', - 'credit_with_no_invoice' => 'You must have an invoice set when using a credit in a payment', - 'client_id_required' => 'Client id is required', - 'expense_number_taken' => 'Expense number already taken', - 'invoice_number_taken' => 'Invoice number already taken', - 'payment_id_required' => 'Payment `id` required.', - 'unable_to_retrieve_payment' => 'Unable to retrieve specified payment', - 'invoice_not_related_to_payment' => 'Invoice id :invoice is not related to this payment', - 'credit_not_related_to_payment' => 'Credit id :credit is not related to this payment', - 'max_refundable_invoice' => 'Attempting to refund more than allowed for invoice id :invoice, maximum refundable amount is :amount', - 'refund_without_invoices' => 'Attempting to refund a payment with invoices attached, please specify valid invoice/s to be refunded.', - 'refund_without_credits' => 'Attempting to refund a payment with credits attached, please specify valid credits/s to be refunded.', - 'max_refundable_credit' => 'Attempting to refund more than allowed for credit :credit, maximum refundable amount is :amount', - 'project_client_do_not_match' => 'Project client does not match entity client', - 'quote_number_taken' => 'Quote number already taken', - 'recurring_invoice_number_taken' => 'Recurring Invoice number :number already taken', - 'user_not_associated_with_account' => 'User not associated with this account', - 'amounts_do_not_balance' => 'Amounts do not balance correctly.', - 'insufficient_applied_amount_remaining' => 'Insufficient applied amount remaining to cover payment.', - 'insufficient_credit_balance' => 'Insufficient balance on credit.', - 'one_or_more_invoices_paid' => 'One or more of these invoices have been paid', - 'invoice_cannot_be_refunded' => 'Invoice id :number cannot be refunded', - 'attempted_refund_failed' => 'Attempting to refund :amount only :refundable_amount available for refund', - 'user_not_associated_with_this_account' => 'This user is unable to be attached to this company. Perhaps they have already registered a user on another account?', - 'migration_completed' => 'Migration completed', - 'migration_completed_description' => 'Your migration has completed, please review your data after logging in.', - 'api_404' => '404 | Nothing to see here!', - 'large_account_update_parameter' => 'Cannot load a large account without a updated_at parameter', - 'no_backup_exists' => 'No backup exists for this activity', - 'company_user_not_found' => 'Company User record not found', - 'no_credits_found' => 'No credits found.', - 'action_unavailable' => 'The requested action :action is not available.', - 'no_documents_found' => 'No Documents Found', - 'no_group_settings_found' => 'No group settings found', - 'access_denied' => 'Insufficient privileges to access/modify this resource', - 'invoice_cannot_be_marked_paid' => 'Invoice cannot be marked as paid', - 'invoice_license_or_environment' => 'Invalid license, or invalid environment :environment', - 'route_not_available' => 'Route not available', - 'invalid_design_object' => 'Invalid custom design object', - 'quote_not_found' => 'Quote/s not found', - 'quote_unapprovable' => 'Unable to approve this quote as it has expired.', - 'scheduler_has_run' => 'Scheduler has run', - 'scheduler_has_never_run' => 'Scheduler has never run', - 'self_update_not_available' => 'Self update not available on this system.', - 'user_detached' => 'User detached from company', - 'create_webhook_failure' => 'Failed to create Webhook', - 'payment_message_extended' => 'Thank you for your payment of :amount for :invoice', - 'online_payments_minimum_note' => 'Note: Online payments are supported only if amount is bigger than $1 or currency equivalent.', - 'payment_token_not_found' => 'Payment token not found, please try again. If an issue still persist, try with another payment method', - 'vendor_address1' => 'Vendor Street', - 'vendor_address2' => 'Vendor Apt/Suite', - 'partially_unapplied' => 'Partially Unapplied', - 'select_a_gmail_user' => 'Please select a user authenticated with Gmail', - 'list_long_press' => 'List Long Press', - 'show_actions' => 'Show Actions', - 'start_multiselect' => 'Start Multiselect', - 'email_sent_to_confirm_email' => 'An email has been sent to confirm the email address', - 'converted_paid_to_date' => 'Converted Paid to Date', - 'converted_credit_balance' => 'Converted Credit Balance', - 'converted_total' => 'Converted Total', - 'reply_to_name' => 'Reply-To Name', - 'payment_status_-2' => 'Partially Unapplied', - 'color_theme' => 'Color Theme', - 'start_migration' => 'Start Migration', - 'recurring_cancellation_request' => 'Request for recurring invoice cancellation from :contact', - 'recurring_cancellation_request_body' => ':contact from Client :client requested to cancel Recurring Invoice :invoice', - 'hello' => 'Hello', - 'group_documents' => 'Group documents', - 'quote_approval_confirmation_label' => 'Are you sure you want to approve this quote?', - 'migration_select_company_label' => 'Select companies to migrate', - 'force_migration' => 'Force migration', - 'require_password_with_social_login' => 'Require Password with Social Login', - 'stay_logged_in' => 'Stay Logged In', - 'session_about_to_expire' => 'Warning: Your session is about to expire', - 'count_hours' => ':count Hours', - 'count_day' => '1 Day', - 'count_days' => ':count Days', - 'web_session_timeout' => 'Web Session Timeout', - 'security_settings' => 'Security Settings', - 'resend_email' => 'Resend Email', - 'confirm_your_email_address' => 'Please confirm your email address', - 'freshbooks' => 'FreshBooks', - 'invoice2go' => 'Invoice2go', - 'invoicely' => 'Invoicely', - 'waveaccounting' => 'Wave Accounting', - 'zoho' => 'Zoho', - 'accounting' => 'Accounting', - 'required_files_missing' => 'Please provide all CSVs.', - 'migration_auth_label' => 'Let\'s continue by authenticating.', - 'api_secret' => 'API secret', - 'migration_api_secret_notice' => 'You can find API_SECRET in the .env file or Invoice Ninja v5. If property is missing, leave field blank.', - 'billing_coupon_notice' => 'Your discount will be applied on the checkout.', - 'use_last_email' => 'Use last email', - 'activate_company' => 'Activate Company', - 'activate_company_help' => 'Enable emails, recurring invoices and notifications', - 'an_error_occurred_try_again' => 'An error occurred, please try again', - 'please_first_set_a_password' => 'Please first set a password', - 'changing_phone_disables_two_factor' => 'Warning: Changing your phone number will disable 2FA', - 'help_translate' => 'Help Translate', - 'please_select_a_country' => 'Please select a country', - 'disabled_two_factor' => 'Successfully disabled 2FA', - 'connected_google' => 'Successfully connected account', - 'disconnected_google' => 'Successfully disconnected account', - 'delivered' => 'Delivered', - 'spam' => 'Spam', - 'view_docs' => 'View Docs', - 'enter_phone_to_enable_two_factor' => 'Please provide a mobile phone number to enable two factor authentication', - 'send_sms' => 'Send SMS', - 'sms_code' => 'SMS Code', - 'connect_google' => 'Connect Google', - 'disconnect_google' => 'Disconnect Google', - 'disable_two_factor' => 'Disable Two Factor', - 'invoice_task_datelog' => 'Invoice Task Datelog', - 'invoice_task_datelog_help' => 'Add date details to the invoice line items', - 'promo_code' => 'Promo code', - 'recurring_invoice_issued_to' => 'Recurring invoice issued to', - 'subscription' => 'Subscription', - 'new_subscription' => 'New Subscription', - 'deleted_subscription' => 'Successfully deleted subscription', - 'removed_subscription' => 'Successfully removed subscription', - 'restored_subscription' => 'Successfully restored subscription', - 'search_subscription' => 'Search 1 Subscription', - 'search_subscriptions' => 'Search :count Subscriptions', - 'subdomain_is_not_available' => 'Subdomain is not available', - 'connect_gmail' => 'Connect Gmail', - 'disconnect_gmail' => 'Disconnect Gmail', - 'connected_gmail' => 'Successfully connected Gmail', - 'disconnected_gmail' => 'Successfully disconnected Gmail', - 'update_fail_help' => 'Changes to the codebase may be blocking the update, you can run this command to discard the changes:', - 'client_id_number' => 'Client ID Number', - 'count_minutes' => ':count Minutes', - 'password_timeout' => 'Password Timeout', - 'shared_invoice_credit_counter' => 'Share Invoice/Credit Counter', + 'next' => 'Next', + 'next_step' => 'Next step', + 'notification_credit_sent_subject' => 'Credit :invoice was sent to :client', + 'notification_credit_viewed_subject' => 'Credit :invoice was viewed by :client', + 'notification_credit_sent' => 'The following client :client was emailed Credit :invoice for :amount.', + 'notification_credit_viewed' => 'The following client :client viewed Credit :credit for :amount.', + 'reset_password_text' => 'Enter your email to reset your password.', + 'password_reset' => 'Password reset', + 'account_login_text' => 'Welcome! Glad to see you.', + 'request_cancellation' => 'Request cancellation', + 'delete_payment_method' => 'Delete Payment Method', + 'about_to_delete_payment_method' => 'You are about to delete the payment method.', + 'action_cant_be_reversed' => 'Action can\'t be reversed', + 'profile_updated_successfully' => 'The profile has been updated successfully.', + 'currency_ethiopian_birr' => 'Ethiopian Birr', + 'client_information_text' => 'Use a permanent address where you can receive mail.', + 'status_id' => 'Invoice Status', + 'email_already_register' => 'This email is already linked to an account', + 'locations' => 'Locations', + 'freq_indefinitely' => 'Indefinitely', + 'cycles_remaining' => 'Cycles remaining', + 'i_understand_delete' => 'I understand, delete', + 'download_files' => 'Download Files', + 'download_timeframe' => 'Use this link to download your files, the link will expire in 1 hour.', + 'new_signup' => 'New Signup', + 'new_signup_text' => 'A new account has been created by :user - :email - from IP address: :ip', + 'notification_payment_paid_subject' => 'Payment was made by :client', + 'notification_partial_payment_paid_subject' => 'Partial payment was made by :client', + 'notification_payment_paid' => 'A payment of :amount was made by client :client towards :invoice', + 'notification_partial_payment_paid' => 'A partial payment of :amount was made by client :client towards :invoice', + 'notification_bot' => 'Notification Bot', + 'invoice_number_placeholder' => 'Invoice # :invoice', + 'entity_number_placeholder' => ':entity # :entity_number', + 'email_link_not_working' => 'If the button above isn\'t working for you, please click on the link', + 'display_log' => 'Display Log', + 'send_fail_logs_to_our_server' => 'Report errors in realtime', + 'setup' => 'Setup', + 'quick_overview_statistics' => 'Quick overview & statistics', + 'update_your_personal_info' => 'Update your personal information', + 'name_website_logo' => 'Name, website & logo', + 'make_sure_use_full_link' => 'Make sure you use full link to your site', + 'personal_address' => 'Personal address', + 'enter_your_personal_address' => 'Enter your personal address', + 'enter_your_shipping_address' => 'Enter your shipping address', + 'list_of_invoices' => 'List of invoices', + 'with_selected' => 'With selected', + 'invoice_still_unpaid' => 'This invoice is still not paid. Click the button to complete the payment', + 'list_of_recurring_invoices' => 'List of recurring invoices', + 'details_of_recurring_invoice' => 'Here are some details about recurring invoice', + 'cancellation' => 'Cancellation', + 'about_cancellation' => 'In case you want to stop the recurring invoice, please click to request the cancellation.', + 'cancellation_warning' => 'Warning! You are requesting a cancellation of this service. Your service may be cancelled with no further notification to you.', + 'cancellation_pending' => 'Cancellation pending, we\'ll be in touch!', + 'list_of_payments' => 'List of payments', + 'payment_details' => 'Details of the payment', + 'list_of_payment_invoices' => 'List of invoices affected by the payment', + 'list_of_payment_methods' => 'List of payment methods', + 'payment_method_details' => 'Details of payment method', + 'permanently_remove_payment_method' => 'Permanently remove this payment method.', + 'warning_action_cannot_be_reversed' => 'Warning! This action can not be reversed!', + 'confirmation' => 'Confirmation', + 'list_of_quotes' => 'Quotes', + 'waiting_for_approval' => 'Waiting for approval', + 'quote_still_not_approved' => 'This quote is still not approved', + 'list_of_credits' => 'Credits', + 'required_extensions' => 'Required extensions', + 'php_version' => 'PHP version', + 'writable_env_file' => 'Writable .env file', + 'env_not_writable' => '.env file is not writable by the current user.', + 'minumum_php_version' => 'Minimum PHP version', + 'satisfy_requirements' => 'Make sure all requirements are satisfied.', + 'oops_issues' => 'Oops, something does not look right!', + 'open_in_new_tab' => 'Open in new tab', + 'complete_your_payment' => 'Complete payment', + 'authorize_for_future_use' => 'Authorize payment method for future use', + 'page' => 'Page', + 'per_page' => 'Per page', + 'of' => 'Of', + 'view_credit' => 'View Credit', + 'to_view_entity_password' => 'To view the :entity you need to enter password.', + 'showing_x_of' => 'Showing :first to :last out of :total results', + 'no_results' => 'No results found.', + 'payment_failed_subject' => 'Payment failed for Client :client', + 'payment_failed_body' => 'A payment made by client :client failed with message :message', + 'register' => 'Register', + 'register_label' => 'Create your account in seconds', + 'password_confirmation' => 'Confirm your password', + 'verification' => 'Verification', + 'complete_your_bank_account_verification' => 'Before using a bank account it must be verified.', + 'checkout_com' => 'Checkout.com', + 'footer_label' => 'Copyright © :year :company.', + 'credit_card_invalid' => 'Provided credit card number is not valid.', + 'month_invalid' => 'Provided month is not valid.', + 'year_invalid' => 'Provided year is not valid.', + 'https_required' => 'HTTPS is required, form will fail', + 'if_you_need_help' => 'If you need help you can post to our', + 'update_password_on_confirm' => 'After updating password, your account will be confirmed.', + 'bank_account_not_linked' => 'To pay with a bank account, first you have to add it as payment method.', + 'application_settings_label' => 'Let\'s store basic information about your Invoice Ninja!', + 'recommended_in_production' => 'Highly recommended in production', + 'enable_only_for_development' => 'Enable only for development', + 'test_pdf' => 'Test PDF', + 'checkout_authorize_label' => 'Checkout.com can be can saved as payment method for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.', + 'sofort_authorize_label' => 'Bank account (SOFORT) can be can saved as payment method for future use, once you complete your first transaction. Don\'t forget to check "Store payment details" during payment process.', + 'node_status' => 'Node status', + 'npm_status' => 'NPM status', + 'node_status_not_found' => 'I could not find Node anywhere. Is it installed?', + 'npm_status_not_found' => 'I could not find NPM anywhere. Is it installed?', + 'locked_invoice' => 'This invoice is locked and unable to be modified', + 'downloads' => 'Downloads', + 'resource' => 'Resource', + 'document_details' => 'Details about the document', + 'hash' => 'Hash', + 'resources' => 'Resources', + 'allowed_file_types' => 'Allowed file types:', + 'common_codes' => 'Common codes and their meanings', + 'payment_error_code_20087' => '20087: Bad Track Data (invalid CVV and/or expiry date)', + 'download_selected' => 'Download selected', + 'to_pay_invoices' => 'To pay invoices, you have to', + 'add_payment_method_first' => 'add payment method', + 'no_items_selected' => 'No items selected.', + 'payment_due' => 'Payment due', + 'account_balance' => 'Account Balance', + 'thanks' => 'Thanks', + 'minimum_required_payment' => 'Minimum required payment is :amount', + 'under_payments_disabled' => 'Company doesn\'t support underpayments.', + 'over_payments_disabled' => 'Company doesn\'t support overpayments.', + 'saved_at' => 'Saved at :time', + 'credit_payment' => 'Credit applied to Invoice :invoice_number', + 'credit_subject' => 'New credit :number from :account', + 'credit_message' => 'To view your credit for :amount, click the link below.', + 'payment_type_Crypto' => 'Cryptocurrency', + 'payment_type_Credit' => 'Credit', + 'store_for_future_use' => 'Store for future use', + 'pay_with_credit' => 'Pay with credit', + 'payment_method_saving_failed' => 'Payment method can\'t be saved for future use.', + 'pay_with' => 'Pay with', + 'n/a' => 'N/A', + 'by_clicking_next_you_accept_terms' => 'By clicking "Next step" you accept terms.', + 'not_specified' => 'Not specified', + 'before_proceeding_with_payment_warning' => 'Before proceeding with payment, you have to fill following fields', + 'after_completing_go_back_to_previous_page' => 'After completing, go back to previous page.', + 'pay' => 'Pay', + 'instructions' => 'Instructions', + 'notification_invoice_reminder1_sent_subject' => 'Reminder 1 for Invoice :invoice was sent to :client', + 'notification_invoice_reminder2_sent_subject' => 'Reminder 2 for Invoice :invoice was sent to :client', + 'notification_invoice_reminder3_sent_subject' => 'Reminder 3 for Invoice :invoice was sent to :client', + 'notification_invoice_custom_sent_subject' => 'Custom reminder for Invoice :invoice was sent to :client', + 'notification_invoice_reminder_endless_sent_subject' => 'Endless reminder for Invoice :invoice was sent to :client', + 'assigned_user' => 'Assigned User', + 'setup_steps_notice' => 'To proceed to next step, make sure you test each section.', + 'setup_phantomjs_note' => 'Note about Phantom JS. Read more.', + 'minimum_payment' => 'Minimum Payment', + 'no_action_provided' => 'No action provided. If you believe this is wrong, please contact the support.', + 'no_payable_invoices_selected' => 'No payable invoices selected. Make sure you are not trying to pay draft invoice or invoice with zero balance due.', + 'required_payment_information' => 'Required payment details', + 'required_payment_information_more' => 'To complete a payment we need more details about you.', + 'required_client_info_save_label' => 'We will save this, so you don\'t have to enter it next time.', + 'notification_credit_bounced' => 'We were unable to deliver Credit :invoice to :contact. \n :error', + 'notification_credit_bounced_subject' => 'Unable to deliver Credit :invoice', + 'save_payment_method_details' => 'Save payment method details', + 'new_card' => 'New card', + 'new_bank_account' => 'New bank account', + 'company_limit_reached' => 'Limit of :limit companies per account.', + 'credits_applied_validation' => 'Total credits applied cannot be MORE than total of invoices', + 'credit_number_taken' => 'Credit number already taken', + 'credit_not_found' => 'Credit not found', + 'invoices_dont_match_client' => 'Selected invoices are not from a single client', + 'duplicate_credits_submitted' => 'Duplicate credits submitted.', + 'duplicate_invoices_submitted' => 'Duplicate invoices submitted.', + 'credit_with_no_invoice' => 'You must have an invoice set when using a credit in a payment', + 'client_id_required' => 'Client id is required', + 'expense_number_taken' => 'Expense number already taken', + 'invoice_number_taken' => 'Invoice number already taken', + 'payment_id_required' => 'Payment `id` required.', + 'unable_to_retrieve_payment' => 'Unable to retrieve specified payment', + 'invoice_not_related_to_payment' => 'Invoice id :invoice is not related to this payment', + 'credit_not_related_to_payment' => 'Credit id :credit is not related to this payment', + 'max_refundable_invoice' => 'Attempting to refund more than allowed for invoice id :invoice, maximum refundable amount is :amount', + 'refund_without_invoices' => 'Attempting to refund a payment with invoices attached, please specify valid invoice/s to be refunded.', + 'refund_without_credits' => 'Attempting to refund a payment with credits attached, please specify valid credits/s to be refunded.', + 'max_refundable_credit' => 'Attempting to refund more than allowed for credit :credit, maximum refundable amount is :amount', + 'project_client_do_not_match' => 'Project client does not match entity client', + 'quote_number_taken' => 'Quote number already taken', + 'recurring_invoice_number_taken' => 'Recurring Invoice number :number already taken', + 'user_not_associated_with_account' => 'User not associated with this account', + 'amounts_do_not_balance' => 'Amounts do not balance correctly.', + 'insufficient_applied_amount_remaining' => 'Insufficient applied amount remaining to cover payment.', + 'insufficient_credit_balance' => 'Insufficient balance on credit.', + 'one_or_more_invoices_paid' => 'One or more of these invoices have been paid', + 'invoice_cannot_be_refunded' => 'Invoice id :number cannot be refunded', + 'attempted_refund_failed' => 'Attempting to refund :amount only :refundable_amount available for refund', + 'user_not_associated_with_this_account' => 'This user is unable to be attached to this company. Perhaps they have already registered a user on another account?', + 'migration_completed' => 'Migration completed', + 'migration_completed_description' => 'Your migration has completed, please review your data after logging in.', + 'api_404' => '404 | Nothing to see here!', + 'large_account_update_parameter' => 'Cannot load a large account without a updated_at parameter', + 'no_backup_exists' => 'No backup exists for this activity', + 'company_user_not_found' => 'Company User record not found', + 'no_credits_found' => 'No credits found.', + 'action_unavailable' => 'The requested action :action is not available.', + 'no_documents_found' => 'No Documents Found', + 'no_group_settings_found' => 'No group settings found', + 'access_denied' => 'Insufficient privileges to access/modify this resource', + 'invoice_cannot_be_marked_paid' => 'Invoice cannot be marked as paid', + 'invoice_license_or_environment' => 'Invalid license, or invalid environment :environment', + 'route_not_available' => 'Route not available', + 'invalid_design_object' => 'Invalid custom design object', + 'quote_not_found' => 'Quote/s not found', + 'quote_unapprovable' => 'Unable to approve this quote as it has expired.', + 'scheduler_has_run' => 'Scheduler has run', + 'scheduler_has_never_run' => 'Scheduler has never run', + 'self_update_not_available' => 'Self update not available on this system.', + 'user_detached' => 'User detached from company', + 'create_webhook_failure' => 'Failed to create Webhook', + 'payment_message_extended' => 'Thank you for your payment of :amount for :invoice', + 'online_payments_minimum_note' => 'Note: Online payments are supported only if amount is bigger than $1 or currency equivalent.', + 'payment_token_not_found' => 'Payment token not found, please try again. If an issue still persist, try with another payment method', + 'vendor_address1' => 'Vendor Street', + 'vendor_address2' => 'Vendor Apt/Suite', + 'partially_unapplied' => 'Partially Unapplied', + 'select_a_gmail_user' => 'Please select a user authenticated with Gmail', + 'list_long_press' => 'List Long Press', + 'show_actions' => 'Show Actions', + 'start_multiselect' => 'Start Multiselect', + 'email_sent_to_confirm_email' => 'An email has been sent to confirm the email address', + 'converted_paid_to_date' => 'Converted Paid to Date', + 'converted_credit_balance' => 'Converted Credit Balance', + 'converted_total' => 'Converted Total', + 'reply_to_name' => 'Reply-To Name', + 'payment_status_-2' => 'Partially Unapplied', + 'color_theme' => 'Color Theme', + 'start_migration' => 'Start Migration', + 'recurring_cancellation_request' => 'Request for recurring invoice cancellation from :contact', + 'recurring_cancellation_request_body' => ':contact from Client :client requested to cancel Recurring Invoice :invoice', + 'hello' => 'Hello', + 'group_documents' => 'Group documents', + 'quote_approval_confirmation_label' => 'Are you sure you want to approve this quote?', + 'migration_select_company_label' => 'Select companies to migrate', + 'force_migration' => 'Force migration', + 'require_password_with_social_login' => 'Require Password with Social Login', + 'stay_logged_in' => 'Stay Logged In', + 'session_about_to_expire' => 'Warning: Your session is about to expire', + 'count_hours' => ':count Hours', + 'count_day' => '1 Day', + 'count_days' => ':count Days', + 'web_session_timeout' => 'Web Session Timeout', + 'security_settings' => 'Security Settings', + 'resend_email' => 'Resend Email', + 'confirm_your_email_address' => 'Please confirm your email address', + 'freshbooks' => 'FreshBooks', + 'invoice2go' => 'Invoice2go', + 'invoicely' => 'Invoicely', + 'waveaccounting' => 'Wave Accounting', + 'zoho' => 'Zoho', + 'accounting' => 'Accounting', + 'required_files_missing' => 'Please provide all CSVs.', + 'migration_auth_label' => 'Let\'s continue by authenticating.', + 'api_secret' => 'API secret', + 'migration_api_secret_notice' => 'You can find API_SECRET in the .env file or Invoice Ninja v5. If property is missing, leave field blank.', + 'billing_coupon_notice' => 'Your discount will be applied on the checkout.', + 'use_last_email' => 'Use last email', + 'activate_company' => 'Activate Company', + 'activate_company_help' => 'Enable emails, recurring invoices and notifications', + 'an_error_occurred_try_again' => 'An error occurred, please try again', + 'please_first_set_a_password' => 'Please first set a password', + 'changing_phone_disables_two_factor' => 'Warning: Changing your phone number will disable 2FA', + 'help_translate' => 'Help Translate', + 'please_select_a_country' => 'Please select a country', + 'disabled_two_factor' => 'Successfully disabled 2FA', + 'connected_google' => 'Successfully connected account', + 'disconnected_google' => 'Successfully disconnected account', + 'delivered' => 'Delivered', + 'spam' => 'Spam', + 'view_docs' => 'View Docs', + 'enter_phone_to_enable_two_factor' => 'Please provide a mobile phone number to enable two factor authentication', + 'send_sms' => 'Send SMS', + 'sms_code' => 'SMS Code', + 'connect_google' => 'Connect Google', + 'disconnect_google' => 'Disconnect Google', + 'disable_two_factor' => 'Disable Two Factor', + 'invoice_task_datelog' => 'Invoice Task Datelog', + 'invoice_task_datelog_help' => 'Add date details to the invoice line items', + 'promo_code' => 'Promo code', + 'recurring_invoice_issued_to' => 'Recurring invoice issued to', + 'subscription' => 'Subscription', + 'new_subscription' => 'New Subscription', + 'deleted_subscription' => 'Successfully deleted subscription', + 'removed_subscription' => 'Successfully removed subscription', + 'restored_subscription' => 'Successfully restored subscription', + 'search_subscription' => 'Search 1 Subscription', + 'search_subscriptions' => 'Search :count Subscriptions', + 'subdomain_is_not_available' => 'Subdomain is not available', + 'connect_gmail' => 'Connect Gmail', + 'disconnect_gmail' => 'Disconnect Gmail', + 'connected_gmail' => 'Successfully connected Gmail', + 'disconnected_gmail' => 'Successfully disconnected Gmail', + 'update_fail_help' => 'Changes to the codebase may be blocking the update, you can run this command to discard the changes:', + 'client_id_number' => 'Client ID Number', + 'count_minutes' => ':count Minutes', + 'password_timeout' => 'Password Timeout', + 'shared_invoice_credit_counter' => 'Share Invoice/Credit Counter', 'activity_80' => ':user created subscription :subscription', 'activity_81' => ':user updated subscription :subscription', 'activity_82' => ':user archived subscription :subscription', @@ -4891,6 +4893,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/components/schemas/company_settings.yaml b/openapi/components/schemas/company_settings.yaml index 15dc5bf8cd..2ee34f675f 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