Merge pull request #8384 from turbo124/v5-develop

v5.5.95
This commit is contained in:
David Bomba 2023-03-17 20:00:56 +11:00 committed by GitHub
commit 8320021e08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 338 additions and 234 deletions

View File

@ -1 +1 @@
5.5.94
5.5.95

View File

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

View File

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

View File

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

View File

@ -80,6 +80,7 @@ class PrePaymentController extends Controller
$invoice = $invoice_repo->save($data, $invoice)
->service()
->markSent()
->applyNumber()
->fillDefaults()
->save();
@ -107,8 +108,10 @@ 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 == 'on' ? true : false,
];
return $this->render('invoices.payment', $data);

View File

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

View File

@ -0,0 +1,35 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Webhook;
use App\Http\Requests\Request;
class RetryWebhookRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return auth()->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',
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,62 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\Subscription;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\Company;
use App\Notifications\Ninja\RenewalFailureNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Imdhemy\Purchases\Events\AppStore\DidRenew;
class AppStoreRenewSubscription implements ShouldQueue
{
/**
* Create the event listener.
*
* @param ActivityRepository $activity_repo
*/
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle(DidRenew $event)
{
$inapp_transaction_id = $event->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();
}
}

View File

@ -1,49 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\Subscription;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\Company;
use App\Notifications\Ninja\RenewalFailureNotification;
use Carbon\Carbon;
use Illuminate\Contracts\Queue\ShouldQueue;
use Imdhemy\Purchases\Events\GooglePlay\SubscriptionRenewed;
class PlayStoreRenewSubscription implements ShouldQueue
{
public function handle(SubscriptionRenewed $event)
{
$notification = $event->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;
}
}
}

View File

@ -388,6 +388,13 @@ class PaymentEmailEngine extends BaseEmailEngine
*/
private function buildViewButton(string $link, string $text): string
{
if ($this->settings->email_style == 'plain') {
return '<a href="'. $link .'" target="_blank">'. $text .'</a>';
}
return '
<div>
<!--[if (gte mso 9)|(IE)]>

View File

@ -116,7 +116,7 @@ class TemplateEmail extends Mailable
}//remove whitespace if any has been inserted.
}
$this->subject($this->build_email->getSubject())
$this->subject(str_replace("<br>", "", $this->build_email->getSubject()))
->text('email.template.text', [
'text_body' => $this->build_email->getTextBody(),
'whitelabel' => $this->client->user->account->isPaid() ? true : false,

View File

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

View File

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

View File

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

View File

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

View File

@ -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'));
@ -256,6 +266,8 @@ class InstantPayment
'payment_method_id' => $payment_method_id,
'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) {

View File

@ -41,7 +41,7 @@ class EmailMailable extends Mailable
public function envelope()
{
return new Envelope(
subject: $this->email_object->subject,
subject: str_replace("<br>","",$this->email_object->subject),
tags: [$this->email_object->company_key],
replyTo: $this->email_object->reply_to,
from: $this->email_object->from,

View File

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

View File

@ -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,
]);

View File

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

View File

@ -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,36 @@ 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->frequency_id ?: 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->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();
$recurring_invoice->service()->applyNumber()->save();
}
return;
}
if (strlen($invoice->number) > 1 && str_starts_with($invoice->number, "####"))
$invoice->number = '';

View File

