From f14de426846efeb4d68c29f3682555d7a68450bd Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 17 Mar 2023 12:49:08 +1100 Subject: [PATCH 01/12] Add recurring options to ivnoice --- app/Factory/RecurringInvoiceFactory.php | 1 + .../RecurringInvoiceToInvoiceFactory.php | 1 + .../ClientPortal/PrePaymentController.php | 4 +++ app/Http/ViewComposers/PortalComposer.php | 4 +-- app/Services/ClientPortal/InstantPayment.php | 12 ++++++- app/Services/Invoice/ApplyNumber.php | 7 +++++ app/Services/Invoice/AutoBillInvoice.php | 12 ++++++- app/Services/Payment/UpdateInvoicePayment.php | 28 +++++++++++++++-- .../Subscription/SubscriptionService.php | 2 +- ...d_proforma_flag_for_recurring_invoices.php | 31 +++++++++++++++++++ lang/en/texts.php | 4 ++- .../ninja2020/invoices/payment.blade.php | 3 ++ .../ninja2020/pre_payments/index.blade.php | 18 +++++------ 13 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 database/migrations/2023_03_17_012309_add_proforma_flag_for_recurring_invoices.php diff --git a/app/Factory/RecurringInvoiceFactory.php b/app/Factory/RecurringInvoiceFactory.php index e246d5cea30a..54fbbc354272 100644 --- a/app/Factory/RecurringInvoiceFactory.php +++ b/app/Factory/RecurringInvoiceFactory.php @@ -53,6 +53,7 @@ class RecurringInvoiceFactory $invoice->remaining_cycles = -1; $invoice->paid_to_date = 0; $invoice->auto_bill_enabled = false; + $invoice->is_proforma = false; $invoice->auto_bill = 'off'; return $invoice; diff --git a/app/Factory/RecurringInvoiceToInvoiceFactory.php b/app/Factory/RecurringInvoiceToInvoiceFactory.php index 3692e458c62e..63b5fa024bec 100644 --- a/app/Factory/RecurringInvoiceToInvoiceFactory.php +++ b/app/Factory/RecurringInvoiceToInvoiceFactory.php @@ -46,6 +46,7 @@ class RecurringInvoiceToInvoiceFactory $invoice->custom_value4 = $recurring_invoice->custom_value4; $invoice->amount = $recurring_invoice->amount; $invoice->uses_inclusive_taxes = $recurring_invoice->uses_inclusive_taxes; + $invoice->is_proforma = $recurring_invoice->is_proforma; $invoice->custom_surcharge1 = $recurring_invoice->custom_surcharge1; $invoice->custom_surcharge2 = $recurring_invoice->custom_surcharge2; diff --git a/app/Http/Controllers/ClientPortal/PrePaymentController.php b/app/Http/Controllers/ClientPortal/PrePaymentController.php index 9c8070e572d4..8e3f37777804 100644 --- a/app/Http/Controllers/ClientPortal/PrePaymentController.php +++ b/app/Http/Controllers/ClientPortal/PrePaymentController.php @@ -80,6 +80,7 @@ class PrePaymentController extends Controller $invoice = $invoice_repo->save($data, $invoice) ->service() ->markSent() + ->applyNumber() ->fillDefaults() ->save(); @@ -107,6 +108,9 @@ class PrePaymentController extends Controller 'hashed_ids' => $invoices->pluck('hashed_id'), 'total' => $total, 'pre_payment' => true, + 'frequency_id' => $request->frequency_id, + 'remaining_cycles' => $request->remaining_cycles, + 'is_recurring' => $request->is_recurring, ]; diff --git a/app/Http/ViewComposers/PortalComposer.php b/app/Http/ViewComposers/PortalComposer.php index 747cc9572adb..3e796d110b5b 100644 --- a/app/Http/ViewComposers/PortalComposer.php +++ b/app/Http/ViewComposers/PortalComposer.php @@ -138,9 +138,9 @@ class PortalComposer $data[] = ['title' => ctrans('texts.subscriptions'), 'url' => 'client.subscriptions.index', 'icon' => 'calendar']; } - if(property_exists($this->settings, 'client_initiated_payments') && $this->settings->client_initiated_payments) { + // if(property_exists($this->settings, 'client_initiated_payments') && $this->settings->client_initiated_payments) { $data[] = ['title' => ctrans('texts.pre_payment'), 'url' => 'client.pre_payments.index', 'icon' => 'dollar-sign']; - } + // } return $data; } diff --git a/app/Services/ClientPortal/InstantPayment.php b/app/Services/ClientPortal/InstantPayment.php index 6dcae4a17921..3cdc656e15c6 100644 --- a/app/Services/ClientPortal/InstantPayment.php +++ b/app/Services/ClientPortal/InstantPayment.php @@ -34,6 +34,7 @@ class InstantPayment use MakesHash; use MakesDates; + /** $request mixed */ public Request $request; public function __construct(Request $request) @@ -214,7 +215,16 @@ class InstantPayment $credit_totals = 0; } - $hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals, 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals)), 'pre_payment' => $this->request->pre_payment]; + /** $hash_data = mixed[] */ + $hash_data = [ + 'invoices' => $payable_invoices->toArray(), + 'credits' => $credit_totals, + 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals)), + 'pre_payment' => $this->request->pre_payment, + 'frequency_id' => $this->request->frequency_id, + 'remaining_cycles' => $this->request->remaining_cycles, + 'is_recurring' => $this->request->is_recurring, + ]; if ($this->request->query('hash')) { $hash_data['billing_context'] = Cache::get($this->request->query('hash')); diff --git a/app/Services/Invoice/ApplyNumber.php b/app/Services/Invoice/ApplyNumber.php index ee4368f557b3..93c5adb187b5 100644 --- a/app/Services/Invoice/ApplyNumber.php +++ b/app/Services/Invoice/ApplyNumber.php @@ -40,6 +40,13 @@ class ApplyNumber extends AbstractService return $this->invoice; } + /** Do no give pro forma invoices a proper invoice number */ + if($this->invoice->is_proforma) { + $this->invoice->number = ctrans('texts.pre_payment') . " " . now()->format('Y-m-d : H:i:s'); + $this->invoice->saveQuietly(); + return $this->invoice; + } + switch ($this->client->getSetting('counter_number_applied')) { case 'when_saved': $this->trySaving(); diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index cb40857bcabb..d60a14fa0953 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -121,7 +121,17 @@ class AutoBillInvoice extends AbstractService $payment_hash = PaymentHash::create([ 'hash' => Str::random(64), - 'data' => ['amount_with_fee' => $amount + $fee, 'invoices' => [['invoice_id' => $this->invoice->hashed_id, 'amount' => $amount, 'invoice_number' => $this->invoice->number]]], + 'data' => [ + 'amount_with_fee' => $amount + $fee, + 'invoices' => [ + [ + 'invoice_id' => $this->invoice->hashed_id, + 'amount' => $amount, + 'invoice_number' => $this->invoice->number, + 'pre_payment' => $this->invoice->is_proforma, + ], + ], + ], 'fee_total' => $fee, 'fee_invoice_id' => $this->invoice->id, ]); diff --git a/app/Services/Payment/UpdateInvoicePayment.php b/app/Services/Payment/UpdateInvoicePayment.php index 0fe1a9001edc..7f6b0372e8c0 100644 --- a/app/Services/Payment/UpdateInvoicePayment.php +++ b/app/Services/Payment/UpdateInvoicePayment.php @@ -11,12 +11,14 @@ namespace App\Services\Payment; -use App\Events\Invoice\InvoiceWasUpdated; +use App\Utils\Ninja; use App\Models\Invoice; use App\Models\Payment; use App\Models\PaymentHash; -use App\Utils\Ninja; use App\Utils\Traits\MakesHash; +use App\Models\RecurringInvoice; +use App\Factory\RecurringInvoiceFactory; +use App\Events\Invoice\InvoiceWasUpdated; class UpdateInvoicePayment { @@ -93,9 +95,31 @@ class UpdateInvoicePayment $invoice->is_deleted = true; $invoice->deleted_at = now(); $invoice->saveQuietly(); + + if (property_exists($this->payment_hash->data, 'is_recurring') && $this->payment_hash->data->is_recurring == "1") { + $recurring_invoice = RecurringInvoiceFactory::create($invoice->company_id, $invoice->user_id); + $recurring_invoice->client_id = $invoice->client_id; + $recurring_invoice->line_items = $invoice->line_items; + $recurring_invoice->frequency_id = $this->payment_hash->data->is_recurring ?: RecurringInvoice::FREQUENCY_MONTHLY; + $recurring_invoice->date = now(); + $recurring_invoice->remaining_cycles = $this->payment_hash->data->remaining_cycles; + $recurring_invoice->auto_bill = 'always'; + $recurring_invoice->auto_bill_enabled = true; + $recurring_invoice->due_date_days = 'on_receipt'; + $recurring_invoice->next_send_date = now()->format('Y-m-d'); + $recurring_invoice->next_send_date_client = now()->format('Y-m-d'); + + $recurring_invoice->saveQuietly(); + $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); + $recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient(); + $recurring_invoice->saveQuietly(); + } + return; } + + if (strlen($invoice->number) > 1 && str_starts_with($invoice->number, "####")) $invoice->number = ''; diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index 2bc4060aadb0..49f44eca53ad 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -1087,7 +1087,7 @@ class SubscriptionService } - private function setAutoBillFlag($auto_bill) + private function setAutoBillFlag($auto_bill): bool { if ($auto_bill == 'always' || $auto_bill == 'optout') { return true; diff --git a/database/migrations/2023_03_17_012309_add_proforma_flag_for_recurring_invoices.php b/database/migrations/2023_03_17_012309_add_proforma_flag_for_recurring_invoices.php new file mode 100644 index 000000000000..afdbcb3faa72 --- /dev/null +++ b/database/migrations/2023_03_17_012309_add_proforma_flag_for_recurring_invoices.php @@ -0,0 +1,31 @@ +boolean('is_proforma')->default(false); + }); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}; diff --git a/lang/en/texts.php b/lang/en/texts.php index 19230f5b0211..a76a4df9b1c7 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5021,7 +5021,9 @@ $LANG = array( 'payment_type_Klarna' => 'Klarna', 'payment_type_Interac E Transfer' => 'Interac E Transfer', 'pre_payment' => 'Pre Payment', - 'client_remaining_cycles_helper' => 'The number of times this invoice will be generated', + 'number_of_payments' => 'Number of payments', + 'number_of_payments_helper' => 'The number of times this payment will be made', + 'pre_payment_indefinitely' => 'Continue until cancelled', ); diff --git a/resources/views/portal/ninja2020/invoices/payment.blade.php b/resources/views/portal/ninja2020/invoices/payment.blade.php index adc7b32c90b6..bc476baca7ed 100644 --- a/resources/views/portal/ninja2020/invoices/payment.blade.php +++ b/resources/views/portal/ninja2020/invoices/payment.blade.php @@ -14,6 +14,9 @@ + + +
diff --git a/resources/views/portal/ninja2020/pre_payments/index.blade.php b/resources/views/portal/ninja2020/pre_payments/index.blade.php index 50acf0fa0ad6..2a0455c25f63 100644 --- a/resources/views/portal/ninja2020/pre_payments/index.blade.php +++ b/resources/views/portal/ninja2020/pre_payments/index.blade.php @@ -56,15 +56,15 @@ @endcomponent
- @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.cycles_remaining')]) + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.number_of_payments')]) - + @endcomponent @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.frequency')]) @@ -73,14 +73,14 @@ - + - - - - - + + + + + @endcomponent
From 8c7aa563f4a6daac64803d4939b4008878bd6619 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 17 Mar 2023 13:55:46 +1100 Subject: [PATCH 02/12] Fixes for liap --- .../ClientPortal/PrePaymentController.php | 3 +- .../AppStoreRenewSubscription.php | 62 ------------------- .../PlayStoreRenewSubscription.php | 49 --------------- app/Models/Gateway.php | 62 +++++-------------- app/Services/ClientPortal/InstantPayment.php | 1 + app/Services/Payment/UpdateInvoicePayment.php | 8 ++- config/liap.php | 6 +- config/ninja.php | 1 + .../gateways/includes/save_card.blade.php | 4 ++ .../gateways/stripe/credit_card/pay.blade.php | 5 ++ 10 files changed, 36 insertions(+), 165 deletions(-) delete mode 100644 app/Listeners/Subscription/AppStoreRenewSubscription.php delete mode 100644 app/Listeners/Subscription/PlayStoreRenewSubscription.php diff --git a/app/Http/Controllers/ClientPortal/PrePaymentController.php b/app/Http/Controllers/ClientPortal/PrePaymentController.php index 8e3f37777804..874be8a353aa 100644 --- a/app/Http/Controllers/ClientPortal/PrePaymentController.php +++ b/app/Http/Controllers/ClientPortal/PrePaymentController.php @@ -110,9 +110,8 @@ class PrePaymentController extends Controller 'pre_payment' => true, 'frequency_id' => $request->frequency_id, 'remaining_cycles' => $request->remaining_cycles, - 'is_recurring' => $request->is_recurring, + 'is_recurring' => $request->is_recurring == 'on' ? true : false, ]; - return $this->render('invoices.payment', $data); diff --git a/app/Listeners/Subscription/AppStoreRenewSubscription.php b/app/Listeners/Subscription/AppStoreRenewSubscription.php deleted file mode 100644 index 07e5c493f9b3..000000000000 --- a/app/Listeners/Subscription/AppStoreRenewSubscription.php +++ /dev/null @@ -1,62 +0,0 @@ -getSubscriptionId(); //$subscription_id - - nlog("inapp upgrade processing for = {$inapp_transaction_id}"); - - MultiDB::findAndSetDbByInappTransactionId($inapp_transaction_id); - - $account = Account::where('inapp_transaction_id', $inapp_transaction_id)->first(); - - if (!$account) { - $ninja_company = Company::on('db-ninja-01')->find(config('ninja.ninja_default_company_id')); - $ninja_company->notification(new RenewalFailureNotification("{$inapp_transaction_id}"))->ninja(); - return; - } - - if ($account->plan_term == 'month') { - $account->plan_expires = now()->addMonth(); - } elseif ($account->plan_term == 'year') { - $account->plan_expires = now()->addYear(); - } - - $account->save(); - } -} diff --git a/app/Listeners/Subscription/PlayStoreRenewSubscription.php b/app/Listeners/Subscription/PlayStoreRenewSubscription.php deleted file mode 100644 index 1a3bb1684fa8..000000000000 --- a/app/Listeners/Subscription/PlayStoreRenewSubscription.php +++ /dev/null @@ -1,49 +0,0 @@ -getServerNotification(); - nlog("google"); - nlog($notification); - $in_app_identifier = $event->getSubscriptionIdentifier(); - - $parts = explode("..", $in_app_identifier); - - MultiDB::findAndSetDbByInappTransactionId($parts[0]); - - $expirationTime = $event->getSubscription()->getExpiryTime(); - - $account = Account::where('inapp_transaction_id', 'like', $parts[0]."%")->first(); - - if ($account) { - $account->update(['plan_expires' => Carbon::parse($expirationTime)]); - } - - if (!$account) { - $ninja_company = Company::on('db-ninja-01')->find(config('ninja.ninja_default_company_id')); - $ninja_company->notification(new RenewalFailureNotification("{$in_app_identifier}"))->ninja(); - return; - } - } -} diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 9c19e252020e..aa1105b852a9 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -117,13 +117,10 @@ class Gateway extends StaticModel switch ($this->id) { case 1: return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Authorize.net - break; case 3: return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true]]; //eWay - break; case 11: return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true]]; //Payfast - break; case 7: return [ GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true, 'webhooks' => [' ']], // Mollie @@ -136,31 +133,29 @@ class Gateway extends StaticModel return [ GatewayType::PAYPAL => ['refund' => false, 'token_billing' => false], ]; //Paypal - break; case 20: + case 56: return [ - GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded']], - GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'customer.source.updated','payment_intent.processing']], - GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing','payment_intent.succeeded','payment_intent.partially_funded']], + GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'customer.source.updated','payment_intent.processing', 'payment_intent.payment_failed']], + GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing','payment_intent.succeeded','payment_intent.partially_funded', 'payment_intent.payment_failed']], GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], - GatewayType::BACS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated']], - GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], + GatewayType::BACS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated', 'payment_intent.payment_failed']], + GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], ]; - break; case 39: return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout - break; case 46: return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Paytrace case 49: @@ -168,40 +163,16 @@ class Gateway extends StaticModel GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']], ]; //WePay - break; case 50: return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], //Braintree GatewayType::PAYPAL => ['refund' => true, 'token_billing' => true], GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']], ]; - break; - case 56: //Stripe - return [ - GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded']], - GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'customer.source.updated','payment_intent.processing']], - GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing','payment_intent.succeeded','payment_intent.partially_funded']], - GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], - GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], - GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::BACS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated']], - GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], - GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], - ]; - break; case 57: return [ GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true], //Square ]; - break; case 52: return [ GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true, 'webhooks' => [' ']], // GoCardless @@ -214,16 +185,13 @@ class Gateway extends StaticModel return [ GatewayType::HOSTED_PAGE => ['refund' => false, 'token_billing' => false, 'webhooks' => [' ']], // Razorpay ]; - break; case 59: return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], // Forte GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']], ]; - break; default: return []; - break; } } } diff --git a/app/Services/ClientPortal/InstantPayment.php b/app/Services/ClientPortal/InstantPayment.php index 3cdc656e15c6..b8cc2b6cba7d 100644 --- a/app/Services/ClientPortal/InstantPayment.php +++ b/app/Services/ClientPortal/InstantPayment.php @@ -266,6 +266,7 @@ class InstantPayment 'payment_method_id' => $payment_method_id, 'amount_with_fee' => $invoice_totals + $fee_totals, 'client' => $client, + 'pre_payment' => $this->request->pre_payment, ]; if ($is_credit_payment || $totals <= 0) { diff --git a/app/Services/Payment/UpdateInvoicePayment.php b/app/Services/Payment/UpdateInvoicePayment.php index 7f6b0372e8c0..295496a78be8 100644 --- a/app/Services/Payment/UpdateInvoicePayment.php +++ b/app/Services/Payment/UpdateInvoicePayment.php @@ -100,7 +100,7 @@ class UpdateInvoicePayment $recurring_invoice = RecurringInvoiceFactory::create($invoice->company_id, $invoice->user_id); $recurring_invoice->client_id = $invoice->client_id; $recurring_invoice->line_items = $invoice->line_items; - $recurring_invoice->frequency_id = $this->payment_hash->data->is_recurring ?: RecurringInvoice::FREQUENCY_MONTHLY; + $recurring_invoice->frequency_id = $this->payment_hash->data->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY; $recurring_invoice->date = now(); $recurring_invoice->remaining_cycles = $this->payment_hash->data->remaining_cycles; $recurring_invoice->auto_bill = 'always'; @@ -108,7 +108,11 @@ class UpdateInvoicePayment $recurring_invoice->due_date_days = 'on_receipt'; $recurring_invoice->next_send_date = now()->format('Y-m-d'); $recurring_invoice->next_send_date_client = now()->format('Y-m-d'); - + $recurring_invoice->amount = $invoice->amount; + $recurring_invoice->balance = $invoice->amount; + $recurring_invoice->status_id = RecurringInvoice::STATUS_ACTIVE; + $recurring_invoice->is_proforma = true; + $recurring_invoice->saveQuietly(); $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); $recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient(); diff --git a/config/liap.php b/config/liap.php index f140554cf65f..cbc569b294b0 100644 --- a/config/liap.php +++ b/config/liap.php @@ -2,7 +2,6 @@ use Imdhemy\Purchases\Events\AppStore\DidRenew; use App\Listeners\Subscription\AppStoreRenewSubscription; -use App\Listeners\Subscription\PlayStoreRenewSubscription; use Imdhemy\Purchases\Events\GooglePlay\SubscriptionRenewed; return [ @@ -97,8 +96,9 @@ return [ /* \Imdhemy\Purchases\Events\GooglePlay\SubscriptionRecovered::class => [ \App\Listeners\GooglePlay\SubscriptionRecovered::class, ],*/ - SubscriptionRenewed::class => [PlayStoreRenewSubscription::class], - DidRenew::class => [AppStoreRenewSubscription::class], + + DidRenew::class => class_exists(\Modules\Admin\Listeners\Subscription\AppleAutoRenew::class) ? [\Modules\Admin\Listeners\Subscription\AppleAutoRenew::class] : [], + SubscriptionRenewed::class => class_exists(\Modules\Admin\Listeners\Subscription\GoogleAutoRenew::class) ? [\Modules\Admin\Listeners\Subscription\GoogleAutoRenew::class] : [], ], diff --git a/config/ninja.php b/config/ninja.php index e847e0c1d386..0006bbeaa153 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -213,4 +213,5 @@ return [ 'config_name' => env("YODLEE_CONFIG_NAME", false), ], 'licenses' => env('LICENSES',false), + 'google_application_credentials' => env("GOOGLE_APPLICATION_CREDENTIALS", false), ]; diff --git a/resources/views/portal/ninja2020/gateways/includes/save_card.blade.php b/resources/views/portal/ninja2020/gateways/includes/save_card.blade.php index 7a4ea8b41a3f..ce4967258a9f 100644 --- a/resources/views/portal/ninja2020/gateways/includes/save_card.blade.php +++ b/resources/views/portal/ninja2020/gateways/includes/save_card.blade.php @@ -21,6 +21,10 @@ $checked_off = ''; } + if (isset($pre_payment) && $pre_payment == '1') { + $token_billing_string = 'true'; + } + @endphp @if($token_billing) diff --git a/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php index 295a53270897..d4d81ef11d7d 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php @@ -7,6 +7,11 @@ if($gateway_instance->token_billing == 'off' || $gateway_instance->token_billing == 'optin'){ $token_billing_string = 'false'; } + + if (isset($pre_payment) && $pre_payment == '1') { + $token_billing_string = 'true'; + } + @endphp From d91099a279bd455376ba00e1b7a856da8b4aabc2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 17 Mar 2023 14:29:53 +1100 Subject: [PATCH 03/12] Apply recurring invoice number to proforma recurring --- app/Services/Payment/UpdateInvoicePayment.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Services/Payment/UpdateInvoicePayment.php b/app/Services/Payment/UpdateInvoicePayment.php index 295496a78be8..aba182f80668 100644 --- a/app/Services/Payment/UpdateInvoicePayment.php +++ b/app/Services/Payment/UpdateInvoicePayment.php @@ -116,7 +116,8 @@ class UpdateInvoicePayment $recurring_invoice->saveQuietly(); $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); $recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient(); - $recurring_invoice->saveQuietly(); + $recurring_invoice->service()->applyNumber()->save(); + } return; From 033ce1af9519e6be9cd382a17b444191e6eb0b61 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 17 Mar 2023 14:38:00 +1100 Subject: [PATCH 04/12] Fixes for pre purchase --- app/Services/ClientPortal/InstantPayment.php | 1 + .../portal/ninja2020/gateways/includes/save_card.blade.php | 2 +- .../portal/ninja2020/gateways/stripe/credit_card/pay.blade.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Services/ClientPortal/InstantPayment.php b/app/Services/ClientPortal/InstantPayment.php index b8cc2b6cba7d..1b0daac41314 100644 --- a/app/Services/ClientPortal/InstantPayment.php +++ b/app/Services/ClientPortal/InstantPayment.php @@ -267,6 +267,7 @@ class InstantPayment 'amount_with_fee' => $invoice_totals + $fee_totals, 'client' => $client, 'pre_payment' => $this->request->pre_payment, + 'is_recurring' => $this->request->is_recurring, ]; if ($is_credit_payment || $totals <= 0) { diff --git a/resources/views/portal/ninja2020/gateways/includes/save_card.blade.php b/resources/views/portal/ninja2020/gateways/includes/save_card.blade.php index ce4967258a9f..86ca662f4f92 100644 --- a/resources/views/portal/ninja2020/gateways/includes/save_card.blade.php +++ b/resources/views/portal/ninja2020/gateways/includes/save_card.blade.php @@ -21,7 +21,7 @@ $checked_off = ''; } - if (isset($pre_payment) && $pre_payment == '1') { + if (isset($pre_payment) && $pre_payment == '1' && isset($is_recurring) && $is_recurring == '1') { $token_billing_string = 'true'; } diff --git a/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php index d4d81ef11d7d..c4bc33bbdf5a 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php @@ -8,7 +8,7 @@ $token_billing_string = 'false'; } - if (isset($pre_payment) && $pre_payment == '1') { + if (isset($pre_payment) && $pre_payment == '1' && isset($is_recurring) && $is_recurring == '1') { $token_billing_string = 'true'; } From 17cfd637e4e34011212769f1ac20ebf683b13ca8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 17 Mar 2023 16:58:57 +1100 Subject: [PATCH 05/12] Encode the recurring expense id --- app/Transformers/ExpenseTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Transformers/ExpenseTransformer.php b/app/Transformers/ExpenseTransformer.php index c9adf5e3ddad..f703da58058a 100644 --- a/app/Transformers/ExpenseTransformer.php +++ b/app/Transformers/ExpenseTransformer.php @@ -87,7 +87,7 @@ class ExpenseTransformer extends EntityTransformer 'currency_id' => (string) $expense->currency_id ?: '', 'category_id' => $this->encodePrimaryKey($expense->category_id), 'payment_type_id' => (string) $expense->payment_type_id ?: '', - 'recurring_expense_id' => (string) $expense->recurring_expense_id ?: '', + 'recurring_expense_id' => (string) $this->encodePrimaryKey($expense->recurring_expense_id) ?: '', 'is_deleted' => (bool) $expense->is_deleted, 'should_be_invoiced' => (bool) $expense->should_be_invoiced, 'invoice_documents' => (bool) $expense->invoice_documents, From 58491eaf069a635d47ac8dab4b20cbe4a13e264e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 17 Mar 2023 17:01:45 +1100 Subject: [PATCH 06/12] cs-fixer --- app/Jobs/Cron/RecurringExpensesCron.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Jobs/Cron/RecurringExpensesCron.php b/app/Jobs/Cron/RecurringExpensesCron.php index 773a97e26ebe..5b7fb2c33cae 100644 --- a/app/Jobs/Cron/RecurringExpensesCron.php +++ b/app/Jobs/Cron/RecurringExpensesCron.php @@ -40,7 +40,7 @@ class RecurringExpensesCron * * @return void */ - public function handle() : void + public function handle(): void { /* Get all expenses where the send date is less than NOW + 30 minutes() */ nlog('Sending recurring expenses '.Carbon::now()->format('Y-m-d h:i:s')); From 4add5be30714eacaa8bf9dd8afa28c6d73c7b659 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 17 Mar 2023 17:23:48 +1100 Subject: [PATCH 07/12] Wire up Reminder activities --- app/Jobs/Util/ReminderJob.php | 55 ++++++++++++++++++----------------- app/Models/Invoice.php | 3 +- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/app/Jobs/Util/ReminderJob.php b/app/Jobs/Util/ReminderJob.php index 2aef984496d4..bf56b28546fe 100644 --- a/app/Jobs/Util/ReminderJob.php +++ b/app/Jobs/Util/ReminderJob.php @@ -11,27 +11,33 @@ namespace App\Jobs\Util; -use App\DataMapper\InvoiceItem; -use App\Events\Invoice\InvoiceWasEmailed; -use App\Jobs\Entity\EmailEntity; -use App\Jobs\Ninja\TransactionLog; -use App\Libraries\MultiDB; -use App\Models\Invoice; -use App\Models\TransactionEvent; use App\Utils\Ninja; -use App\Utils\Traits\MakesDates; -use App\Utils\Traits\MakesReminders; +use App\Models\Invoice; +use App\Libraries\MultiDB; use Illuminate\Bus\Queueable; +use Illuminate\Support\Carbon; +use App\DataMapper\InvoiceItem; +use App\Jobs\Entity\EmailEntity; +use App\Models\TransactionEvent; +use App\Utils\Traits\MakesDates; +use App\Jobs\Ninja\TransactionLog; +use Illuminate\Support\Facades\App; +use App\Utils\Traits\MakesReminders; +use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\InteractsWithQueue; +use App\Events\Invoice\InvoiceWasEmailed; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\App; +use App\Events\Invoice\InvoiceReminderWasEmailed; class ReminderJob implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesReminders, MakesDates; + use Dispatchable; + use InteractsWithQueue; + use Queueable; + use SerializesModels; + use MakesReminders; + use MakesDates; public $tries = 1; @@ -44,7 +50,7 @@ class ReminderJob implements ShouldQueue * * @return void */ - public function handle() :void + public function handle(): void { set_time_limit(0); @@ -73,7 +79,7 @@ class ReminderJob implements ShouldQueue }); } else { //multiDB environment, need to - + foreach (MultiDB::$dbs as $db) { MultiDB::setDB($db); @@ -94,7 +100,7 @@ class ReminderJob implements ShouldQueue }) ->with('invitations')->chunk(50, function ($invoices) { // if ($invoice->refresh() && $invoice->isPayable()) { - + foreach ($invoices as $invoice) { $this->sendReminderForInvoice($invoice); } @@ -125,22 +131,17 @@ class ReminderJob implements ShouldQueue $enabled_reminder = 'enable_reminder_endless'; } - //check if this reminder needs to be emailed - //15-01-2022 - insert addition if block if send_reminders is definitely set if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) && $invoice->client->getSetting($enabled_reminder) && $invoice->client->getSetting('send_reminders') && (Ninja::isSelfHost() || $invoice->company->account->isPaidHostedClient())) { $invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) { if ($invitation->contact && !$invitation->contact->trashed() && $invitation->contact->email) { - EmailEntity::dispatch($invitation, $invitation->company, $reminder_template)->delay(now()->addSeconds(3)); + EmailEntity::dispatch($invitation, $invitation->company, $reminder_template); nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}"); + $invoice->entityEmailEvent($invitation, $reminder_template); } }); - - if ($invoice->invitations->count() > 0) { - event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template)); - } } $invoice->service()->setReminder()->save(); } else { @@ -156,7 +157,7 @@ class ReminderJob implements ShouldQueue * @param string $template * @return Invoice */ - private function calcLateFee($invoice, $template) :Invoice + private function calcLateFee($invoice, $template): Invoice { $late_fee_amount = 0; $late_fee_percent = 0; @@ -196,7 +197,7 @@ class ReminderJob implements ShouldQueue * * @return Invoice */ - private function setLateFee($invoice, $amount, $percent) :Invoice + private function setLateFee($invoice, $amount, $percent): Invoice { App::forgetInstance('translator'); $t = app('translator'); @@ -217,7 +218,7 @@ class ReminderJob implements ShouldQueue $fee += round($invoice->balance * $percent / 100, 2); } - $invoice_item = new InvoiceItem; + $invoice_item = new InvoiceItem(); $invoice_item->type_id = '5'; $invoice_item->product_key = trans('texts.fee'); $invoice_item->notes = ctrans('texts.late_fee_added', ['date' => $this->translateDate(now()->startOfDay(), $invoice->client->date_format(), $invoice->client->locale())]); diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 4b897ff62181..ef381eb035d6 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -721,7 +721,7 @@ class Invoice extends BaseModel return 0; } - public function entityEmailEvent($invitation, $reminder_template, $template) + public function entityEmailEvent($invitation, $reminder_template, $template = '') { switch ($reminder_template) { case 'invoice': @@ -737,6 +737,7 @@ class Invoice extends BaseModel event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), Activity::INVOICE_REMINDER3_SENT)); break; case 'reminder_endless': + case 'endless_reminder': event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), Activity::INVOICE_REMINDER_ENDLESS_SENT)); break; default: From 200bcd80b7a884fa948e6776bb82cb1aacdbde26 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 17 Mar 2023 17:36:49 +1100 Subject: [PATCH 08/12] Add Payment Emailed Activity --- .../Payment/PaymentEmailedActivity.php | 26 ++++++++++++++----- app/Models/Activity.php | 2 ++ app/Providers/EventServiceProvider.php | 3 ++- app/Services/Payment/SendEmail.php | 4 +++ lang/en/texts.php | 2 ++ 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/Listeners/Payment/PaymentEmailedActivity.php b/app/Listeners/Payment/PaymentEmailedActivity.php index af6701c4d486..5fcf3e765abd 100644 --- a/app/Listeners/Payment/PaymentEmailedActivity.php +++ b/app/Listeners/Payment/PaymentEmailedActivity.php @@ -11,33 +11,47 @@ namespace App\Listeners\Payment; +use App\Models\Activity; use App\Libraries\MultiDB; -use App\Utils\Traits\Notifications\UserNotifies; +use App\Repositories\ActivityRepository; use Illuminate\Contracts\Queue\ShouldQueue; +use App\Utils\Traits\Notifications\UserNotifies; class PaymentEmailedActivity implements ShouldQueue { - use UserNotifies; + protected $activity_repo; /** * Create the event listener. * - * @return void + * @param ActivityRepository $activity_repo */ - public function __construct() + public function __construct(ActivityRepository $activity_repo) { + $this->activity_repo = $activity_repo; } /** * Handle the event. * * @param object $event - * @return bool */ public function handle($event) { MultiDB::setDb($event->company->db); - $payment = $event->payment; + + $fields = new \stdClass(); + + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->payment->user_id; + + $fields->user_id = $user_id; + $fields->client_id = $event->payment->client_id; + $fields->company_id = $event->payment->company_id; + $fields->activity_type_id = Activity::PAYMENT_EMAILED; + $fields->payment_id = $event->payment->id; + + $this->activity_repo->save($fields, $event->payment, $event->event_vars); + } } diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 3d84f724cbf8..d25a3333ecf4 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -284,6 +284,8 @@ class Activity extends StaticModel const ACCEPT_PURCHASE_ORDER = 137; + const PAYMENT_EMAILED = 138; + protected $casts = [ 'is_system' => 'boolean', 'updated_at' => 'timestamp', diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index a672bafc963b..ae34c09620cd 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -171,6 +171,7 @@ use App\Listeners\Activity\VendorUpdatedActivity; use App\Listeners\Contact\UpdateContactLastLogin; use App\Listeners\Invoice\InvoiceDeletedActivity; use App\Listeners\Payment\PaymentBalanceActivity; +use App\Listeners\Payment\PaymentEmailedActivity; use App\Listeners\Quote\QuoteCreatedNotification; use App\Listeners\Quote\QuoteEmailedNotification; use App\Events\Invoice\InvoiceWasEmailedAndFailed; @@ -454,7 +455,7 @@ class EventServiceProvider extends ServiceProvider InvitationViewedListener::class, ], PaymentWasEmailed::class => [ - // PaymentEmailedActivity::class, + PaymentEmailedActivity::class, ], PaymentWasEmailedAndFailed::class => [ // PaymentEmailFailureActivity::class, diff --git a/app/Services/Payment/SendEmail.php b/app/Services/Payment/SendEmail.php index d967fd9ed34d..a16e6ea926b2 100644 --- a/app/Services/Payment/SendEmail.php +++ b/app/Services/Payment/SendEmail.php @@ -11,9 +11,11 @@ namespace App\Services\Payment; +use App\Utils\Ninja; use App\Models\Payment; use App\Models\ClientContact; use App\Jobs\Payment\EmailPayment; +use App\Events\Payment\PaymentWasEmailed; class SendEmail { @@ -36,6 +38,8 @@ class SendEmail // $invoice->invitations->each(function ($invitation) { // if (!$invitation->contact->trashed() && $invitation->contact->email) { EmailPayment::dispatch($this->payment, $this->payment->company, $this->contact); + + event(new PaymentWasEmailed($this->payment, $this->payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); // } // }); // }); diff --git a/lang/en/texts.php b/lang/en/texts.php index a76a4df9b1c7..9cf72a3d8cf7 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5024,6 +5024,8 @@ $LANG = array( 'number_of_payments' => 'Number of payments', 'number_of_payments_helper' => 'The number of times this payment will be made', 'pre_payment_indefinitely' => 'Continue until cancelled', + 'notification_payment_emailed' => 'Payment :payment was emailed to :client', + 'notification_payment_emailed_subject' => 'Payment :payment was emailed', ); From ab2362e8741cadce12e6acc0ab9841964488fa61 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 17 Mar 2023 18:27:26 +1100 Subject: [PATCH 09/12] Retry webhooks --- app/Http/Controllers/BaseController.php | 2 - app/Http/Controllers/WebhookController.php | 45 +++++++++++++++---- .../Requests/Webhook/RetryWebhookRequest.php | 35 +++++++++++++++ app/Jobs/Util/WebhookSingle.php | 4 +- lang/en/texts.php | 1 + routes/api.php | 1 + tests/Feature/WebhookAPITest.php | 34 ++++++++++++++ 7 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 app/Http/Requests/Webhook/RetryWebhookRequest.php diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 9afcc12c7155..86dc368934c1 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -913,7 +913,6 @@ class BaseController extends Controller * List response * * @param mixed $query - * @return void */ protected function listResponse($query) { @@ -1010,7 +1009,6 @@ class BaseController extends Controller * Item Response * * @param mixed $item - * @return void */ protected function itemResponse($item) { diff --git a/app/Http/Controllers/WebhookController.php b/app/Http/Controllers/WebhookController.php index f77fef636efb..f33c3ea0674b 100644 --- a/app/Http/Controllers/WebhookController.php +++ b/app/Http/Controllers/WebhookController.php @@ -12,19 +12,22 @@ namespace App\Http\Controllers; +use App\Models\Webhook; +use Illuminate\Support\Str; +use Illuminate\Http\Response; use App\Factory\WebhookFactory; use App\Filters\WebhookFilters; -use App\Http\Requests\Webhook\CreateWebhookRequest; -use App\Http\Requests\Webhook\DestroyWebhookRequest; -use App\Http\Requests\Webhook\EditWebhookRequest; -use App\Http\Requests\Webhook\ShowWebhookRequest; -use App\Http\Requests\Webhook\StoreWebhookRequest; -use App\Http\Requests\Webhook\UpdateWebhookRequest; -use App\Models\Webhook; +use App\Utils\Traits\MakesHash; +use App\Jobs\Util\WebhookSingle; use App\Repositories\BaseRepository; use App\Transformers\WebhookTransformer; -use App\Utils\Traits\MakesHash; -use Illuminate\Http\Response; +use App\Http\Requests\Webhook\EditWebhookRequest; +use App\Http\Requests\Webhook\ShowWebhookRequest; +use App\Http\Requests\Webhook\RetryWebhookRequest; +use App\Http\Requests\Webhook\StoreWebhookRequest; +use App\Http\Requests\Webhook\CreateWebhookRequest; +use App\Http\Requests\Webhook\UpdateWebhookRequest; +use App\Http\Requests\Webhook\DestroyWebhookRequest; class WebhookController extends BaseController { @@ -487,4 +490,28 @@ class WebhookController extends BaseController return $this->listResponse(Webhook::withTrashed()->whereIn('id', $this->transformKeys($ids))); } + + public function retry(RetryWebhookRequest $request, Webhook $webhook) + { + match($request->entity) { + 'invoice' => $includes ='client', + 'payment' => $includes ='invoices,client', + 'project' => $includes ='client', + 'purchase_order' => $includes ='vendor', + 'quote' => $includes ='client', + default => $includes = '' + }; + + $class = 'App\Models\\'.ucfirst(Str::camel($request->entity)); + + $entity = $class::withTrashed()->where('id', $this->decodePrimaryKey($request->entity_id))->company()->first(); + + if(!$entity){ + return response()->json(['message' => ctrans('texts.record_not_found')], 400); + } + + WebhookSingle::dispatchSync($webhook->id, $entity, auth()->user()->company()->db, $includes); + + return $this->itemResponse($webhook); + } } diff --git a/app/Http/Requests/Webhook/RetryWebhookRequest.php b/app/Http/Requests/Webhook/RetryWebhookRequest.php new file mode 100644 index 000000000000..5b98c06ab233 --- /dev/null +++ b/app/Http/Requests/Webhook/RetryWebhookRequest.php @@ -0,0 +1,35 @@ +user()->isAdmin(); + } + + public function rules() + { + return [ + 'entity' => 'required|bail|in:client,credit,invoice,product,task,payment,quote,purchase_order,expense,project,vendor', + 'entity_id' => 'required|bail|string', + ]; + } +} diff --git a/app/Jobs/Util/WebhookSingle.php b/app/Jobs/Util/WebhookSingle.php index 84398b00ee1a..ab5bd5af1710 100644 --- a/app/Jobs/Util/WebhookSingle.php +++ b/app/Jobs/Util/WebhookSingle.php @@ -104,8 +104,10 @@ class WebhookSingle implements ShouldQueue $resource = new Item($this->entity, $transformer, $this->entity->getEntityType()); $data = $manager->createData($resource)->toArray(); + + $headers = is_array($subscription->headers) ? $subscription->headers : []; - $this->postData($subscription, $data, $subscription->headers); + $this->postData($subscription, $data, $headers); } private function postData($subscription, $data, $headers = []) diff --git a/lang/en/texts.php b/lang/en/texts.php index 9cf72a3d8cf7..aad041a416b3 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5026,6 +5026,7 @@ $LANG = array( 'pre_payment_indefinitely' => 'Continue until cancelled', 'notification_payment_emailed' => 'Payment :payment was emailed to :client', 'notification_payment_emailed_subject' => 'Payment :payment was emailed', + 'record_not_found' => 'Record not found', ); diff --git a/routes/api.php b/routes/api.php index daf2db3a84a1..aa90773fe12e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -336,6 +336,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::resource('webhooks', WebhookController::class); Route::post('webhooks/bulk', [WebhookController::class, 'bulk'])->name('webhooks.bulk'); + Route::post('webhooks/{webhook}/retry', [WebhookController::class, 'retry'])->name('webhooks.retry'); /*Subscription and Webhook routes */ // Route::post('hooks', [SubscriptionController::class, 'subscribe'])->name('hooks.subscribe'); diff --git a/tests/Feature/WebhookAPITest.php b/tests/Feature/WebhookAPITest.php index a2ed5c54f3a3..2d62be234cab 100644 --- a/tests/Feature/WebhookAPITest.php +++ b/tests/Feature/WebhookAPITest.php @@ -11,6 +11,7 @@ namespace Tests\Feature; +use App\Jobs\Util\WebhookSingle; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -45,6 +46,39 @@ class WebhookAPITest extends TestCase $this->withoutExceptionHandling(); } + public function testWebhookRetry() + { + + $data = [ + 'target_url' => 'http://hook.com', + 'event_id' => 1, //create client + 'format' => 'JSON', + 'headers' => [] + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson("/api/v1/webhooks", $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $data = [ + 'entity' => 'client', + 'entity_id' => $this->client->hashed_id, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson("/api/v1/webhooks/".$arr['data']['id']."/retry", $data); + + $response->assertStatus(200); + + } + public function testWebhookGetFilter() { $response = $this->withHeaders([ From dc2353db048b56b966658a5479037c80554edb78 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 17 Mar 2023 19:41:38 +1100 Subject: [PATCH 10/12] Fixes for html entities in subject --- app/Mail/Engine/PaymentEmailEngine.php | 7 +++++++ app/Mail/TemplateEmail.php | 2 +- app/Services/Email/EmailMailable.php | 2 +- app/Utils/VendorHtmlEngine.php | 7 +++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/Mail/Engine/PaymentEmailEngine.php b/app/Mail/Engine/PaymentEmailEngine.php index 1b274fca2353..215949837570 100644 --- a/app/Mail/Engine/PaymentEmailEngine.php +++ b/app/Mail/Engine/PaymentEmailEngine.php @@ -388,6 +388,13 @@ class PaymentEmailEngine extends BaseEmailEngine */ private function buildViewButton(string $link, string $text): string { + + + if ($this->settings->email_style == 'plain') { + return ''. $text .''; + } + + return '