From 6eaf3632d95e42c977e9332d7623ed57d346b858 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 29 Aug 2024 14:36:38 +1000 Subject: [PATCH] Add payment failure emails --- app/DataMapper/CompanySettings.php | 5 ++ app/DataMapper/EmailTemplateDefaults.php | 20 ++++++- app/Events/Invoice/InvoiceAutoBillFailed.php | 35 +++++++++++ app/Events/Invoice/InvoiceAutoBillSuccess.php | 35 +++++++++++ .../ClientPortal/PrePaymentController.php | 2 +- app/Jobs/Mail/PaymentFailedMailer.php | 2 +- .../Invoice/InvoiceAutoBillFailedActivity.php | 59 ++++++++++++++++++ .../InvoiceAutoBillSuccessActivity.php | 58 ++++++++++++++++++ app/Mail/Admin/ClientPaymentFailureObject.php | 60 ++++++++++++------- app/Models/Activity.php | 5 +- app/Models/BaseModel.php | 10 +--- .../Stripe/UpdatePaymentMethods.php | 19 +++++- app/Providers/EventServiceProvider.php | 10 ++++ app/Providers/StaticServiceProvider.php | 4 ++ app/Services/Invoice/AutoBillInvoice.php | 5 ++ app/Utils/HtmlEngine.php | 1 + lang/en/texts.php | 3 + 17 files changed, 298 insertions(+), 35 deletions(-) create mode 100644 app/Events/Invoice/InvoiceAutoBillFailed.php create mode 100644 app/Events/Invoice/InvoiceAutoBillSuccess.php create mode 100644 app/Listeners/Invoice/InvoiceAutoBillFailedActivity.php create mode 100644 app/Listeners/Invoice/InvoiceAutoBillSuccessActivity.php diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 9fde1fee41..2e201f637f 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -518,7 +518,12 @@ class CompanySettings extends BaseSettings public string $payment_flow = 'default'; //smooth + public string $email_subject_payment_failed = ''; + public string $email_template_payment_failed = ''; + public static $casts = [ + 'email_template_payment_failed' => 'string', + 'email_subject_payment_failed' => 'string', 'payment_flow' => 'string', 'enable_quote_reminder1' => 'bool', 'quote_num_days_reminder1' => 'int', diff --git a/app/DataMapper/EmailTemplateDefaults.php b/app/DataMapper/EmailTemplateDefaults.php index 7633b7edd0..a63b7f8901 100644 --- a/app/DataMapper/EmailTemplateDefaults.php +++ b/app/DataMapper/EmailTemplateDefaults.php @@ -30,6 +30,7 @@ class EmailTemplateDefaults 'email_template_custom2', 'email_template_custom3', 'email_template_purchase_order', + 'email_template_payment_failed' ]; public static function getDefaultTemplate($template, $locale) @@ -39,6 +40,8 @@ class EmailTemplateDefaults switch ($template) { /* Template */ + case 'email_template_payment_failed': + return self::emailPaymentFailedTemplate(); case 'email_template_invoice': return self::emailInvoiceTemplate(); case 'email_template_quote': @@ -73,6 +76,9 @@ class EmailTemplateDefaults case 'email_subject_invoice': return self::emailInvoiceSubject(); + case 'email_subject_payment_failed': + return self::emailPaymentFailedSubject(); + case 'email_subject_quote': return self::emailQuoteSubject(); @@ -127,6 +133,16 @@ class EmailTemplateDefaults } } + public static function emailPaymentFailedSubject() + { + return ctrans('texts.notification_invoice_payment_failed_subject', ['invoice' => '$number']); + } + + public static function emailPaymentFailedTemplate() + { + return '

$client

'.ctrans('texts.client_payment_failure_body', ['invoice' => '$number', 'amount' => '$amount']).'

$gateway_payment_error

$payment_button
'; + } + public static function emailQuoteReminder1Subject() { return ctrans('texts.quote_reminder_subject', ['quote' => '$number', 'company' => '$company.name']); @@ -135,9 +151,7 @@ class EmailTemplateDefaults public static function emailQuoteReminder1Body() { - $invoice_message = '

$client

'.self::transformText('quote_reminder_message').'

$view_button
'; - - return $invoice_message; + return '

$client

'.self::transformText('quote_reminder_message').'