@ -49,7 +49,8 @@ class TriggeredActions extends AbstractService
$this->quote = $this->quote->service()->convert()->save();
}
if ($this->request->has('approve') && $this->request->input('approve') == 'true' && in_array($this->quote->status_id, [Quote::STATUS_SENT, Quote::STATUS_DRAFT])) {
// if ($this->request->has('approve') && $this->request->input('approve') == 'true' && in_array($this->quote->status_id, [Quote::STATUS_SENT, Quote::STATUS_DRAFT])) {
if ($this->request->has('approve') && $this->request->input('approve') == 'true') {
$this->quote = $this->quote->service()->approveWithNoCoversion()->save();
}

View File

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

View File

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

View File

@ -808,6 +808,13 @@ html {
*/
private function buildViewButton(string $link, string $text): string
{
if ($this->settings->email_style == 'plain') {
return '<a href="'. $link .'" target="_blank">'. $text .'</a>';
}
return '
<div>
<!--[if (gte mso 9)|(IE)]>

View File

@ -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] : [],
],

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.5.94',
'app_tag' => '5.5.94',
'app_version' => '5.5.95',
'app_tag' => '5.5.95',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),
@ -213,4 +213,5 @@ return [
'config_name' => env("YODLEE_CONFIG_NAME", false),
],
'licenses' => env('LICENSES',false),
'google_application_credentials' => env("GOOGLE_APPLICATION_CREDENTIALS", false),
];

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('recurring_invoices', function (Blueprint $table) {
$table->boolean('is_proforma')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
};

View File

@ -5021,7 +5021,12 @@ $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',
'notification_payment_emailed' => 'Payment :payment was emailed to :client',
'notification_payment_emailed_subject' => 'Payment :payment was emailed',
'record_not_found' => 'Record not found',
);

View File

@ -21,6 +21,10 @@
$checked_off = '';
}
if (isset($pre_payment) && $pre_payment == '1' && isset($is_recurring) && $is_recurring == '1') {
$token_billing_string = 'true';
}
@endphp
@if($token_billing)

View File

@ -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' && isset($is_recurring) && $is_recurring == '1') {
$token_billing_string = 'true';
}
@endphp

View File

@ -14,6 +14,9 @@
<input type="hidden" name="payment_method_id" id="payment_method_id">
<input type="hidden" name="signature">
<input type="hidden" name="pre_payment" value="{{ isset($pre_payment) ? $pre_payment : false }}">
<input type="hidden" name="is_recurring" value="{{ isset($is_recurring) ? $is_recurring : false }}">
<input type="hidden" name="frequency_id" value="{{ isset($frequency_id) ? $frequency_id : false }}">
<input type="hidden" name="remaining_cycles" value="{{ isset($remaining_cycles) ? $remaining_cycles : false }}">
<div class="container mx-auto">
<div class="grid grid-cols-6 gap-4">

View File

@ -56,15 +56,15 @@
@endcomponent
<div x-cloak x-show="show">
@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')])
<select name="remaining_cycles" class="form-select input w-full bg-white">
<option value="-1" selected>{{ ctrans('texts.freq_indefinitely')}}</option>
<option value="-1" selected>{{ ctrans('texts.pre_payment_indefinitely')}}</option>
@for($i = 1; $i < 60; $i++)
<option value={{$i}}>{{$i}}</option>
@endfor
</select>
<span class="py-2">
<label for="remaining_cycles" class="col-form-label text-center col-lg-3 text-gray-900">{{ ctrans ('texts.client_remaining_cycles_helper')}}</label>
<label for="remaining_cycles" class="col-form-label text-center col-lg-3 text-gray-900">{{ ctrans ('texts.number_of_payments_helper')}}</label>
</span>
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.frequency')])
@ -73,14 +73,14 @@
<option value="2">{{ ctrans('texts.freq_weekly') }}</option>
<option value="3">{{ ctrans('texts.freq_two_weeks') }}</option>
<option value="4">{{ ctrans('texts.freq_four_weeks') }}</option>
<option value="5">{{ ctrans('texts.freq_monthly') }}</option>
<option value="5" selected>{{ ctrans('texts.freq_monthly') }}</option>
<option value="6">{{ ctrans('texts.freq_two_months') }}</option>
<option value="7">{{ ctrans('texts.freq_three_months') }}</option>
<option value="8">{{ ctrans('texts.') }}</option>
<option value="9">{{ ctrans('texts.') }}</option>
<option value="10">{{ ctrans('texts.') }}</option>
<option value="11">{{ ctrans('texts.') }}</option>
<option value="12">{{ ctrans('texts.') }}</option>
<option value="8">{{ ctrans('texts.freq_four_months') }}</option>
<option value="9">{{ ctrans('texts.freq_six_months') }}</option>
<option value="10">{{ ctrans('texts.freq_annually') }}</option>
<option value="11">{{ ctrans('texts.freq_two_years') }}</option>
<option value="12">{{ ctrans('texts.freq_three_years') }}</option>
</select>
@endcomponent
</div>

View File

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

View File

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