diff --git a/VERSION.txt b/VERSION.txt index 4d0120e6ec17..0eb5fb01a6b0 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.3.49 \ No newline at end of file +5.3.51 \ No newline at end of file diff --git a/app/Helpers/Mail/GmailTransport.php b/app/Helpers/Mail/GmailTransport.php index 3c929f00d9ba..80fa507052f0 100644 --- a/app/Helpers/Mail/GmailTransport.php +++ b/app/Helpers/Mail/GmailTransport.php @@ -76,7 +76,24 @@ class GmailTransport extends Transport } - $this->gmail->send(); + /** + * Google is very strict with their + * sending limits, if we hit 429s, sleep and + * retry again later. + */ + try{ + + $this->gmail->send(); + + } + catch(\Google\Service\Exception $e) + { + nlog("gmail exception"); + nlog($e->getErrors()); + + sleep(5); + $this->gmail->send(); + } $this->sendPerformed($message); diff --git a/app/Http/Controllers/Auth/ContactLoginController.php b/app/Http/Controllers/Auth/ContactLoginController.php index 18fbf462dc0d..eb64a0ac41e4 100644 --- a/app/Http/Controllers/Auth/ContactLoginController.php +++ b/app/Http/Controllers/Auth/ContactLoginController.php @@ -152,6 +152,7 @@ class ContactLoginController extends Controller public function logout() { Auth::guard('contact')->logout(); + request()->session()->invalidate(); return redirect('/client/login'); } diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index 24ff67640387..0748afca7a2f 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -98,6 +98,7 @@ class InvitationController extends Controller $client_contact->email = Str::random(15) . "@example.com"; $client_contact->save(); if (request()->has('client_hash') && request()->input('client_hash') == $invitation->contact->client->client_hash) { + request()->session()->invalidate(); auth()->guard('contact')->loginUsingId($client_contact->id, true); } elseif ((bool) $invitation->contact->client->getSetting('enable_client_portal_password') !== false) { @@ -106,6 +107,7 @@ class InvitationController extends Controller } else { nlog("else - default - login contact"); + request()->session()->invalidate(); auth()->guard('contact')->loginUsingId($client_contact->id, true); } diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index 0e42d64f47d4..220276dd784c 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -90,12 +90,15 @@ class PaymentController extends Controller public function response(PaymentResponseRequest $request) { + $gateway = CompanyGateway::findOrFail($request->input('company_gateway_id')); - - $payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->payment_hash])->first(); + $payment_hash = PaymentHash::where('hash', $request->payment_hash)->first(); + $invoice = Invoice::with('client')->find($payment_hash->fee_invoice_id); + $client = $invoice ? $invoice->client : auth()->user()->client; return $gateway - ->driver(auth()->user()->client) + // ->driver(auth()->user()->client) + ->driver($client) ->setPaymentMethod($request->input('payment_method_id')) ->setPaymentHash($payment_hash) ->checkRequirements() diff --git a/app/Http/Controllers/DesignController.php b/app/Http/Controllers/DesignController.php index e66e98f0324a..dfb1f50e3928 100644 --- a/app/Http/Controllers/DesignController.php +++ b/app/Http/Controllers/DesignController.php @@ -401,7 +401,7 @@ class DesignController extends BaseController } $design->design = $d; - $design->save(); + // $design->save(); /* This is required as the base template does not know to inject the table elements diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index 867646fdc0a2..5836d2091c21 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -18,6 +18,7 @@ use App\Factory\CloneQuoteToInvoiceFactory; use App\Factory\QuoteFactory; use App\Filters\QuoteFilters; use App\Http\Requests\Quote\ActionQuoteRequest; +use App\Http\Requests\Quote\BulkActionQuoteRequest; use App\Http\Requests\Quote\CreateQuoteRequest; use App\Http\Requests\Quote\DestroyQuoteRequest; use App\Http\Requests\Quote\EditQuoteRequest; @@ -510,7 +511,7 @@ class QuoteController extends BaseController * ), * ) */ - public function bulk() + public function bulk(BulkActionQuoteRequest $request) { $action = request()->input('action'); diff --git a/app/Http/Livewire/BillingPortalPurchase.php b/app/Http/Livewire/BillingPortalPurchase.php index e6a5de79c66e..cf97db87d771 100644 --- a/app/Http/Livewire/BillingPortalPurchase.php +++ b/app/Http/Livewire/BillingPortalPurchase.php @@ -454,6 +454,7 @@ class BillingPortalPurchase extends Component $contact = ClientContact::query() ->where('email', $this->email) + ->where('company_id', $this->subscription->company_id) ->first(); $mailer = new NinjaMailerObject(); diff --git a/app/Http/Middleware/ContactKeyLogin.php b/app/Http/Middleware/ContactKeyLogin.php index c590bdb4dd3b..a40b423d91d0 100644 --- a/app/Http/Middleware/ContactKeyLogin.php +++ b/app/Http/Middleware/ContactKeyLogin.php @@ -37,6 +37,7 @@ class ContactKeyLogin { if (Auth::guard('contact')->check()) { Auth::guard('contact')->logout(); + $request->session()->invalidate(); } if ($request->segment(2) && $request->segment(2) == 'magic_link' && $request->segment(3)) { diff --git a/app/Http/Middleware/SessionDomains.php b/app/Http/Middleware/SessionDomains.php index d24ab7955796..0881aad8ed0f 100644 --- a/app/Http/Middleware/SessionDomains.php +++ b/app/Http/Middleware/SessionDomains.php @@ -35,7 +35,7 @@ class SessionDomains if (strpos($domain_name, 'invoicing.co') !== false) { - config(['session.domain' => '.invoicing.co']); + // config(['session.domain' => '.invoicing.co']); } else{ diff --git a/app/Http/Requests/Expense/UpdateExpenseRequest.php b/app/Http/Requests/Expense/UpdateExpenseRequest.php index a4646ef73efd..8b8c668d5af9 100644 --- a/app/Http/Requests/Expense/UpdateExpenseRequest.php +++ b/app/Http/Requests/Expense/UpdateExpenseRequest.php @@ -34,10 +34,10 @@ class UpdateExpenseRequest extends Request public function rules() { /* Ensure we have a client name, and that all emails are unique*/ + $rules = []; + // $rules['country_id'] = 'integer|nullable'; - $rules['country_id'] = 'integer|nullable'; - - $rules['contacts.*.email'] = 'nullable|distinct'; + // $rules['contacts.*.email'] = 'nullable|distinct'; if (isset($this->number)) { $rules['number'] = Rule::unique('expenses')->where('company_id', auth()->user()->company()->id)->ignore($this->expense->id); diff --git a/app/Http/Requests/Quote/BulkActionQuoteRequest.php b/app/Http/Requests/Quote/BulkActionQuoteRequest.php new file mode 100644 index 000000000000..330ed304fe77 --- /dev/null +++ b/app/Http/Requests/Quote/BulkActionQuoteRequest.php @@ -0,0 +1,41 @@ +all(); + + $rules = []; + + if($input['action'] == 'convert_to_invoice') + $rules['action'] = [new ConvertableQuoteRule()]; + + return $rules; + } + +} diff --git a/app/Http/ValidationRules/PaymentAppliedValidAmount.php b/app/Http/ValidationRules/PaymentAppliedValidAmount.php index 357cde9f0ea0..7c8031e93f79 100644 --- a/app/Http/ValidationRules/PaymentAppliedValidAmount.php +++ b/app/Http/ValidationRules/PaymentAppliedValidAmount.php @@ -74,6 +74,7 @@ class PaymentAppliedValidAmount implements Rule } } - return $payment_amounts >= $invoice_amounts; + // nlog("{round($payment_amounts,3)} >= {round($invoice_amounts,3)}"); + return round($payment_amounts,3) >= round($invoice_amounts,3); } } diff --git a/app/Http/ValidationRules/Quote/ConvertableQuoteRule.php b/app/Http/ValidationRules/Quote/ConvertableQuoteRule.php new file mode 100644 index 000000000000..a4c8074a102f --- /dev/null +++ b/app/Http/ValidationRules/Quote/ConvertableQuoteRule.php @@ -0,0 +1,67 @@ +checkQuoteIsConvertable(); //if it exists, return false! + } + + /** + * @return string + */ + public function message() + { + return ctrans('texts.quote_has_expired'); + } + + /** + * @return bool + */ + private function checkQuoteIsConvertable() : bool + { + $ids = request()->input('ids'); + + $quotes = Quote::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get(); + + foreach($quotes as $quote){ + + if(!$quote->service()->isConvertable()) + return false; + + } + + return true; + + } +} diff --git a/app/Import/Transformers/Csv/InvoiceTransformer.php b/app/Import/Transformers/Csv/InvoiceTransformer.php index dfd6d8de2e2c..b6fee5536319 100644 --- a/app/Import/Transformers/Csv/InvoiceTransformer.php +++ b/app/Import/Transformers/Csv/InvoiceTransformer.php @@ -45,7 +45,7 @@ class InvoiceTransformer extends BaseTransformer { 'client_id' => $this->getClient( $this->getString( $invoice_data, 'client.name' ), $this->getString( $invoice_data, 'client.email' ) ), 'discount' => $this->getFloat( $invoice_data, 'invoice.discount' ), 'po_number' => $this->getString( $invoice_data, 'invoice.po_number' ), - 'date' => isset( $invoice_data['invoice.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['invoice.date'] ) ) : null, + 'date' => isset( $invoice_data['invoice.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['invoice.date'] ) ) : now()->format('Y-m-d'), 'due_date' => isset( $invoice_data['invoice.due_date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['invoice.due_date'] ) ) : null, 'terms' => $this->getString( $invoice_data, 'invoice.terms' ), 'public_notes' => $this->getString( $invoice_data, 'invoice.public_notes' ), @@ -72,7 +72,7 @@ class InvoiceTransformer extends BaseTransformer { 'status_id' => $invoiceStatusMap[ $status = strtolower( $this->getString( $invoice_data, 'invoice.status' ) ) ] ?? Invoice::STATUS_SENT, - 'viewed' => $status === 'viewed', + // 'viewed' => $status === 'viewed', 'archived' => $status === 'archived', ]; diff --git a/app/Import/Transformers/Waveaccounting/InvoiceTransformer.php b/app/Import/Transformers/Waveaccounting/InvoiceTransformer.php index 701e4c6ff6f5..4d83651a58bc 100644 --- a/app/Import/Transformers/Waveaccounting/InvoiceTransformer.php +++ b/app/Import/Transformers/Waveaccounting/InvoiceTransformer.php @@ -35,7 +35,8 @@ class InvoiceTransformer extends BaseTransformer { 'company_id' => $this->maps['company']->id, 'client_id' => $this->getClient( $customer_name = $this->getString( $invoice_data, 'Customer' ), null ), 'number' => $invoice_number = $this->getString( $invoice_data, 'Invoice Number' ), - 'date' => isset( $invoice_data['Invoice Date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['Transaction Date'] ) ) : null, + 'date' => date( 'Y-m-d', strtotime( $invoice_data['Transaction Date'] ) ) ?: now()->format('Y-m-d'), //27-01-2022 + // 'date' => isset( $invoice_data['Invoice Date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['Transaction Date'] ) ) : null, 'currency_id' => $this->getCurrencyByCode( $invoice_data, 'Currency' ), 'status_id' => Invoice::STATUS_SENT, ]; diff --git a/app/Jobs/Company/CompanyImport.php b/app/Jobs/Company/CompanyImport.php index 88ee380f3cfc..5150740c8e84 100644 --- a/app/Jobs/Company/CompanyImport.php +++ b/app/Jobs/Company/CompanyImport.php @@ -1381,7 +1381,13 @@ class CompanyImport implements ShouldQueue $new_obj->company_id = $this->company->id; $new_obj->fill($obj_array); $new_obj->save(['timestamps' => false]); - $new_obj->number = $this->getNextRecurringExpenseNumber($client = Client::find($obj_array['client_id']), $new_obj); + $new_obj->number = $this->getNextRecurringExpenseNumber($new_obj); + } + elseif($class == 'App\Models\CompanyLedger'){ + $new_obj = $class::firstOrNew( + [$match_key => $obj->{$match_key}, 'company_id' => $this->company->id], + $obj_array, + ); } else{ $new_obj = $class::withTrashed()->firstOrNew( diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index dfaec8927bc3..1b8713cd1034 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -211,15 +211,15 @@ class NinjaMailerJob implements ShouldQueue } //17-01-2022 - ensure we have a token otherwise we fail gracefully to default sending engine - if(strlen($user->oauth_user_token) == 0){ - $this->nmo->settings->email_sending_method = 'default'; - return $this->setMailDriver(); - } + // if(strlen($user->oauth_user_token) == 0){ + // $this->nmo->settings->email_sending_method = 'default'; + // return $this->setMailDriver(); + // } $google->getClient()->setAccessToken(json_encode($user->oauth_user_token)); //need to slow down gmail requests otherwise we hit 429's - sleep(rand(1,3)); + sleep(rand(2,6)); } catch(\Exception $e) { $this->logMailError('Gmail Token Invalid', $this->company->clients()->first()); diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index e610d0cfca8e..cbadc3ff1808 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -97,7 +97,7 @@ class Gateway extends StaticModel ]; case 15: return [ - GatewayType::PAYPAL => ['refund' => true, 'token_billing' => false] + GatewayType::PAYPAL => ['refund' => false, 'token_billing' => false] ]; //Paypal break; case 20: @@ -140,15 +140,15 @@ class Gateway extends StaticModel GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable','charge.succeeded']], 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']], //Stripe - GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], - GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], - GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], - GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], - GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], - GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], - GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], - GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], + GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded','payment_intent.succeeded']], //Stripe + 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']], ]; break; case 57: diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 183a6d0a9108..5465f9cd0640 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -13,6 +13,7 @@ namespace App\Models; use App\Events\Payment\PaymentWasRefunded; use App\Events\Payment\PaymentWasVoided; +use App\Models\GatewayType; use App\Services\Ledger\LedgerService; use App\Services\Payment\PaymentService; use App\Utils\Ninja; @@ -148,6 +149,11 @@ class Payment extends BaseModel return $this->belongsTo(PaymentType::class); } + public function gateway_type() + { + return $this->belongsTo(GatewayType::class); + } + public function paymentables() { return $this->hasMany(Paymentable::class); diff --git a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php index ba359b259443..26a1b9d6aa48 100644 --- a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php +++ b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php @@ -156,7 +156,8 @@ class AuthorizePaymentMethod $paymentOne = new PaymentType(); $paymentOne->setOpaqueData($op); - $contact = $this->authorize->client->primary_contact()->first(); + $contact = $this->authorize->client->primary_contact()->first() ?: $this->authorize->client->contacts()->first(); + $billto = false; if ($contact) { diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index bd232b655d41..5e6cf9ae0958 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -418,12 +418,15 @@ class BaseDriver extends AbstractPaymentDriver throw new PaymentFailed($error, $e->getCode()); } - public function sendFailureMail($error = '') + public function sendFailureMail($error) { if (!is_null($this->payment_hash)) { $this->unWindGatewayFees($this->payment_hash); } + + if(!$error) + $error = ''; PaymentFailedMailer::dispatch( $this->payment_hash, diff --git a/app/PaymentDrivers/Stripe/ACSS.php b/app/PaymentDrivers/Stripe/ACSS.php index f82b4782de1f..cab2b571133d 100644 --- a/app/PaymentDrivers/Stripe/ACSS.php +++ b/app/PaymentDrivers/Stripe/ACSS.php @@ -143,6 +143,10 @@ class ACSS 'payment_method_types' => ['acss_debit'], 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::ACSS, + ], 'payment_method_options' => [ 'acss_debit' => [ 'mandate_options' => [ @@ -150,7 +154,6 @@ class ACSS 'interval_description' => 'when any invoice becomes due', 'transaction_type' => 'personal' // TODO: check if is company or personal https://stripe.com/docs/payments/acss-debit ], - 'currency' => $this->stripe->client->currency()->code, ] ] ], $this->stripe->stripe_connect_auth); diff --git a/app/PaymentDrivers/Stripe/ApplePay.php b/app/PaymentDrivers/Stripe/ApplePay.php index 658900cd5d47..36e925c5f0a1 100644 --- a/app/PaymentDrivers/Stripe/ApplePay.php +++ b/app/PaymentDrivers/Stripe/ApplePay.php @@ -48,6 +48,10 @@ class ApplePay $data['intent'] = \Stripe\PaymentIntent::create([ 'amount' => $data['stripe_amount'], 'currency' => $this->stripe_driver->client->getCurrencyCode(), + 'metadata' => [ + 'payment_hash' => $this->stripe_driver->payment_hash->hash, + 'gateway_type_id' => GatewayType::APPLE_PAY, + ], ], $this->stripe_driver->stripe_connect_auth); $this->stripe_driver->payment_hash->data = array_merge((array) $this->stripe_driver->payment_hash->data, ['stripe_amount' => $data['stripe_amount']]); diff --git a/app/PaymentDrivers/Stripe/BECS.php b/app/PaymentDrivers/Stripe/BECS.php index a225dc3f214d..11e550601083 100644 --- a/app/PaymentDrivers/Stripe/BECS.php +++ b/app/PaymentDrivers/Stripe/BECS.php @@ -56,7 +56,10 @@ class BECS 'setup_future_usage' => 'off_session', 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), - + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::BECS, + ], ], $this->stripe->stripe_connect_auth); $data['pi_client_secret'] = $intent->client_secret; diff --git a/app/PaymentDrivers/Stripe/Bancontact.php b/app/PaymentDrivers/Stripe/Bancontact.php index 303dfa41d2e5..4c23fd8e2dba 100644 --- a/app/PaymentDrivers/Stripe/Bancontact.php +++ b/app/PaymentDrivers/Stripe/Bancontact.php @@ -52,6 +52,10 @@ class Bancontact 'payment_method_types' => ['bancontact'], 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::BANCONTACT, + ], ], $this->stripe->stripe_connect_auth); diff --git a/app/PaymentDrivers/Stripe/BrowserPay.php b/app/PaymentDrivers/Stripe/BrowserPay.php index b5ba9c7ca57f..7aa18353d762 100644 --- a/app/PaymentDrivers/Stripe/BrowserPay.php +++ b/app/PaymentDrivers/Stripe/BrowserPay.php @@ -71,6 +71,10 @@ class BrowserPay implements MethodInterface 'currency' => $this->stripe->client->getCurrencyCode(), 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::APPLE_PAY, + ], ]; $data['gateway'] = $this->stripe; diff --git a/app/PaymentDrivers/Stripe/Charge.php b/app/PaymentDrivers/Stripe/Charge.php index 351399841c4e..222390eeb2fb 100644 --- a/app/PaymentDrivers/Stripe/Charge.php +++ b/app/PaymentDrivers/Stripe/Charge.php @@ -78,6 +78,10 @@ class Charge 'customer' => $cgt->gateway_customer_reference, 'confirm' => true, 'description' => $description, + 'metadata' => [ + 'payment_hash' => $payment_hash->hash, + 'gateway_type_id' => GatewayType::CREDIT_CARD, + ], ]; $response = $this->stripe->createPaymentIntent($data, $this->stripe->stripe_connect_auth); diff --git a/app/PaymentDrivers/Stripe/CreditCard.php b/app/PaymentDrivers/Stripe/CreditCard.php index 1959cc7a05b1..4fb3691010d7 100644 --- a/app/PaymentDrivers/Stripe/CreditCard.php +++ b/app/PaymentDrivers/Stripe/CreditCard.php @@ -63,10 +63,13 @@ class CreditCard 'currency' => $this->stripe->client->getCurrencyCode(), 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::CREDIT_CARD, + ], + 'setup_future_usage' => 'off_session', ]; - $payment_intent_data['setup_future_usage'] = 'off_session'; - $data['intent'] = $this->stripe->createPaymentIntent($payment_intent_data); $data['gateway'] = $this->stripe; diff --git a/app/PaymentDrivers/Stripe/EPS.php b/app/PaymentDrivers/Stripe/EPS.php index 35abb4c69a5c..4d7b3db3a8a4 100644 --- a/app/PaymentDrivers/Stripe/EPS.php +++ b/app/PaymentDrivers/Stripe/EPS.php @@ -52,7 +52,10 @@ class EPS 'payment_method_types' => ['eps'], 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), - + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::EPS, + ], ], $this->stripe->stripe_connect_auth); $data['pi_client_secret'] = $intent->client_secret; diff --git a/app/PaymentDrivers/Stripe/FPX.php b/app/PaymentDrivers/Stripe/FPX.php index 0bf85e48512e..8a73ce6c98d4 100644 --- a/app/PaymentDrivers/Stripe/FPX.php +++ b/app/PaymentDrivers/Stripe/FPX.php @@ -53,7 +53,10 @@ class FPX 'payment_method_types' => ['fpx'], 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), - + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::FPX, + ], ], $this->stripe->stripe_connect_auth); $data['pi_client_secret'] = $intent->client_secret; diff --git a/app/PaymentDrivers/Stripe/GIROPAY.php b/app/PaymentDrivers/Stripe/GIROPAY.php index dbbdc5f79962..e9dbb4d0cca6 100644 --- a/app/PaymentDrivers/Stripe/GIROPAY.php +++ b/app/PaymentDrivers/Stripe/GIROPAY.php @@ -52,7 +52,10 @@ class GIROPAY 'payment_method_types' => ['giropay'], 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), - + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::GIROPAY, + ], ], $this->stripe->stripe_connect_auth); $data['pi_client_secret'] = $intent->client_secret; diff --git a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php new file mode 100644 index 000000000000..f621e923d22b --- /dev/null +++ b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php @@ -0,0 +1,201 @@ +stripe_request = $stripe_request; + $this->company_key = $company_key; + $this->company_gateway_id = $company_gateway_id; + } + + public function handle() + { + // nlog($this->stripe_request); + // nlog(optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['gateway_type_id']); + // nlog(optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['payment_hash']); + // nlog(optional($this->stripe_request['object']['charges']['data'][0]['payment_method_details']['card'])['brand']); + + MultiDB::findAndSetDbByCompanyKey($this->company_key); + + $company = Company::where('company_key', $this->company_key)->first(); + + foreach ($this->stripe_request as $transaction) { + + if(array_key_exists('payment_intent', $transaction)) + { + $payment = Payment::query() + ->where('company_id', $company->id) + ->where(function ($query) use ($transaction) { + $query->where('transaction_reference', $transaction['payment_intent']) + ->orWhere('transaction_reference', $transaction['id']); + }) + ->first(); + } + else + { + $payment = Payment::query() + ->where('company_id', $company->id) + ->where('transaction_reference', $transaction['id']) + ->first(); + } + + if ($payment) { + $payment->status_id = Payment::STATUS_COMPLETED; + $payment->save(); + + $this->payment_completed = true; + } + } + + + if($this->payment_completed) + return; + + + if(optional($this->stripe_request['object']['charges']['data'][0])['id']){ + + $company = Company::where('company_key', $this->company_key)->first(); + + $payment = Payment::query() + ->where('company_id', $company->id) + ->where('transaction_reference', $this->stripe_request['object']['charges']['data'][0]['id']) + ->first(); + + //return early + if($payment && $payment->status_id == Payment::STATUS_COMPLETED){ + nlog(" payment found and status correct - returning "); + return; + } + elseif($payment){ + $payment->status_id = Payment::STATUS_COMPLETED; + $payment->save(); + } + + + $hash = optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['payment_hash']; + + $payment_hash = PaymentHash::where('hash', $hash)->first(); + + nlog("no payment found"); + + if(optional($this->stripe_request['object']['charges']['data'][0]['metadata']['payment_hash']) && in_array('card', $this->stripe_request['object']['allowed_source_types'])) + { + nlog("hash found"); + + $hash = $this->stripe_request['object']['charges']['data'][0]['metadata']['payment_hash']; + + $payment_hash = PaymentHash::where('hash', $hash)->first(); + $invoice = Invoice::with('client')->find($payment_hash->fee_invoice_id); + $client = $invoice->client; + + $this->updateCreditCardPayment($payment_hash, $client); + } + + } + + } + + private function updateCreditCardPayment($payment_hash, $client) + { + $company_gateway = CompanyGateway::find($this->company_gateway_id); + $payment_method_type = optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['gateway_type_id']; + $driver = $company_gateway->driver($client)->init()->setPaymentMethod($payment_method_type); + + $payment_hash->data = array_merge((array) $payment_hash->data, $this->stripe_request); + $payment_hash->save(); + $driver->setPaymentHash($payment_hash); + + $data = [ + 'payment_method' => $payment_hash->data->object->payment_method, + 'payment_type' => PaymentType::parseCardType(strtolower(optional($this->stripe_request['object']['charges']['data'][0]['payment_method_details']['card'])['brand'])) ?: PaymentType::CREDIT_CARD_OTHER, + 'amount' => $payment_hash->data->amount_with_fee, + 'transaction_reference' => $this->stripe_request['object']['charges']['data'][0]['id'], + 'gateway_type_id' => GatewayType::CREDIT_CARD, + ]; + + $payment = $driver->createPayment($data, Payment::STATUS_COMPLETED); + + SystemLogger::dispatch( + ['response' => $this->stripe_request, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_STRIPE, + $client, + $client->company, + ); + + } + + //charge # optional($this->stripe_request['object']['charges']['data'][0])['id'] + //metadata # optional($this->stripe_request['object']['charges']['data'][0]['metadata']['gateway_type_id'] + //metadata # optional($this->stripe_request['object']['charges']['data'][0]['metadata']['payment_hash'] + + +/** + * + * $intent = \Stripe\PaymentIntent::retrieve('{{PAYMENT_INTENT_ID}}'); + $charges = $intent->charges->data; + * + * + * $payment = Payment::query() + ->where('company_id', $request->getCompany()->id) + ->where('transaction_reference', $transaction['id']) + ->first(); + + + if ($payment) { + $payment->status_id = Payment::STATUS_COMPLETED; + $payment->save(); + } + + + + + * */ + + + +} \ No newline at end of file diff --git a/app/PaymentDrivers/Stripe/PRZELEWY24.php b/app/PaymentDrivers/Stripe/PRZELEWY24.php index 64988894fc84..d93acbb9c22b 100644 --- a/app/PaymentDrivers/Stripe/PRZELEWY24.php +++ b/app/PaymentDrivers/Stripe/PRZELEWY24.php @@ -52,7 +52,10 @@ class PRZELEWY24 'payment_method_types' => ['p24'], 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), - + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::PRZELEWY24, + ], ], $this->stripe->stripe_connect_auth); $data['pi_client_secret'] = $intent->client_secret; diff --git a/app/PaymentDrivers/Stripe/SEPA.php b/app/PaymentDrivers/Stripe/SEPA.php index cc4129d6a920..ac2d0ccd81f6 100644 --- a/app/PaymentDrivers/Stripe/SEPA.php +++ b/app/PaymentDrivers/Stripe/SEPA.php @@ -54,6 +54,10 @@ class SEPA 'setup_future_usage' => 'off_session', 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::SEPA, + ], ], $this->stripe->stripe_connect_auth); $data['pi_client_secret'] = $intent->client_secret; diff --git a/app/PaymentDrivers/Stripe/SOFORT.php b/app/PaymentDrivers/Stripe/SOFORT.php index 383061ceabce..a35993073671 100644 --- a/app/PaymentDrivers/Stripe/SOFORT.php +++ b/app/PaymentDrivers/Stripe/SOFORT.php @@ -52,7 +52,10 @@ class SOFORT 'payment_method_types' => ['sofort'], 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), - + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::SOFORT, + ], ], $this->stripe->stripe_connect_auth); $data['pi_client_secret'] = $intent->client_secret; diff --git a/app/PaymentDrivers/Stripe/Utilities.php b/app/PaymentDrivers/Stripe/Utilities.php index dd04e64aec21..77493dbf6851 100644 --- a/app/PaymentDrivers/Stripe/Utilities.php +++ b/app/PaymentDrivers/Stripe/Utilities.php @@ -18,7 +18,7 @@ trait Utilities public function convertFromStripeAmount($amount, $precision, $currency) { - if(in_array($amount, ["BIF","CLP","DJF","GNF","JPY","KMF","KRW","MGA","PYG","RWF","UGX","VND","VUV","XAF","XOF","XPF"])) + if(in_array($currency->code, ["BIF","CLP","DJF","GNF","JPY","KMF","KRW","MGA","PYG","RWF","UGX","VND","VUV","XAF","XOF","XPF"])) return $amount; return $amount / pow(10, $precision); @@ -28,7 +28,7 @@ trait Utilities public function convertToStripeAmount($amount, $precision, $currency) { - if(in_array($amount, ["BIF","CLP","DJF","GNF","JPY","KMF","KRW","MGA","PYG","RWF","UGX","VND","VUV","XAF","XOF","XPF"])) + if(in_array($currency->code, ["BIF","CLP","DJF","GNF","JPY","KMF","KRW","MGA","PYG","RWF","UGX","VND","VUV","XAF","XOF","XPF"])) return $amount; return round(($amount * pow(10, $precision)),0); diff --git a/app/PaymentDrivers/Stripe/iDeal.php b/app/PaymentDrivers/Stripe/iDeal.php index 1f16122ae1b8..8358c949fefc 100644 --- a/app/PaymentDrivers/Stripe/iDeal.php +++ b/app/PaymentDrivers/Stripe/iDeal.php @@ -52,7 +52,10 @@ class iDeal 'payment_method_types' => ['ideal'], 'customer' => $this->stripe->findOrCreateCustomer(), 'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), - + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::IDEAL, + ], ], $this->stripe->stripe_connect_auth); $data['pi_client_secret'] = $intent->client_secret; diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 39f896be0583..7d0ed85771de 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -25,25 +25,26 @@ use App\Models\PaymentHash; use App\Models\PaymentType; use App\Models\SystemLog; use App\PaymentDrivers\Stripe\ACH; +use App\PaymentDrivers\Stripe\ACSS; use App\PaymentDrivers\Stripe\Alipay; use App\PaymentDrivers\Stripe\ApplePay; +use App\PaymentDrivers\Stripe\BECS; +use App\PaymentDrivers\Stripe\Bancontact; +use App\PaymentDrivers\Stripe\BrowserPay; use App\PaymentDrivers\Stripe\Charge; use App\PaymentDrivers\Stripe\Connect\Verify; use App\PaymentDrivers\Stripe\CreditCard; -use App\PaymentDrivers\Stripe\ImportCustomers; -use App\PaymentDrivers\Stripe\SOFORT; -use App\PaymentDrivers\Stripe\SEPA; -use App\PaymentDrivers\Stripe\PRZELEWY24; -use App\PaymentDrivers\Stripe\GIROPAY; -use App\PaymentDrivers\Stripe\iDeal; use App\PaymentDrivers\Stripe\EPS; -use App\PaymentDrivers\Stripe\Bancontact; -use App\PaymentDrivers\Stripe\BECS; -use App\PaymentDrivers\Stripe\ACSS; -use App\PaymentDrivers\Stripe\BrowserPay; use App\PaymentDrivers\Stripe\FPX; +use App\PaymentDrivers\Stripe\GIROPAY; +use App\PaymentDrivers\Stripe\ImportCustomers; +use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook; +use App\PaymentDrivers\Stripe\PRZELEWY24; +use App\PaymentDrivers\Stripe\SEPA; +use App\PaymentDrivers\Stripe\SOFORT; use App\PaymentDrivers\Stripe\UpdatePaymentMethods; use App\PaymentDrivers\Stripe\Utilities; +use App\PaymentDrivers\Stripe\iDeal; use App\Utils\Traits\MakesHash; use Exception; use Illuminate\Http\RedirectResponse; @@ -472,10 +473,7 @@ class StripePaymentDriver extends BaseDriver $response = null; try { - // $response = $this->stripe - // ->refunds - // ->create(['charge' => $payment->transaction_reference, 'amount' => $this->convertToStripeAmount($amount, $this->client->currency()->precision, $this->client->currency())], $meta); - + $response = \Stripe\Refund::create([ 'charge' => $payment->transaction_reference, 'amount' => $this->convertToStripeAmount($amount, $this->client->currency()->precision, $this->client->currency()) @@ -532,7 +530,15 @@ class StripePaymentDriver extends BaseDriver // Allow app to catch up with webhook request. sleep(2); - if ($request->type === 'charge.succeeded' || $request->type === 'payment_intent.succeeded') { + //payment_intent.succeeded - this will confirm or cancel the payment + if($request->type === 'payment_intent.succeeded'){ + PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(10); + // PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id); + return response()->json([], 200); + } + + if ($request->type === 'charge.succeeded') { + // if ($request->type === 'charge.succeeded' || $request->type === 'payment_intent.succeeded') { foreach ($request->data as $transaction) { @@ -559,6 +565,7 @@ class StripePaymentDriver extends BaseDriver $payment->save(); } } + } elseif ($request->type === 'source.chargeable') { $this->init(); diff --git a/app/Services/Client/Statement.php b/app/Services/Client/Statement.php index 886579d8a32c..c93ca1264daa 100644 --- a/app/Services/Client/Statement.php +++ b/app/Services/Client/Statement.php @@ -174,15 +174,17 @@ class Statement $item->tax_rate1 = 5; } - $product = Product::first(); + //$product = Product::first(); - $item->cost = (float) $product->cost; - $item->product_key = $product->product_key; - $item->notes = $product->notes; - $item->custom_value1 = $product->custom_value1; - $item->custom_value2 = $product->custom_value2; - $item->custom_value3 = $product->custom_value3; - $item->custom_value4 = $product->custom_value4; + $product = new \stdClass; + + $item->cost = (float) 10; + $item->product_key = 'test'; + $item->notes = 'test notes'; + $item->custom_value1 = 'custom value1'; + $item->custom_value2 = 'custom value2'; + $item->custom_value3 = 'custom value3'; + $item->custom_value4 = 'custom value4'; $line_items[] = $item; } diff --git a/app/Services/Credit/CreditService.php b/app/Services/Credit/CreditService.php index 8bd2e8c891a1..e7b00b44f995 100644 --- a/app/Services/Credit/CreditService.php +++ b/app/Services/Credit/CreditService.php @@ -108,6 +108,7 @@ class CreditService $this->updateBalance($adjustment) ->updatePaidToDate($adjustment) + ->setStatus(Credit::STATUS_APPLIED) ->save(); //create a negative payment of total $this->credit->balance @@ -136,7 +137,6 @@ class CreditService ->client ->service() ->updatePaidToDate($adjustment) - ->setStatus(Credit::STATUS_APPLIED) ->save(); event('eloquent.created: App\Models\Payment', $payment); diff --git a/app/Services/Payment/UpdateInvoicePayment.php b/app/Services/Payment/UpdateInvoicePayment.php index 16c5d9738b2c..5a6a2a23921e 100644 --- a/app/Services/Payment/UpdateInvoicePayment.php +++ b/app/Services/Payment/UpdateInvoicePayment.php @@ -55,6 +55,16 @@ class UpdateInvoicePayment if($paid_amount > $invoice->partial && $paid_amount > $invoice->balance) $paid_amount = $invoice->balance; + /*Improve performance here - 26-01-2022 - also change the order of events for invoice first*/ + $invoice->service() //caution what if we amount paid was less than partial - we wipe it! + ->clearPartial() + ->updateBalance($paid_amount * -1) + ->updatePaidToDate($paid_amount) + ->updateStatus() + ->touchPdf() + ->workFlow() + ->save(); + /* Updates the company ledger */ $this->payment ->ledger() @@ -77,19 +87,22 @@ class UpdateInvoicePayment $this->payment->applied += $paid_amount; - $invoice->service() //caution what if we amount paid was less than partial - we wipe it! - ->clearPartial() - ->updateBalance($paid_amount * -1) - ->updatePaidToDate($paid_amount) - ->updateStatus() - ->save(); + // $invoice->service() //caution what if we amount paid was less than partial - we wipe it! + // ->clearPartial() + // ->updateBalance($paid_amount * -1) + // ->updatePaidToDate($paid_amount) + // ->updateStatus() + // ->save(); + + // $invoice->refresh(); + + // $invoice->service() + // ->touchPdf(true) + // ->workFlow() + // ->save(); + - $invoice->refresh(); - $invoice->service() - ->touchPdf(true) - ->workFlow() - ->save(); }); diff --git a/config/ninja.php b/config/ninja.php index 17084ce63212..0999c644e89b 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -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.3.49', - 'app_tag' => '5.3.49', + 'app_version' => '5.3.51', + 'app_tag' => '5.3.51', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/routes/api.php b/routes/api.php index e7f71619a675..af8e3393b6be 100644 --- a/routes/api.php +++ b/routes/api.php @@ -13,7 +13,7 @@ use Illuminate\Support\Facades\Route; -Route::group(['middleware' => ['throttle:10,1', 'api_secret_check']], function () { +Route::group(['middleware' => ['throttle:300,1', 'api_secret_check']], function () { Route::post('api/v1/signup', 'AccountController@store')->name('signup.submit'); Route::post('api/v1/oauth_login', 'Auth\LoginController@oauthApiLogin'); }); diff --git a/routes/client.php b/routes/client.php index 85b4ca9aaa93..ecdf91f81a95 100644 --- a/routes/client.php +++ b/routes/client.php @@ -28,7 +28,7 @@ Route::get('error', 'ClientPortal\ContactHashLoginController@errorPage')->name(' Route::get('client/payment/{contact_key}/{payment_id}', 'ClientPortal\InvitationController@paymentRouter')->middleware(['domain_db','contact_key_login']); Route::get('client/ninja/{contact_key}/{company_key}', 'ClientPortal\NinjaPlanController@index')->name('client.ninja_contact_login')->middleware(['domain_db']); -Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence','domain_db'], 'prefix' => 'client', 'as' => 'client.'], function () { +Route::group(['middleware' => ['auth:contact', 'locale', 'domain_db'], 'prefix' => 'client', 'as' => 'client.'], function () { Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit Route::get('plan', 'ClientPortal\NinjaPlanController@plan')->name('plan'); // name = (dashboard. index / create / show / update / destroy / edit