$view_button
'; } diff --git a/app/Events/Invoice/InvoiceAutoBillFailed.php b/app/Events/Invoice/InvoiceAutoBillFailed.php new file mode 100644 index 0000000000..ce8d94a8ce --- /dev/null +++ b/app/Events/Invoice/InvoiceAutoBillFailed.php @@ -0,0 +1,35 @@ +payment_hash) { - // $amount = array_sum(array_column($this->payment_hash->invoices(), 'amount')) + $this->payment_hash->fee_total; + $amount = $this->payment_hash?->amount_with_fee() ?: 0; $invoice = Invoice::query()->whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->first(); } diff --git a/app/Listeners/Invoice/InvoiceAutoBillFailedActivity.php b/app/Listeners/Invoice/InvoiceAutoBillFailedActivity.php new file mode 100644 index 0000000000..6db4087daa --- /dev/null +++ b/app/Listeners/Invoice/InvoiceAutoBillFailedActivity.php @@ -0,0 +1,59 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDB($event->company->db); + + $fields = new stdClass(); + + $user_id = isset($event->event_vars['user_id']) ? $event->event_vars['user_id'] : $event->invoice->user_id; + + $fields->user_id = $user_id; + $fields->client_id = $event->invoice->client_id; + $fields->company_id = $event->invoice->company_id; + $fields->activity_type_id = Activity::AUTOBILL_FAILURE; + $fields->invoice_id = $event->invoice->id; + $fields->notes = $event->notes ?? ''; + + $this->activity_repo->save($fields, $event->invoice, $event->event_vars); + + } +} diff --git a/app/Listeners/Invoice/InvoiceAutoBillSuccessActivity.php b/app/Listeners/Invoice/InvoiceAutoBillSuccessActivity.php new file mode 100644 index 0000000000..c936fdd49a --- /dev/null +++ b/app/Listeners/Invoice/InvoiceAutoBillSuccessActivity.php @@ -0,0 +1,58 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDB($event->company->db); + + $fields = new stdClass(); + + $user_id = isset($event->event_vars['user_id']) ? $event->event_vars['user_id'] : $event->invoice->user_id; + + $fields->user_id = $user_id; + $fields->client_id = $event->invoice->client_id; + $fields->company_id = $event->invoice->company_id; + $fields->activity_type_id = Activity::AUTOBILL_SUCCESS; + $fields->invoice_id = $event->invoice->id; + + $this->activity_repo->save($fields, $event->invoice, $event->event_vars); + + } +} diff --git a/app/Mail/Admin/ClientPaymentFailureObject.php b/app/Mail/Admin/ClientPaymentFailureObject.php index c0dc385072..842d0f84fb 100644 --- a/app/Mail/Admin/ClientPaymentFailureObject.php +++ b/app/Mail/Admin/ClientPaymentFailureObject.php @@ -11,12 +11,14 @@ namespace App\Mail\Admin; +use stdClass; +use App\Utils\Ninja; use App\Models\Invoice; use App\Utils\HtmlEngine; -use App\Utils\Ninja; use App\Utils\Traits\MakesHash; use Illuminate\Support\Facades\App; -use stdClass; +use App\DataMapper\EmailTemplateDefaults; +use App\Utils\Number; class ClientPaymentFailureObject { @@ -60,20 +62,20 @@ class ClientPaymentFailureObject } App::forgetInstance('translator'); - /* Init a new copy of the translator*/ $t = app('translator'); - /* Set the locale*/ App::setLocale($this->client->locale()); - /* Set customized translations _NOW_ */ $t->replace(Ninja::transformTranslations($this->company->settings)); $this->invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get(); + $data = $this->getData(); + $mail_obj = new stdClass(); $mail_obj->amount = $this->getAmount(); - $mail_obj->subject = $this->getSubject(); + $mail_obj->subject = $data['subject']; $mail_obj->data = $this->getData(); - $mail_obj->markdown = 'email.client.generic'; + + $mail_obj->markdown = 'email.template.client'; $mail_obj->tag = $this->company->company_key; $mail_obj->text_view = 'email.template.text'; @@ -82,16 +84,32 @@ class ClientPaymentFailureObject private function getAmount() { - return array_sum(array_column($this->payment_hash->invoices(), 'amount')) + $this->payment_hash->fee_total; + $amount = array_sum(array_column($this->payment_hash->invoices(), 'amount')) + $this->payment_hash->fee_total; + + return Number::formatMoney($amount, $this->client); } private function getSubject() { - return - ctrans( - 'texts.notification_invoice_payment_failed_subject', - ['invoice' => implode(',', $this->invoices->pluck('number')->toArray())] - ); + + if(strlen($this->client->getSetting('email_subject_payment_failed') ?? '') > 2){ + return $this->client->getSetting('email_subject_payment_failed'); + } + else { + return EmailTemplateDefaults::getDefaultTemplate('email_subject_payment_failed', $this->client->locale()); + } + + } + + private function getBody() + { + + if(strlen($this->client->getSetting('email_template_payment_failed') ?? '') > 2) { + return $this->client->getSetting('email_template_payment_failed'); + } else { + return EmailTemplateDefaults::getDefaultTemplate('email_template_payment_failed', $this->client->locale()); + } + } private function getData() @@ -104,17 +122,17 @@ class ClientPaymentFailureObject $signature = $this->client->getSetting('email_signature'); $html_variables = (new HtmlEngine($invitation))->makeValues(); + + $html_variables['$gateway_payment_error'] = $this->error ?? ''; + $html_variables['$total'] = $this->getAmount(); + $signature = str_replace(array_keys($html_variables), array_values($html_variables), $signature); + $subject = str_replace(array_keys($html_variables), array_values($html_variables), $this->getSubject()); + $content = str_replace(array_keys($html_variables), array_values($html_variables), $this->getBody()); $data = [ - 'title' => ctrans( - 'texts.notification_invoice_payment_failed_subject', - [ - 'invoice' => $this->invoices->first()->number, - ] - ), - 'greeting' => ctrans('texts.email_salutation', ['name' => $this->client->present()->name()]), - 'content' => ctrans('texts.client_payment_failure_body', ['invoice' => implode(',', $this->invoices->pluck('number')->toArray()), 'amount' => $this->getAmount()]), + 'subject' => $subject, + 'body' => $content, 'signature' => $signature, 'logo' => $this->company->present()->logo(), 'settings' => $this->client->getMergedSettings(), diff --git a/app/Models/Activity.php b/app/Models/Activity.php index c2f7d6c501..cc57c9a0af 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -265,6 +265,10 @@ class Activity extends StaticModel public const QUOTE_REMINDER1_SENT = 142; + public const AUTOBILL_SUCCESS = 143; //:invoice auto billing succeeded + + public const AUTOBILL_FAILURE = 144; //:invoice autobilling failed :note + protected $casts = [ 'is_system' => 'boolean', 'updated_at' => 'timestamp', @@ -286,7 +290,6 @@ class Activity extends StaticModel return $this->encodePrimaryKey($this->id); } - public function getEntityType() { return self::class; diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 186058f241..dece6ea475 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -31,6 +31,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundExceptio * @package App\Models * @property-read mixed $hashed_id * @property string $number + * @property object|null $e_invoice * @property int $company_id * @property int $id * @property int $user_id @@ -296,17 +297,12 @@ class BaseModel extends Model } // special catch here for einvoicing eventing - if($event_id == Webhook::EVENT_SENT_INVOICE && $this->e_invoice){ - $this->handleEinvoiceSending(); + if($event_id == Webhook::EVENT_SENT_INVOICE && ($this instanceof Invoice) && $this->e_invoice){ + // Einvoice } } - private function handleEinvoiceSending() - { - - } - /** * Returns the base64 encoded PDF string of the entity * @deprecated - unused implementation diff --git a/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php b/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php index 244be05bfc..db354bce9c 100644 --- a/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php +++ b/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php @@ -49,6 +49,18 @@ class UpdatePaymentMethods $this->addOrUpdateCard($method, $customer->id, $client, GatewayType::CREDIT_CARD); } + $link_methods = PaymentMethod::all( + [ + 'customer' => $customer->id, + 'type' => 'link', + ], + $this->stripe->stripe_connect_auth + ); + + foreach ($link_methods as $method) { + $this->addOrUpdateCard($method, $customer->id, $client, GatewayType::CREDIT_CARD); + } + $alipay_methods = PaymentMethod::all( [ 'customer' => $customer->id, @@ -217,9 +229,14 @@ class UpdatePaymentMethods private function buildPaymentMethodMeta(PaymentMethod $method, $type_id) { + nlog($method->type); + switch ($type_id) { case GatewayType::CREDIT_CARD: + if($method->type == 'link') + return new \stdClass(); + /** * @class \Stripe\PaymentMethod $method * @property \Stripe\StripeObject $card @@ -240,7 +257,7 @@ class UpdatePaymentMethods return $payment_meta; case GatewayType::ALIPAY: case GatewayType::SOFORT: - + return new \stdClass(); case GatewayType::SEPA: diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 57ddd8d7e1..caa924253c 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -155,6 +155,8 @@ use App\Listeners\Activity\TaskUpdatedActivity; use App\Listeners\Invoice\InvoiceEmailActivity; use App\Listeners\SendVerificationNotification; use App\Events\Credit\CreditWasEmailedAndFailed; +use App\Events\Invoice\InvoiceAutoBillFailed; +use App\Events\Invoice\InvoiceAutoBillSuccess; use App\Listeners\Activity\CreatedQuoteActivity; use App\Listeners\Activity\DeleteClientActivity; use App\Listeners\Activity\DeleteCreditActivity; @@ -250,6 +252,8 @@ use App\Events\RecurringExpense\RecurringExpenseWasArchived; use App\Events\RecurringExpense\RecurringExpenseWasRestored; use App\Events\RecurringInvoice\RecurringInvoiceWasArchived; use App\Events\RecurringInvoice\RecurringInvoiceWasRestored; +use App\Listeners\Invoice\InvoiceAutoBillFailedActivity; +use App\Listeners\Invoice\InvoiceAutoBillSuccessActivity; use App\Listeners\PurchaseOrder\CreatePurchaseOrderActivity; use App\Listeners\PurchaseOrder\PurchaseOrderViewedActivity; use App\Listeners\PurchaseOrder\UpdatePurchaseOrderActivity; @@ -426,6 +430,12 @@ class EventServiceProvider extends ServiceProvider ExpenseRestoredActivity::class, ], //Invoices + InvoiceAutoBillSuccess::class => [ + InvoiceAutoBillSuccessActivity::class, + ], + InvoiceAutoBillFailed::class => [ + InvoiceAutoBillFailedActivity::class, + ], InvoiceWasMarkedSent::class => [ ], InvoiceWasUpdated::class => [ diff --git a/app/Providers/StaticServiceProvider.php b/app/Providers/StaticServiceProvider.php index 00aa774c20..848bddb77b 100644 --- a/app/Providers/StaticServiceProvider.php +++ b/app/Providers/StaticServiceProvider.php @@ -222,6 +222,10 @@ class StaticServiceProvider extends ServiceProvider 'subject' => EmailTemplateDefaults::emailPaymentSubject(), 'body' => EmailTemplateDefaults::emailPaymentTemplate(), ], + 'payment_failed' => [ + 'subject' => EmailTemplateDefaults::emailPaymentFailedSubject(), + 'body' => EmailTemplateDefaults::emailPaymentFailedTemplate(), + ], 'quote_reminder1' => [ 'subject' => EmailTemplateDefaults::emailQuoteReminder1Subject(), 'body' => EmailTemplateDefaults::emailQuoteReminder1Body(), diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 15e4ef6454..cfb686dac5 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -23,6 +23,8 @@ use App\Models\PaymentHash; use App\Models\PaymentType; use Illuminate\Support\Str; use App\DataMapper\InvoiceItem; +use App\Events\Invoice\InvoiceAutoBillFailed; +use App\Events\Invoice\InvoiceAutoBillSuccess; use App\Factory\PaymentFactory; use App\Services\AbstractService; use App\Models\ClientGatewayToken; @@ -157,6 +159,8 @@ class AutoBillInvoice extends AbstractService } catch (\Exception $e) { nlog('payment NOT captured for '.$this->invoice->number.' with error '.$e->getMessage()); + event(new InvoiceAutoBillFailed($this->invoice, $this->invoice->company, Ninja::eventVars(), $e->getMessage())); + } $this->invoice->auto_bill_tries += 1; @@ -170,6 +174,7 @@ class AutoBillInvoice extends AbstractService if ($payment) { info('Auto Bill payment captured for '.$this->invoice->number); + event(new InvoiceAutoBillSuccess($this->invoice, $this->invoice->company, Ninja::eventVars())); } } diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 89b7d470e5..e39f257e06 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -733,6 +733,7 @@ class HtmlEngine $data['$payment.number'] = ['value' => '', 'label' => ctrans('texts.payment_number')]; $data['$payment.transaction_reference'] = ['value' => '', 'label' => ctrans('texts.transaction_reference')]; $data['$payment.refunded'] = ['value' => '', 'label' => ctrans('texts.refund')]; + $data['$gateway_payment_error'] = ['value' => '', 'label' => ctrans('texts.error')]; if ($this->entity_string == 'invoice' && $this->entity->net_payments()->exists()) { $payment_list = '

'; diff --git a/lang/en/texts.php b/lang/en/texts.php index 55ded895bd..372bccf2f5 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5321,6 +5321,9 @@ $lang = array( 'applies_to' => 'Applies To', 'accept_purchase_order' => 'Accept Purchase Order', 'round_to_seconds' => 'Round To Seconds', + 'activity_142' => 'Quote :number Reminder 1 Sent', + 'activity_143' => 'Auto Bill succeeded for Invoice :invoice', + 'activity_144' => 'Auto Bill failed for Invoice :invoice. :notes', ); return $lang;