1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 20:52:56 +01:00

Add payment failure emails

This commit is contained in:
David Bomba 2024-08-29 14:36:38 +10:00
parent e2365ee0f9
commit 6eaf3632d9
17 changed files with 298 additions and 35 deletions

View File

@ -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',

View File

@ -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 '<p>$client<br><br>'.ctrans('texts.client_payment_failure_body', ['invoice' => '$number', 'amount' => '$amount']).'</p><div class="center">$gateway_payment_error</div><br><div class="center">$payment_button</div>';
}
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 = '<p>$client<br><br>'.self::transformText('quote_reminder_message').'</p><div class="center">$view_button</div>';
return $invoice_message;
return '<p>$client<br><br>'.self::transformText('quote_reminder_message').'</p><div class="center">$view_button</div>';
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Events\Invoice;
use App\Models\Company;
use App\Models\Invoice;
use Illuminate\Queue\SerializesModels;
/**
* Class InvoiceAutoBillFailed.
*/
class InvoiceAutoBillFailed
{
use SerializesModels;
/**
* Create a new event instance.
*
* @param Invoice $invoice
* @param Company $company
* @param array $event_vars
*/
public function __construct(public Invoice $invoice, public Company $company, public array $event_vars, public ?string $notes)
{
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Events\Invoice;
use App\Models\Company;
use App\Models\Invoice;
use Illuminate\Queue\SerializesModels;
/**
* Class InvoiceAutoBillSuccess.
*/
class InvoiceAutoBillSuccess
{
use SerializesModels;
/**
* Create a new event instance.
*
* @param Invoice $invoice
* @param Company $company
* @param array $event_vars
*/
public function __construct(public Invoice $invoice, public Company $company, public array $event_vars)
{
}
}

View File

@ -35,7 +35,7 @@ class PrePaymentController extends Controller
/**
* Show the list of payments.
*
* @return Factory|View
* @return Factory|View|\Illuminate\Http\RedirectResponse
*/
public function index()
{

View File

@ -84,7 +84,7 @@ class PaymentFailedMailer implements ShouldQueue
$invoice = false;
if ($this->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();
}

View File

@ -0,0 +1,59 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\Invoice;
use App\Libraries\MultiDB;
use App\Models\Activity;
use App\Repositories\ActivityRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use stdClass;
class InvoiceAutoBillFailedActivity implements ShouldQueue
{
protected $activity_repo;
public $delay = 10;
/**
* Create the event listener.
*
* @param ActivityRepository $activity_repo
*/
public function __construct(ActivityRepository $activity_repo)
{
$this->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);
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\Invoice;
use App\Libraries\MultiDB;
use App\Models\Activity;
use App\Repositories\ActivityRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use stdClass;
class InvoiceAutoBillSuccessActivity implements ShouldQueue
{
protected $activity_repo;
public $delay = 10;
/**
* Create the event listener.
*
* @param ActivityRepository $activity_repo
*/
public function __construct(ActivityRepository $activity_repo)
{
$this->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);
}
}

View File

@ -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(),

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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 => [

View File

@ -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(),

View File

@ -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()));
}
}

View File

@ -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 = '<br><br>';

View File

@ -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;