diff --git a/app/Console/Commands/CreateTestData.php b/app/Console/Commands/CreateTestData.php index 40d74f98e2b9..8124381d36d4 100644 --- a/app/Console/Commands/CreateTestData.php +++ b/app/Console/Commands/CreateTestData.php @@ -11,6 +11,7 @@ use App\Factory\InvoiceItemFactory; use App\Factory\PaymentFactory; use App\Helpers\Invoice\InvoiceSum; use App\Jobs\Company\UpdateCompanyLedgerWithInvoice; +use App\Jobs\Invoice\CreateInvoiceInvitations; use App\Jobs\Invoice\UpdateInvoicePayment; use App\Listeners\Invoice\CreateInvoiceInvitation; use App\Models\CompanyToken; @@ -316,8 +317,8 @@ class CreateTestData extends Command $this->invoice_repo->markSent($invoice); - event(new InvoiceWasMarkedSent($invoice)); - + CreateInvoiceInvitations::dispatch($invoice); + if(rand(0, 1)) { $payment = PaymentFactory::create($client->company->id, $client->user->id); diff --git a/app/Console/Commands/SendTestEmails.php b/app/Console/Commands/SendTestEmails.php index d6ccaf04b329..f46f14e62d85 100644 --- a/app/Console/Commands/SendTestEmails.php +++ b/app/Console/Commands/SendTestEmails.php @@ -2,7 +2,10 @@ namespace App\Console\Commands; +use App\Factory\ClientFactory; use App\Mail\TemplateEmail; +use App\Models\Client; +use App\Models\ClientContact; use App\Models\Invoice; use App\Models\User; use Illuminate\Console\Command; @@ -43,7 +46,9 @@ class SendTestEmails extends Command public function handle() { $this->sendTemplateEmails('plain'); + sleep(5); $this->sendTemplateEmails('light'); + sleep(5); $this->sendTemplateEmails('dark'); } @@ -57,21 +62,48 @@ class SendTestEmails extends Command ]; $user = User::whereEmail('user@example.com')->first(); + $client = Client::all()->first(); if(!$user){ $user = factory(\App\Models\User::class)->create([ 'confirmation_code' => '123', + 'email' => 'admin@business.com', + 'first_name' => 'John', + 'last_name' => 'Doe', + ]); + } + + if(!$client) { + + $client = ClientFactory::create($user->company()->id, $user->id); + $client->save(); + + factory(\App\Models\ClientContact::class,1)->create([ + 'user_id' => $user->id, + 'client_id' => $client->id, + 'company_id' => $company->id, + 'is_primary' => 1, + 'send_invoice' => true, + 'email' => 'exy@example.com', + ]); + + factory(\App\Models\ClientContact::class,1)->create([ + 'user_id' => $user->id, + 'client_id' => $client->id, + 'company_id' => $company->id, + 'send_invoice' => true, + 'email' => 'exy2@example.com', ]); } $cc_emails = [config('ninja.testvars.test_email')]; $bcc_emails = [config('ninja.testvars.test_email')]; - Mail::to(config('ninja.testvars.test_email')) + Mail::to(config('ninja.testvars.test_email'),'Mr Test') ->cc($cc_emails) ->bcc($bcc_emails) //->replyTo(also_available_if_needed) - ->send(new TemplateEmail($message, $template, $user)); + ->send(new TemplateEmail($message, $template, $user, $client)); } } diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index b2b4937286ec..7972918cf86c 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -69,6 +69,7 @@ class CompanySettings extends BaseSettings public $translations; public $counter_number_applied = 'when_saved'; // when_saved , when_sent , when_paid + public $quote_number_applied = 'when_saved'; // when_saved , when_sent /* Counters */ public $invoice_number_pattern = ''; public $invoice_number_counter = 1; @@ -222,6 +223,7 @@ class CompanySettings extends BaseSettings 'gmail_sending_user_id' => 'string', 'currency_id' => 'string', 'counter_number_applied' => 'string', + 'quote_number_applied' => 'string', 'email_subject_custom1' => 'string', 'email_subject_custom2' => 'string', 'email_subject_custom3' => 'string', diff --git a/app/Events/Invoice/InvoiceWasEmailed.php b/app/Events/Invoice/InvoiceWasEmailed.php index 459e57e4cfe0..86c1ab7610c4 100644 --- a/app/Events/Invoice/InvoiceWasEmailed.php +++ b/app/Events/Invoice/InvoiceWasEmailed.php @@ -26,19 +26,13 @@ class InvoiceWasEmailed */ public $invoice; - /** - * @var string - */ - public $notes; - /** * Create a new event instance. * * @param Invoice $invoice */ - public function __construct(Invoice $invoice, $notes) + public function __construct(Invoice $invoice) { $this->invoice = $invoice; - $this->notes = $notes; } } diff --git a/app/Events/Invoice/InvoiceWasEmailedAndFailed.php b/app/Events/Invoice/InvoiceWasEmailedAndFailed.php new file mode 100644 index 000000000000..f8a814ff1bcf --- /dev/null +++ b/app/Events/Invoice/InvoiceWasEmailedAndFailed.php @@ -0,0 +1,45 @@ +invoice = $invoice; + + $this->errors = $errors; + } +} diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 8fbd0c9fd5c4..35eae3b4866a 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -25,6 +25,7 @@ use App\Http\Requests\Invoice\ShowInvoiceRequest; use App\Http\Requests\Invoice\StoreInvoiceRequest; use App\Http\Requests\Invoice\UpdateInvoiceRequest; use App\Jobs\Entity\ActionEntity; +use App\Jobs\Invoice\EmailInvoice; use App\Jobs\Invoice\MarkInvoicePaid; use App\Jobs\Invoice\StoreInvoice; use App\Models\Invoice; @@ -660,7 +661,7 @@ class InvoiceController extends BaseController return $this->listResponse($invoice); break; case 'email': - + EmailInvoice::dispatch($invoice); if(!$bulk) return response()->json(['message'=>'email sent'],200); break; diff --git a/app/Http/Controllers/OpenAPI/CompanySettingsSchema.php b/app/Http/Controllers/OpenAPI/CompanySettingsSchema.php index 199d456998e8..dedfa1b499c9 100644 --- a/app/Http/Controllers/OpenAPI/CompanySettingsSchema.php +++ b/app/Http/Controllers/OpenAPI/CompanySettingsSchema.php @@ -21,10 +21,11 @@ * @OA\Property(property="email_style", type="string", example="light", description="options include plain,light,dark,custom"), * @OA\Property(property="reply_to_email", type="string", example="email@gmail.com", description="The reply to email address"), * @OA\Property(property="bcc_email", type="string", example="email@gmail.com, contact@gmail.com", description="A comma separate list of BCC emails"), - * * @OA\Property(property="pdf_email_attachment", type="boolean", example=true, description="Toggles whether to attach PDF as attachment"), * @OA\Property(property="ubl_email_attachment", type="boolean", example=true, description="Toggles whether to attach UBL as attachment"), * @OA\Property(property="email_style_custom", type="string", example="", description="The custom template"), + * @OA\Property(property="counter_number_applied", type="string", example="when_sent", description="enum when the invoice number counter is set, ie when_saved, when_sent, when_paid"), + * @OA\Property(property="quote_number_applied", type="string", example="when_sent", description="enum when the quote number counter is set, ie when_saved, when_sent"), * @OA\Property(property="custom_message_dashboard", type="string", example="Please pay invoices immediately", description="____________"), * @OA\Property(property="custom_message_unpaid_invoice", type="string", example="Please pay invoices immediately", description="____________"), * @OA\Property(property="custom_message_paid_invoice", type="string", example="Thanks for paying this invoice!", description="____________"), @@ -44,7 +45,6 @@ * @OA\Property(property="vendor_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="ticket_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the ticket number pattern"), * @OA\Property(property="ticket_number_counter", type="integer", example="1", description="____________"), - * * @OA\Property(property="payment_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the payment number pattern"), * @OA\Property(property="payment_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="invoice_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the invoice number pattern"), diff --git a/app/Http/Controllers/OpenAPI/InvoiceSchema.php b/app/Http/Controllers/OpenAPI/InvoiceSchema.php index 0ec8d8f0f185..d5acc258330e 100644 --- a/app/Http/Controllers/OpenAPI/InvoiceSchema.php +++ b/app/Http/Controllers/OpenAPI/InvoiceSchema.php @@ -5,10 +5,10 @@ * type="object", * @OA\Property(property="id", type="string", example="Opnel5aKBz", description="_________"), * @OA\Property(property="user_id", type="string", example="", description="__________"), + * @OA\Property(property="assigned_user_id", type="string", example="", description="__________"), * @OA\Property(property="company_id", type="string", example="", description="________"), * @OA\Property(property="client_id", type="string", example="", description="________"), * @OA\Property(property="status_id", type="string", example="", description="________"), - * @OA\Property(property="invoice_type_id", type="string", example="", description="________"), * @OA\Property(property="number", type="string", example="INV_101", description="The invoice number - is a unique alpha numeric number per invoice per company"), * @OA\Property(property="po_number", type="string", example="", description="________"), * @OA\Property(property="terms", type="string", example="", description="________"), @@ -35,6 +35,7 @@ * @OA\Property(property="is_deleted", type="boolean", example=true, description="_________"), * @OA\Property(property="uses_inclusive_taxes", type="boolean", example=true, description="Defines the type of taxes used as either inclusive or exclusive"), * @OA\Property(property="date", type="string", format="date", example="1994-07-30", description="The Invoice Date"), + * @OA\Property(property="last_sent_date", type="string", format="date", example="1994-07-30", description="The last date the invoice was sent out"), * @OA\Property(property="next_send_date", type="string", format="date", example="1994-07-30", description="The Next date for a reminder to be sent"), * @OA\Property(property="partial_due_date", type="string", format="date", example="1994-07-30", description="_________"), * @OA\Property(property="due_date", type="string", format="date", example="1994-07-30", description="_________"), diff --git a/app/Http/Controllers/OpenAPI/QuoteSchema.php b/app/Http/Controllers/OpenAPI/QuoteSchema.php index 066d7370f9f8..ef78305a8bab 100644 --- a/app/Http/Controllers/OpenAPI/QuoteSchema.php +++ b/app/Http/Controllers/OpenAPI/QuoteSchema.php @@ -3,8 +3,50 @@ * @OA\Schema( * schema="Quote", * type="object", - * @OA\Property(property="id", type="string", example="Opnel5aKBz", description="______"), + * @OA\Property(property="id", type="string", example="Opnel5aKBz", description="_________"), + * @OA\Property(property="user_id", type="string", example="", description="__________"), + * @OA\Property(property="assigned_user_id", type="string", example="", description="__________"), + * @OA\Property(property="company_id", type="string", example="", description="________"), + * @OA\Property(property="client_id", type="string", example="", description="________"), + * @OA\Property(property="status_id", type="string", example="", description="________"), + * @OA\Property(property="number", type="string", example="QUOTE_101", description="The quote number - is a unique alpha numeric number per quote per company"), + * @OA\Property(property="po_number", type="string", example="", description="________"), + * @OA\Property(property="terms", type="string", example="", description="________"), + * @OA\Property(property="public_notes", type="string", example="", description="________"), + * @OA\Property(property="private_notes", type="string", example="", description="________"), + * @OA\Property(property="footer", type="string", example="", description="________"), + * @OA\Property(property="custom_value1", type="string", example="", description="________"), + * @OA\Property(property="custom_value2", type="string", example="", description="________"), + * @OA\Property(property="custom_value3", type="string", example="", description="________"), + * @OA\Property(property="custom_value4", type="string", example="", description="________"), + * @OA\Property(property="tax_name1", type="string", example="", description="________"), + * @OA\Property(property="tax_name2", type="string", example="", description="________"), + * @OA\Property(property="tax_rate1", type="number", format="float", example="10.00", description="_________"), + * @OA\Property(property="tax_rate2", type="number", format="float", example="10.00", description="_________"), + * @OA\Property(property="tax_name3", type="string", example="", description="________"), + * @OA\Property(property="tax_rate3", type="number", format="float", example="10.00", description="_________"), * @OA\Property(property="total_taxes", type="number", format="float", example="10.00", description="The total taxes for the quote"), + * @OA\Property(property="line_items", type="object", example="", description="_________"), + * @OA\Property(property="amount", type="number", format="float", example="10.00", description="_________"), + * @OA\Property(property="balance", type="number", format="float", example="10.00", description="_________"), + * @OA\Property(property="discount", type="number", format="float", example="10.00", description="_________"), + * @OA\Property(property="partial", type="number", format="float", example="10.00", description="_________"), + * @OA\Property(property="is_amount_discount", type="boolean", example=true, description="_________"), + * @OA\Property(property="is_deleted", type="boolean", example=true, description="_________"), + * @OA\Property(property="uses_inclusive_taxes", type="boolean", example=true, description="Defines the type of taxes used as either inclusive or exclusive"), + * @OA\Property(property="date", type="string", format="date", example="1994-07-30", description="The Quote Date"), + * @OA\Property(property="last_sent_date", type="string", format="date", example="1994-07-30", description="The last date the quote was sent out"), * @OA\Property(property="next_send_date", type="string", format="date", example="1994-07-30", description="The Next date for a reminder to be sent"), + * @OA\Property(property="partial_due_date", type="string", format="date", example="1994-07-30", description="_________"), + * @OA\Property(property="due_date", type="string", format="date", example="1994-07-30", description="_________"), + * @OA\Property(property="settings",ref="#/components/schemas/CompanySettings"), + * @OA\Property(property="last_viewed", type="number", format="integer", example="1434342123", description="Timestamp"), + * @OA\Property(property="updated_at", type="number", format="integer", example="1434342123", description="Timestamp"), + * @OA\Property(property="archived_at", type="number", format="integer", example="1434342123", description="Timestamp"), + * @OA\Property(property="custom_surcharge1", type="number", format="float", example="10.00", description="First Custom Surcharge"), + * @OA\Property(property="custom_surcharge2", type="number", format="float", example="10.00", description="Second Custom Surcharge"), + * @OA\Property(property="custom_surcharge3", type="number", format="float", example="10.00", description="Third Custom Surcharge"), + * @OA\Property(property="custom_surcharge4", type="number", format="float", example="10.00", description="Fourth Custom Surcharge"), + * @OA\Property(property="custom_surcharge_taxes", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"), * ) */ \ No newline at end of file diff --git a/app/Jobs/Invoice/EmailInvoice.php b/app/Jobs/Invoice/EmailInvoice.php new file mode 100644 index 000000000000..b7d519313146 --- /dev/null +++ b/app/Jobs/Invoice/EmailInvoice.php @@ -0,0 +1,111 @@ +invoice = $invoice; + + } + + /** + * Execute the job. + * + * + * @return void + */ + public function handle() + { + /*Jobs are not multi-db aware, need to set! */ + MultiDB::setDB($this->invoice->company->db); + + $message_array = $this->invoice->getEmailData(); + $message_array['title'] = &$message_array['subject']; + $message_array['footer'] = 'The Footer'; + // + + $variables = array_merge($this->invoice->makeLabels(), $this->invoice->makeValues()); + + $template_style = $this->invoice->client->getSetting('email_style'); + + $this->invoice->invitations->each(function ($invitation) use($message_array, $template_style, $variables){ + + if($invitation->contact->send_invoice && $invitation->contact->email) + { + //there may be template variables left over for the specific contact? need to reparse here //todo this wont work, as if the variables existed, they'll be overwritten already! + $message_array['body'] = str_replace(array_keys($variables), array_values($variables), $message_array['body']); + $message_array['subject'] = str_replace(array_keys($variables), array_values($variables), $message_array['subject']); + + //change the runtime config of the mail provider here: + + //send message + Mail::to($invitation->contact->email) + ->send(new TemplateEmail($message_array, $template_style, $invitation->contact->user, $invitation->contact->client)); + + if( count(Mail::failures()) > 0 ) { + + event(new InvoiceWasEmailedAndFailed($this->invoice, Mail::failures())); + + return $this->logMailError($errors); + + } + + //fire any events + event(new InvoiceWasEmailed($this->invoice)); + + sleep(5); + + } + + }); + + } + + private function logMailError($errors) + { + + SystemLogger::dispatch( + $errors, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_SEND, + SystemLog::TYPE_FAILURE, + $this->invoice->client + ); + + } + +} \ No newline at end of file diff --git a/app/Jobs/Invoice/UpdateInvoicePayment.php b/app/Jobs/Invoice/UpdateInvoicePayment.php index 6f5fa686d4a3..de64bc658d7e 100644 --- a/app/Jobs/Invoice/UpdateInvoicePayment.php +++ b/app/Jobs/Invoice/UpdateInvoicePayment.php @@ -137,7 +137,8 @@ class UpdateInvoicePayment implements ShouldQueue 'invoices' => $invoices, 'invoices_total' => $invoices_total, 'payment_amount' => $this->payment->amount, - 'partial_check_amount' => $total, ], + 'partial_check_amount' => $total, + ], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_PAYMENT_RECONCILIATION_FAILURE, SystemLog::TYPE_LEDGER, diff --git a/app/Jobs/Quote/ApplyQuoteNumber.php b/app/Jobs/Quote/ApplyQuoteNumber.php new file mode 100644 index 000000000000..cdd8e9ac3996 --- /dev/null +++ b/app/Jobs/Quote/ApplyQuoteNumber.php @@ -0,0 +1,81 @@ +quote = $quote; + + $this->settings = $settings; + } + + /** + * Execute the job. + * + * + * @return void + */ + public function handle() + { + //return early + if($this->quote->number != '') + return $this->quote; + + switch ($this->settings->quote_number_applied) { + case 'when_saved': + $this->quote->number = $this->getNextQuoteNumber($this->quote->client); + break; + case 'when_sent': + if($this->quote->status_id == Quote::STATUS_SENT) + $this->quote->number = $this->getNextQuoteNumber($this->quote->client); + break; + + default: + # code... + break; + } + + $this->quote->save(); + + return $this->quote; + + } + + +} diff --git a/app/Listeners/Invoice/InvoiceEmailActivity.php b/app/Listeners/Invoice/InvoiceEmailActivity.php new file mode 100644 index 000000000000..b382adcfd9f8 --- /dev/null +++ b/app/Listeners/Invoice/InvoiceEmailActivity.php @@ -0,0 +1,54 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + + $fields = new \stdClass; + + $fields->invoice_id = $event->invoice->id; + $fields->user_id = $event->invoice->user_id; + $fields->company_id = $event->invoice->company_id; + $fields->activity_type_id = Activity::EMAIL_INVOICE; + + $this->activity_repo->save($fields, $event->invoice); + } +} diff --git a/app/Listeners/Invoice/InvoiceEmailFailedActivity.php b/app/Listeners/Invoice/InvoiceEmailFailedActivity.php new file mode 100644 index 000000000000..0f653e5c43fe --- /dev/null +++ b/app/Listeners/Invoice/InvoiceEmailFailedActivity.php @@ -0,0 +1,54 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + + $fields = new \stdClass; + + $fields->invoice_id = $event->invoice->id; + $fields->user_id = $event->invoice->user_id; + $fields->company_id = $event->invoice->company_id; + $fields->activity_type_id = Activity::EMAIL_INVOICE_FAILED; + + $this->activity_repo->save($fields, $event->invoice); + } +} diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index 99fda55470f5..c8a4ced4a1c8 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -14,7 +14,7 @@ class TemplateEmail extends Mailable private $template; //the template to use - private $message; //the message array (subject and body) + private $message; //the message array // ['body', 'footer', 'title', 'files'] private $user; //the user the email will be sent from @@ -45,7 +45,7 @@ class TemplateEmail extends Mailable $company = $this->client->company; - return $this->from($this->user->email, $this->user->present()->name()) //todo this needs to be fixed to handle the hosted version + $message = $this->from($this->user->email, $this->user->present()->name()) //todo this needs to be fixed to handle the hosted version ->subject($this->message['subject']) ->text('email.template.plain', ['body' => $this->message['body'], 'footer' => $this->message['footer']]) ->view($template_name, [ @@ -56,5 +56,14 @@ class TemplateEmail extends Mailable 'company' => $company ]); + + //conditionally attach files + if($settings->pdf_email_attachment !== false && array_key_exists($this->message['files'])){ + + foreach($this->message['files'] as $file) + $message->attach($file); + } + + return $message; } } \ No newline at end of file diff --git a/app/Models/Activity.php b/app/Models/Activity.php index d18cd4ca8c80..fe9f62df56cd 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -69,6 +69,7 @@ class Activity extends StaticModel const RESTORE_USER=52; const MARK_SENT_INVOICE=53; const PAID_INVOICE=54; + const EMAIL_INVOICE_FAILED=57; protected $casts = [ 'is_system' => 'boolean', diff --git a/app/Models/Client.php b/app/Models/Client.php index 879059d7060f..f79bdb54e862 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -254,7 +254,7 @@ class Client extends BaseModel if($this->group_settings && (property_exists($this->group_settings->settings, $setting) !== false) && (isset($this->group_settings->settings->{$setting}) !== false)){ return $this->group_settings->settings->{$setting}; } - + /*Company Settings*/ if((property_exists($this->company->settings, $setting) != false ) && (isset($this->company->settings->{$setting}) !== false) ){ return $this->company->settings->{$setting}; diff --git a/app/Models/SystemLog.php b/app/Models/SystemLog.php index 89b5b7240a07..1b7d6ca0a140 100644 --- a/app/Models/SystemLog.php +++ b/app/Models/SystemLog.php @@ -17,6 +17,7 @@ class SystemLog extends Model { /* Category IDs */ const CATEGORY_GATEWAY_RESPONSE = 1; + const CATEGORY_MAIL = 2; /* Event IDs*/ const EVENT_PAYMENT_RECONCILIATION_FAILURE = 10; @@ -26,10 +27,13 @@ class SystemLog extends Model const EVENT_GATEWAY_FAILURE = 22; const EVENT_GATEWAY_ERROR = 23; + const EVENT_MAIL_SEND = 30; + /*Type IDs*/ const TYPE_PAYPAL = 300; const TYPE_STRIPE = 301; const TYPE_LEDGER = 302; + const TYPE_FAILURE = 303; protected $fillable = [ 'client_id', diff --git a/app/Models/User.php b/app/Models/User.php index 681075fb5a2f..25a8219e3a3a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -199,6 +199,11 @@ class User extends Authenticatable implements MustVerifyEmail } + public function clients() + { + return $this->hasMany(Client::class); + } + /** * Returns a comma separated list of user permissions * diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 0e7f4e0ebb4e..de7bc12fb648 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -14,6 +14,7 @@ namespace App\Providers; use App\Events\Client\ClientWasCreated; use App\Events\Contact\ContactLoggedIn; use App\Events\Invoice\InvoiceWasCreated; +use App\Events\Invoice\InvoiceWasEmailed; use App\Events\Invoice\InvoiceWasMarkedSent; use App\Events\Invoice\InvoiceWasPaid; use App\Events\Invoice\InvoiceWasUpdated; @@ -29,6 +30,8 @@ use App\Listeners\Invoice\CreateInvoiceActivity; use App\Listeners\Invoice\CreateInvoiceHtmlBackup; use App\Listeners\Invoice\CreateInvoiceInvitation; use App\Listeners\Invoice\CreateInvoicePdf; +use App\Listeners\Invoice\InvoiceEmailActivity; +use App\Listeners\Invoice\InvoiceEmailFailedActivity; use App\Listeners\Invoice\UpdateInvoiceActivity; use App\Listeners\Invoice\UpdateInvoiceInvitations; use App\Listeners\Invoice\UpdateInvoicePayment; @@ -96,7 +99,14 @@ class EventServiceProvider extends ServiceProvider ], InvoiceWasPaid::class => [ CreateInvoiceHtmlBackup::class, - ] + ], + InvoiceWasEmailed::class => [ + InvoiceEmailActivity::class, + ], + InvoiceWasEmailedAndFailed::class => [ + InvoiceEmailFailedActivity::class, + ], + ]; /** diff --git a/app/Repositories/QuoteRepository.php b/app/Repositories/QuoteRepository.php index 68f22146c88c..a4650819d779 100644 --- a/app/Repositories/QuoteRepository.php +++ b/app/Repositories/QuoteRepository.php @@ -13,6 +13,7 @@ namespace App\Repositories; use App\Factory\QuoteInvitationFactory; use App\Helpers\Invoice\InvoiceSum; +use App\Jobs\Quote\ApplyQuoteNumber; use App\Jobs\Quote\CreateQuoteInvitations; use App\Models\Client; use App\Models\ClientContact; @@ -99,8 +100,8 @@ class QuoteRepository extends BaseRepository $quote->save(); $finished_amount = $quote->amount; -//todo need answers on this -// $quote = ApplyInvoiceNumber::dispatchNow($quote, $quote->client->getMergedSettings()); + + $quote = ApplyQuoteNumber::dispatchNow($quote, $quote->client->getMergedSettings()); return $quote->fresh(); } diff --git a/app/Transformers/InvoiceTransformer.php b/app/Transformers/InvoiceTransformer.php index e6fea3679f8f..0315a523a3b0 100644 --- a/app/Transformers/InvoiceTransformer.php +++ b/app/Transformers/InvoiceTransformer.php @@ -96,6 +96,7 @@ class InvoiceTransformer extends EntityTransformer 'discount' => (float) $invoice->discount, 'po_number' => $invoice->po_number ?: '', 'date' => $invoice->date ?: '', + 'last_sent_date' => $invoice->last_sent_date ?: '', 'next_send_date' => $invoice->date ?: '', 'due_date' => $invoice->due_date ?: '', 'terms' => $invoice->terms ?: '', @@ -103,7 +104,6 @@ class InvoiceTransformer extends EntityTransformer 'private_notes' => $invoice->private_notes ?: '', 'is_deleted' => (bool) $invoice->is_deleted, 'uses_inclusive_taxes' => (bool) $invoice->uses_inclusive_taxes, - 'invoice_type_id' => (string) $invoice->invoice_type_id ?: '', 'tax_name1' => $invoice->tax_name1 ? $invoice->tax_name1 : '', 'tax_rate1' => (float) $invoice->tax_rate1, 'tax_name2' => $invoice->tax_name2 ? $invoice->tax_name2 : '', diff --git a/app/Transformers/QuoteTransformer.php b/app/Transformers/QuoteTransformer.php index 74ef0e94de4a..fea5b5811af4 100644 --- a/app/Transformers/QuoteTransformer.php +++ b/app/Transformers/QuoteTransformer.php @@ -85,6 +85,7 @@ class QuoteTransformer extends EntityTransformer 'discount' => (float) $quote->discount, 'po_number' => $quote->po_number ?: '', 'date' => $quote->date ?: '', + 'last_sent_date' => $quote->last_sent_date ?: '', 'next_send_date' => $quote->date ?: '', 'due_date' => $quote->due_date ?: '', 'terms' => $quote->terms ?: '', @@ -92,7 +93,6 @@ class QuoteTransformer extends EntityTransformer 'private_notes' => $quote->private_notes ?: '', 'is_deleted' => (bool) $quote->is_deleted, 'uses_inclusive_taxes' => (bool) $quote->uses_inclusive_taxes, - 'invoice_type_id' => (string) $quote->invoice_type_id ?: '', 'tax_name1' => $quote->tax_name1 ? $quote->tax_name1 : '', 'tax_rate1' => (float) $quote->tax_rate1, 'tax_name2' => $quote->tax_name2 ? $quote->tax_name2 : '', diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index 6207f517bba7..1883b57686c2 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -96,9 +96,44 @@ trait GeneratesCounter return $credit_number; } - public function getNextQuoteNumber() + public function getNextQuoteNumber(Client $client) { + //Reset counters if enabled + $this->resetCounters($client); + $used_counter = 'quote_number_counter'; + + if($this->hasSharedCounter($client)) + $used_counter = 'invoice_number_counter'; + + //todo handle if we have specific client patterns in the future + $pattern = $client->getSetting('quote_number_pattern'); + //Determine if we are using client_counters + if(strpos($pattern, 'clientCounter')) + { + $counter = $client->settings->{$used_counter}; + $counter_entity = $client; + } + elseif(strpos($pattern, 'groupCounter')) + { + $counter = $client->group_settings->{$used_counter}; + $counter_entity = $client->group_settings; + } + else + { + $counter = $client->company->settings->{$used_counter}; + $counter_entity = $client->company; + } + + //Return a valid counter + $pattern = $client->getSetting('quote_number_pattern'); + $padding = $client->getSetting('counter_padding'); + + $quote_number = $this->checkEntityNumber(Quote::class, $client, $counter, $padding, $pattern); + + $this->incrementCounter($counter_entity, $used_counter); + + return $quote_number; } public function getNextRecurringInvoiceNumber() @@ -170,9 +205,10 @@ trait GeneratesCounter * @return boolean True if has shared counter, False otherwise. */ public function hasSharedCounter(Client $client) : bool - { - - return $client->getSetting('shared_invoice_quote_counter') === TRUE; + { +// \Log::error((bool) $client->getSetting('shared_invoice_quote_counter')); +// \Log::error($client->getSetting('shared_invoice_quote_counter')); + return (bool) $client->getSetting('shared_invoice_quote_counter'); } diff --git a/app/Utils/Traits/InvoiceEmailBuilder.php b/app/Utils/Traits/InvoiceEmailBuilder.php index 524b8bf5113e..534fd2f4d0b6 100644 --- a/app/Utils/Traits/InvoiceEmailBuilder.php +++ b/app/Utils/Traits/InvoiceEmailBuilder.php @@ -65,10 +65,12 @@ trait InvoiceEmailBuilder } - $data['body'] = $this->parseTemplate($body_template, false); $data['subject'] = $this->parseTemplate($subject_template, true); + if($client->getSetting('pdf_email_attachment') !== false) + $data['files'][] = $this->pdf_file_path(); + return $data; } @@ -108,8 +110,11 @@ trait InvoiceEmailBuilder { return 'template3'; } + else + return 'invoice'; + //also implement endless reminders here - // + } diff --git a/config/ninja.php b/config/ninja.php index 87c699742027..627f36b8f5be 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -59,7 +59,7 @@ return [ 'stripe' => env('STRIPE_KEYS',''), 'paypal' => env('PAYPAL_KEYS', ''), 'travis' => env('TRAVIS', false), - 'test_email' => env('TEST_EMAIL',''), + 'test_email' => env('TEST_EMAIL','test@example.com'), ], 'contact' => [ 'email' => env('MAIL_FROM_ADDRESS'), diff --git a/database/migrations/2014_10_13_000000_create_users_table.php b/database/migrations/2014_10_13_000000_create_users_table.php index 12622f99f5c4..b70bd73f3092 100644 --- a/database/migrations/2014_10_13_000000_create_users_table.php +++ b/database/migrations/2014_10_13_000000_create_users_table.php @@ -452,6 +452,8 @@ class CreateUsersTable extends Migration $t->string('po_number')->nullable(); $t->date('date')->nullable(); + $t->date('last_sent_date')->nullable(); + $t->datetime('due_date')->nullable(); $t->boolean('is_deleted')->default(false); @@ -660,6 +662,8 @@ class CreateUsersTable extends Migration $t->string('po_number')->nullable(); $t->date('date')->nullable(); + $t->date('last_sent_date')->nullable(); + $t->datetime('due_date')->nullable(); $t->datetime('next_send_date')->nullable(); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index df52d9a9a90f..c11f4bf11fbb 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -801,6 +801,7 @@ $LANG = array( 'activity_54' => ':user reopened ticket :ticket', 'activity_55' => ':contact replied ticket :ticket', 'activity_56' => ':user viewed ticket :ticket', + 'activity_57' => ':invoice failed to send to :client', 'payment' => 'Payment', 'system' => 'System', diff --git a/tests/Feature/InvoiceEmailTest.php b/tests/Feature/InvoiceEmailTest.php index d346d800e2a5..ac4b362e748e 100644 --- a/tests/Feature/InvoiceEmailTest.php +++ b/tests/Feature/InvoiceEmailTest.php @@ -55,6 +55,7 @@ class InvoiceEmailTest extends TestCase $message_array['title'] = &$message_array['subject']; $message_array['footer'] = 'The Footer'; + // $template_style = $this->client->getSetting('email_style'); $template_style = 'light'; @@ -62,24 +63,24 @@ class InvoiceEmailTest extends TestCase $invitations = InvoiceInvitation::whereInvoiceId($this->invoice->id)->get(); - $invitations->each(function($invitation) use($message_array, $template_style) { + $invitations->each(function($invitation) use($message_array, $template_styles) { - $contact = ClientContact::find($invitation->client_contact_id)->first(); + $contact = $invitation->contact; if($contact->send_invoice && $contact->email) { //there may be template variables left over for the specific contact? need to reparse here - + //change the runtime config of the mail provider here: //send message Mail::to($contact->email) - ->send(new TemplateEmail($message_array, $template_style, $this->user, $this->client)); + ->send(new TemplateEmail($message_array, $template_style, $this->user, $contact->client)); //fire any events - sleep(5); + sleep(5);//here to cope with mailtrap time delays } diff --git a/tests/Unit/GeneratesCounterTest.php b/tests/Unit/GeneratesCounterTest.php index a20869742872..f68b16dc9b15 100644 --- a/tests/Unit/GeneratesCounterTest.php +++ b/tests/Unit/GeneratesCounterTest.php @@ -50,6 +50,27 @@ class GeneratesCounterTest extends TestCase $this->assertFalse($this->hasSharedCounter($this->client)); } + public function testHasTrueSharedCounter() + { + $settings = $this->client->getMergedSettings(); + $settings->invoice_number_counter = 1; + $settings->invoice_number_pattern = '{$year}-{$counter}'; + $settings->shared_invoice_quote_counter = 1; + $this->company->settings = $settings; + + $this->company->save(); + + $this->client->settings = $settings; + $this->client->save(); + + $gs = $this->client->group_settings; + $gs->settings = $settings; + $gs->save(); + + $this->assertTrue($this->hasSharedCounter($this->client)); + + } + public function testInvoiceNumberValue() { @@ -63,6 +84,19 @@ class GeneratesCounterTest extends TestCase } + public function testQuoteNumberValue() + { + + $quote_number = $this->getNextQuoteNumber($this->client); + + $this->assertEquals($quote_number, 0001); + + $quote_number = $this->getNextQuoteNumber($this->client); + + $this->assertEquals($quote_number, '0002'); + + } + public function testInvoiceNumberPattern() { $settings = $this->client->company->settings; @@ -79,12 +113,58 @@ class GeneratesCounterTest extends TestCase $invoice_number = $this->getNextInvoiceNumber($this->client); $invoice_number2 = $this->getNextInvoiceNumber($this->client); - $this->assertEquals($invoice_number, '2019-0001'); - $this->assertEquals($invoice_number2, '2019-0002'); + $this->assertEquals($invoice_number, date('Y').'-0001'); + $this->assertEquals($invoice_number2, date('Y').'-0002'); $this->assertEquals($this->client->company->settings->invoice_number_counter,3); } + public function testQuoteNumberPattern() + { + $settings = $this->client->company->settings; + $settings->quote_number_counter = 1; + $settings->quote_number_pattern = '{$year}-{$counter}'; + + $this->client->company->settings = $settings; + $this->client->company->save(); + + $this->client->settings = $settings; + $this->client->save(); + $this->client->fresh(); + + $quote_number = $this->getNextQuoteNumber($this->client); + $quote_number2 = $this->getNextQuoteNumber($this->client); + + $this->assertEquals($quote_number, date('Y').'-0001'); + $this->assertEquals($quote_number2, date('Y').'-0002'); + $this->assertEquals($this->client->company->settings->quote_number_counter,3); + + } + + public function testQuoteNumberPatternWithSharedCounter() + { + $settings = $this->client->company->settings; + $settings->quote_number_counter = 100; + $settings->invoice_number_counter = 1000; + $settings->quote_number_pattern = '{$year}-{$counter}'; + $settings->shared_invoice_quote_counter = true; + + $this->client->company->settings = $settings; + $this->client->company->save(); + + $gs = $this->client->group_settings; + $gs->settings = $settings; + $gs->save(); + + $quote_number = $this->getNextQuoteNumber($this->client); + $quote_number2 = $this->getNextQuoteNumber($this->client); + + $this->assertEquals($quote_number, date('Y').'-1000'); + $this->assertEquals($quote_number2, date('Y').'-1001'); + $this->assertEquals($this->client->company->settings->quote_number_counter,100); + + } + public function testInvoiceClientNumberPattern() { $settings = $this->company->settings; @@ -106,10 +186,10 @@ class GeneratesCounterTest extends TestCase $invoice_number = $this->getNextClientNumber($this->client); - $this->assertEquals($invoice_number, '2019-0001'); + $this->assertEquals($invoice_number, date('Y').'-0001'); $invoice_number = $this->getNextClientNumber($this->client); - $this->assertEquals($invoice_number, '2019-0002'); + $this->assertEquals($invoice_number, date('Y').'-0002'); } @@ -266,8 +346,8 @@ class GeneratesCounterTest extends TestCase $this->client->setSettingsByEntity(Client::class, $settings); $company = Company::find($this->client->company_id); $this->assertEquals($company->settings->client_number_counter,1); - $this->assertEquals($this->getNextNumber($this->client), '2019-1'); - $this->assertEquals($this->getNextNumber($this->client), '2019-2'); + $this->assertEquals($this->getNextNumber($this->client), date('y').'-1'); + $this->assertEquals($this->getNextNumber($this->client), date('y').'-2'); $company = Company::find($this->client->company_id); $this->assertEquals($company->settings->client_number_counter,2);