diff --git a/.github/workflows/react_release.yml b/.github/workflows/react_release.yml
index c4d6f63994aa..ca3a62ce7f64 100644
--- a/.github/workflows/react_release.yml
+++ b/.github/workflows/react_release.yml
@@ -38,6 +38,9 @@ jobs:
sudo php artisan cache:clear
sudo find ./vendor/bin/ -type f -exec chmod +x {} \;
sudo find ./ -type d -exec chmod 755 {} \;
+ - name: Set current date to variable
+ id: set_date
+ run: echo "current_date=$(date '+%Y-%m-%d')" >> $GITHUB_ENV
- name: Prepare React FrontEnd
run: |
@@ -46,10 +49,11 @@ jobs:
git checkout develop
cp .env.example .env
cp ../vite.config.ts.react ./vite.config.js
+ sed -i '/"version"/c\ "version": " Latest Build - ${{ env.current_date }}",' package.json
npm i
npm run build
cp -r dist/* ../public/
- mv dist/index.html ../resources/views/react/index.blade.php
+ mv ../public/index.html ../resources/views/react/index.blade.php
- name: Prepare JS/CSS assets
run: |
diff --git a/VERSION.txt b/VERSION.txt
index d315f4df7571..c0204ea277af 100644
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -1 +1 @@
-5.10.13
\ No newline at end of file
+5.10.16
\ No newline at end of file
diff --git a/app/Console/Commands/BackupUpdate.php b/app/Console/Commands/BackupUpdate.php
index 33b421ea384d..b2a16bff2502 100644
--- a/app/Console/Commands/BackupUpdate.php
+++ b/app/Console/Commands/BackupUpdate.php
@@ -177,7 +177,6 @@ class BackupUpdate extends Command
$doc_bin = $document->getFile();
} catch(\Exception $e) {
nlog("Exception:: BackupUpdate::" . $e->getMessage());
- nlog($e->getMessage());
}
if ($doc_bin) {
diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php
index 374e7783c3fe..efac973651df 100644
--- a/app/Export/CSV/BaseExport.php
+++ b/app/Export/CSV/BaseExport.php
@@ -172,6 +172,7 @@ class BaseExport
'tax_rate3' => 'invoice.tax_rate3',
'recurring_invoice' => 'invoice.recurring_id',
'auto_bill' => 'invoice.auto_bill_enabled',
+ 'project' => 'invoice.project',
];
protected array $recurring_invoice_report_keys = [
@@ -1038,6 +1039,10 @@ class BaseExport
$recurring_filters = [];
+ if($this->company->getSetting('report_include_drafts')){
+ $recurring_filters[] = RecurringInvoice::STATUS_DRAFT;
+ }
+
if (in_array('active', $status_parameters)) {
$recurring_filters[] = RecurringInvoice::STATUS_ACTIVE;
}
diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php
index d87c49ff943c..39ece67a28a9 100644
--- a/app/Export/CSV/InvoiceExport.php
+++ b/app/Export/CSV/InvoiceExport.php
@@ -153,9 +153,9 @@ class InvoiceExport extends BaseExport
private function decorateAdvancedFields(Invoice $invoice, array $entity): array
{
- // if (in_array('invoice.status', $this->input['report_keys'])) {
- // $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id);
- // }
+ if (in_array('invoice.project', $this->input['report_keys'])) {
+ $entity['invoice.project'] = $invoice->project ? $invoice->project->name : '';
+ }
if (in_array('invoice.recurring_id', $this->input['report_keys'])) {
$entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? '';
diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php
index 14a38aebee27..b2b13b523acf 100644
--- a/app/Export/CSV/InvoiceItemExport.php
+++ b/app/Export/CSV/InvoiceItemExport.php
@@ -265,6 +265,10 @@ class InvoiceItemExport extends BaseExport
$entity['invoice.user_id'] = $invoice->user ? $invoice->user->present()->name() : '';// @phpstan-ignore-line
}
+ if (in_array('invoice.project', $this->input['report_keys'])) {
+ $entity['invoice.project'] = $invoice->project ? $invoice->project->name : '';// @phpstan-ignore-line
+ }
+
return $entity;
}
diff --git a/app/Export/Decorators/InvoiceDecorator.php b/app/Export/Decorators/InvoiceDecorator.php
index 35985decba66..b6579b7f546d 100644
--- a/app/Export/Decorators/InvoiceDecorator.php
+++ b/app/Export/Decorators/InvoiceDecorator.php
@@ -92,6 +92,7 @@ class InvoiceDecorator extends Decorator implements DecoratorInterface
{
return $invoice->recurring_invoice ? $invoice->recurring_invoice->number : '';
}
+
public function auto_bill_enabled(Invoice $invoice)
{
return $invoice->auto_bill_enabled ? ctrans('texts.yes') : ctrans('texts.no');
diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php
index 2d2ef7cd06b8..de5a932fb76f 100644
--- a/app/Filters/InvoiceFilters.php
+++ b/app/Filters/InvoiceFilters.php
@@ -153,22 +153,22 @@ class InvoiceFilters extends QueryFilters
{
return $this->builder->where(function ($query) {
- $query->whereIn('invoices.status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT])
- ->where('invoices.is_deleted', 0)
- ->where('invoices.balance', '>', 0)
- ->orWhere(function ($query) {
+ $query->whereIn('status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT])
+ ->where('is_deleted', 0)
+ ->where('balance', '>', 0)
+ ->where(function ($query) {
- $query->whereNull('invoices.due_date')
+ $query->whereNull('due_date')
->orWhere(function ($q) {
- $q->where('invoices.due_date', '>=', now()->startOfDay()->subSecond())->where('invoices.partial', 0);
+ $q->where('due_date', '>=', now()->startOfDay()->subSecond())->where('partial', 0);
})
->orWhere(function ($q) {
- $q->where('invoices.partial_due_date', '>=', now()->startOfDay()->subSecond())->where('invoices.partial', '>', 0);
+ $q->where('partial_due_date', '>=', now()->startOfDay()->subSecond())->where('partial', '>', 0);
});
})
- ->orderByRaw('ISNULL(invoices.due_date), invoices.due_date ' . 'desc')
- ->orderByRaw('ISNULL(invoices.partial_due_date), invoices.partial_due_date ' . 'desc');
+ ->orderByRaw('ISNULL(due_date), due_date ' . 'desc')
+ ->orderByRaw('ISNULL(partial_due_date), partial_due_date ' . 'desc');
});
}
diff --git a/app/Http/Controllers/ChartController.php b/app/Http/Controllers/ChartController.php
index 394e762d9749..07fc7fc238e4 100644
--- a/app/Http/Controllers/ChartController.php
+++ b/app/Http/Controllers/ChartController.php
@@ -66,7 +66,7 @@ class ChartController extends BaseController
return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200);
}
- public function calculatedField(ShowCalculatedFieldRequest $request)
+ public function calculatedFields(ShowCalculatedFieldRequest $request)
{
/** @var \App\Models\User auth()->user() */
diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php
index f1993693a90f..ec30aacc37b6 100644
--- a/app/Http/Controllers/ClientPortal/InvitationController.php
+++ b/app/Http/Controllers/ClientPortal/InvitationController.php
@@ -300,7 +300,9 @@ class InvitationController extends Controller
'signature' => false,
'contact_first_name' => $invitation->contact->first_name ?? '',
'contact_last_name' => $invitation->contact->last_name ?? '',
- 'contact_email' => $invitation->contact->email ?? ''
+ 'contact_email' => $invitation->contact->email ?? '',
+ 'client_city' => $invitation->client->city ?? '',
+ 'client_postal_code' => $invitation->client->postal_code ?? '',
];
$request->replace($data);
diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php
index 728df5eb1de1..94a46bd5cbe5 100644
--- a/app/Http/Controllers/ClientPortal/PaymentController.php
+++ b/app/Http/Controllers/ClientPortal/PaymentController.php
@@ -108,11 +108,11 @@ class PaymentController extends Controller
*/
public function process(Request $request)
{
- $request->validate([
- 'contact_first_name' => ['required'],
- 'contact_last_name' => ['required'],
- 'contact_email' => ['required', 'email'],
- ]);
+ // $request->validate([
+ // 'contact_first_name' => ['required'],
+ // 'contact_last_name' => ['required'],
+ // 'contact_email' => ['required', 'email'],
+ // ]);
return (new InstantPayment($request))->run();
}
diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php
index 66eed43075f6..f0b26ad7d7c7 100644
--- a/app/Http/Controllers/ImportController.php
+++ b/app/Http/Controllers/ImportController.php
@@ -85,7 +85,7 @@ class ImportController extends Controller
$contents = $this->convertEncoding($contents);
// Store the csv in cache with an expiry of 10 minutes
- Cache::put($hash.'-'.$entityType, base64_encode($contents), 600);
+ Cache::put($hash.'-'.$entityType, base64_encode($contents), 1200);
// Parse CSV
$csv_array = $this->getCsvData($contents);
diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php
index 1d65de052fa6..0d5e5a13a83f 100644
--- a/app/Http/ValidationRules/Account/BlackListRule.php
+++ b/app/Http/ValidationRules/Account/BlackListRule.php
@@ -19,8 +19,9 @@ use Illuminate\Contracts\Validation\ValidationRule;
*/
class BlackListRule implements ValidationRule
{
- /** Bad domains +/- dispoable email domains */
+ /** Bad domains +/- disposable email domains */
private array $blacklist = [
+ 'padvn.com',
'anonaddy.me',
'nqmo.com',
'wireconnected.com',
diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php
index b2897352cc4a..fef3ccdd269f 100644
--- a/app/Import/Providers/BaseImport.php
+++ b/app/Import/Providers/BaseImport.php
@@ -98,7 +98,7 @@ class BaseImport
}
/** @var string $base64_encoded_csv */
- $base64_encoded_csv = Cache::pull($this->hash.'-'.$entity_type);
+ $base64_encoded_csv = Cache::get($this->hash.'-'.$entity_type);
if (empty($base64_encoded_csv)) {
return null;
@@ -473,6 +473,8 @@ class BaseImport
$tasks = $this->groupTasks($tasks, $task_number_key);
+ nlog($tasks);
+
foreach ($tasks as $raw_task) {
$task_data = [];
@@ -702,16 +704,16 @@ class BaseImport
->save();
}
- if ($invoice->status_id === Invoice::STATUS_DRAFT) {
- } elseif ($invoice->status_id === Invoice::STATUS_SENT) {
- $invoice = $invoice
- ->service()
- ->markSent()
- ->save();
- } elseif (
- $invoice->status_id <= Invoice::STATUS_SENT &&
- $invoice->amount > 0
- ) {
+ if ($invoice->status_id == Invoice::STATUS_DRAFT) {
+ return $invoice;
+ }
+
+ $invoice = $invoice
+ ->service()
+ ->markSent()
+ ->save();
+
+ if ($invoice->status_id <= Invoice::STATUS_SENT && $invoice->amount > 0) {
if ($invoice->balance <= 0) {
$invoice->status_id = Invoice::STATUS_PAID;
$invoice->save();
diff --git a/app/Import/Providers/Wave.php b/app/Import/Providers/Wave.php
index 89c3ef9931a2..1fbd58b0e0c0 100644
--- a/app/Import/Providers/Wave.php
+++ b/app/Import/Providers/Wave.php
@@ -172,7 +172,7 @@ class Wave extends BaseImport implements ImportInterface
{
$entity_type = 'expense';
- $data = $this->getCsvData($entity_type);
+ $data = $this->getCsvData('invoice');
if (!$data) {
$this->entity_count['expense'] = 0;
@@ -244,14 +244,17 @@ class Wave extends BaseImport implements ImportInterface
if (empty($expense_data['vendor_id'])) {
$vendor_data['user_id'] = $this->getUserIDForRecord($expense_data);
- $vendor_repository->save(
- ['name' => $raw_expense['Vendor Name']],
- $vendor = VendorFactory::create(
- $this->company->id,
- $vendor_data['user_id']
- )
- );
- $expense_data['vendor_id'] = $vendor->id;
+ if(isset($raw_expense['Vendor Name']) || isset($raw_expense['Vendor']))
+ {
+ $vendor_repository->save(
+ ['name' => isset($raw_expense['Vendor Name']) ? $raw_expense['Vendor Name'] : isset($raw_expense['Vendor'])],
+ $vendor = VendorFactory::create(
+ $this->company->id,
+ $vendor_data['user_id']
+ )
+ );
+ $expense_data['vendor_id'] = $vendor->id;
+ }
}
$validator = Validator::make(
diff --git a/app/Import/Transformer/Csv/TaskTransformer.php b/app/Import/Transformer/Csv/TaskTransformer.php
index edd8737131cb..54636349f050 100644
--- a/app/Import/Transformer/Csv/TaskTransformer.php
+++ b/app/Import/Transformer/Csv/TaskTransformer.php
@@ -46,6 +46,7 @@ class TaskTransformer extends BaseTransformer
'company_id' => $this->company->id,
'number' => $this->getString($task_data, 'task.number'),
'user_id' => $this->getString($task_data, 'task.user_id'),
+ 'rate' => $this->getFloat($task_data, 'task.rate'),
'client_id' => $clientId,
'project_id' => $this->getProjectId($projectId, $clientId),
'description' => $this->getString($task_data, 'task.description'),
@@ -87,8 +88,7 @@ class TaskTransformer extends BaseTransformer
$is_billable = true;
}
- if(isset($item['task.start_date']) &&
- isset($item['task.end_date'])) {
+ if(isset($item['task.start_date'])) {
$start_date = $this->resolveStartDate($item);
$end_date = $this->resolveEndDate($item);
} elseif(isset($item['task.duration'])) {
@@ -136,7 +136,7 @@ class TaskTransformer extends BaseTransformer
private function resolveEndDate($item)
{
- $stub_end_date = $item['task.end_date'];
+ $stub_end_date = isset($item['task.end_date']) ? $item['task.end_date'] : $item['task.start_date'];
$stub_end_date .= isset($item['task.end_time']) ? " ".$item['task.end_time'] : '';
try {
diff --git a/app/Import/Transformer/Wave/ExpenseTransformer.php b/app/Import/Transformer/Wave/ExpenseTransformer.php
index 8f37c94b788a..afd282b80d63 100644
--- a/app/Import/Transformer/Wave/ExpenseTransformer.php
+++ b/app/Import/Transformer/Wave/ExpenseTransformer.php
@@ -36,18 +36,26 @@ class ExpenseTransformer extends BaseTransformer
$total_tax += floatval($record['Sales Tax Amount']);
}
- $tax_rate = round(($total_tax / $amount) * 100, 3);
+ $tax_rate = $total_tax > 0 ? round(($total_tax / $amount) * 100, 3) : 0;
+
+ if(isset($data['Notes / Memo']) && strlen($data['Notes / Memo']) > 1)
+ $public_notes = $data['Notes / Memo'];
+ elseif (isset($data['Transaction Description']) && strlen($data['Transaction Description']) > 1)
+ $public_notes = $data['Transaction Description'];
+ else
+ $public_notes = '';
+
$transformed = [
'company_id' => $this->company->id,
'vendor_id' => $this->getVendorIdOrCreate($this->getString($data, 'Vendor')),
'number' => $this->getString($data, 'Bill Number'),
- 'public_notes' => $this->getString($data, 'Notes / Memo'),
+ 'public_notes' => $public_notes,
'date' => $this->parseDate($data['Transaction Date Added']) ?: now()->format('Y-m-d'), //27-01-2022
'currency_id' => $this->company->settings->currency_id,
'category_id' => $this->getOrCreateExpenseCategry($data['Account Name']),
'amount' => $amount,
- 'tax_name1' => $data['Sales Tax Name'],
+ 'tax_name1' => isset($data['Sales Tax Name']) ? $data['Sales Tax Name'] : '',
'tax_rate1' => $tax_rate,
];
diff --git a/app/Livewire/BillingPortalPurchase.php b/app/Livewire/BillingPortalPurchase.php
index 0a15710018c6..079a36325e80 100644
--- a/app/Livewire/BillingPortalPurchase.php
+++ b/app/Livewire/BillingPortalPurchase.php
@@ -188,6 +188,10 @@ class BillingPortalPurchase extends Component
public ?string $contact_email;
+ public ?string $client_city;
+
+ public ?string $client_postal_code;
+
public function mount()
{
MultiDB::setDb($this->db);
@@ -203,7 +207,7 @@ class BillingPortalPurchase extends Component
if (request()->query('coupon')) {
$this->coupon = request()->query('coupon');
$this->handleCoupon();
- } elseif (strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) {
+ } elseif (strlen($this->subscription->promo_code ?? '') == 0 && $this->subscription->promo_discount > 0) {
$this->price = $this->subscription->promo_price;
}
@@ -335,10 +339,6 @@ class BillingPortalPurchase extends Component
{
$this->contact = $contact;
- if ($contact->showRff()) {
- return $this->rff();
- }
-
Auth::guard('contact')->loginUsingId($contact->id, true);
if ($this->subscription->trial_enabled) {
@@ -351,11 +351,20 @@ class BillingPortalPurchase extends Component
if ((int)$this->price == 0) {
$this->steps['payment_required'] = false;
} else {
- $this->steps['fetched_payment_methods'] = true;
+ // $this->steps['fetched_payment_methods'] = true;
}
$this->methods = $contact->client->service()->getPaymentMethods($this->price);
+ foreach($this->methods as $method){
+
+ if($method['is_paypal'] == '1' && !$this->steps['check_rff']){
+ $this->rff();
+ break;
+ }
+
+ }
+
$this->heading_text = ctrans('texts.payment_methods');
return $this;
@@ -366,6 +375,8 @@ class BillingPortalPurchase extends Component
$this->contact_first_name = $this->contact->first_name;
$this->contact_last_name = $this->contact->last_name;
$this->contact_email = $this->contact->email;
+ $this->client_city = $this->contact->client->city;
+ $this->client_postal_code = $this->contact->client->postal_code;
$this->steps['check_rff'] = true;
@@ -377,13 +388,20 @@ class BillingPortalPurchase extends Component
$validated = $this->validate([
'contact_first_name' => ['required'],
'contact_last_name' => ['required'],
+ 'client_city' => ['required'],
+ 'client_postal_code' => ['required'],
'contact_email' => ['required', 'email'],
]);
$this->contact->first_name = $validated['contact_first_name'];
$this->contact->last_name = $validated['contact_last_name'];
$this->contact->email = $validated['contact_email'];
- $this->contact->save();
+ $this->contact->client->postal_code = $validated['client_postal_code'];
+ $this->contact->client->city = $validated['client_city'];
+
+ $this->contact->pushQuietly();
+
+ $this->steps['fetched_payment_methods'] = true;
return $this->getPaymentMethods($this->contact);
}
@@ -395,13 +413,13 @@ class BillingPortalPurchase extends Component
* @param $company_gateway_id
* @param $gateway_type_id
*/
- public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id)
+ public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id, $is_paypal = false)
{
$this->company_gateway_id = $company_gateway_id;
$this->payment_method_id = $gateway_type_id;
$this->handleBeforePaymentEvents();
-
+
}
/**
diff --git a/app/Livewire/BillingPortalPurchasev2.php b/app/Livewire/BillingPortalPurchasev2.php
index d707707578b2..5ee7191700ca 100644
--- a/app/Livewire/BillingPortalPurchasev2.php
+++ b/app/Livewire/BillingPortalPurchasev2.php
@@ -164,6 +164,13 @@ class BillingPortalPurchasev2 extends Component
public $payment_confirmed = false;
public $is_eligible = true;
public $not_eligible_message = '';
+ public $check_rff = false;
+
+ public ?string $contact_first_name;
+ public ?string $contact_last_name;
+ public ?string $contact_email;
+ public ?string $client_city;
+ public ?string $client_postal_code;
public function mount()
{
@@ -472,7 +479,6 @@ class BillingPortalPurchasev2 extends Component
*/
protected function getPaymentMethods(): self
{
- nlog("total amount = {$this->float_amount_total}");
if ($this->float_amount_total == 0) {
$this->methods = [];
@@ -481,10 +487,73 @@ class BillingPortalPurchasev2 extends Component
if ($this->contact && $this->float_amount_total >= 1) {
$this->methods = $this->contact->client->service()->getPaymentMethods($this->float_amount_total);
}
+
+ foreach($this->methods as $method) {
+
+ if($method['is_paypal'] == '1' && !$this->check_rff) {
+ $this->rff();
+ break;
+ }
+
+ }
return $this;
}
+ protected function rff()
+ {
+
+ $this->contact_first_name = $this->contact->first_name;
+ $this->contact_last_name = $this->contact->last_name;
+ $this->contact_email = $this->contact->email;
+ $this->client_city = $this->contact->client->city;
+ $this->client_postal_code = $this->contact->client->postal_code;
+
+ if(
+ strlen($this->contact_first_name ?? '') == 0 ||
+ strlen($this->contact_last_name ?? '') == 0 ||
+ strlen($this->contact_email ?? '') == 0 ||
+ strlen($this->client_city ?? '') == 0 ||
+ strlen($this->client_postal_code ?? '') == 0
+ )
+ {
+ $this->check_rff = true;
+ }
+
+ return $this;
+ }
+
+ public function handleRff()
+ {
+
+ $validated = $this->validate([
+ 'contact_first_name' => ['required'],
+ 'contact_last_name' => ['required'],
+ 'client_city' => ['required'],
+ 'client_postal_code' => ['required'],
+ 'contact_email' => ['required', 'email'],
+ ]);
+
+ $this->check_rff = false;
+
+ $this->contact->first_name = $validated['contact_first_name'];
+ $this->contact->last_name = $validated['contact_last_name'];
+ $this->contact->email = $validated['contact_email'];
+ $this->contact->client->postal_code = $validated['client_postal_code'];
+ $this->contact->client->city = $validated['client_city'];
+
+ $this->contact->pushQuietly();
+
+ $this->refreshComponent();
+
+ return $this;
+ }
+
+ protected function refreshComponent()
+ {
+ $this->dispatch('$refresh');
+ }
+
/**
* Middle method between selecting payment method &
* submitting the from to the backend.
diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php
index fd47d0e6feb0..0885db2c6c33 100644
--- a/app/Models/ClientContact.php
+++ b/app/Models/ClientContact.php
@@ -351,9 +351,9 @@ class ClientContact extends Authenticatable implements HasLocalePreference
public function showRff(): bool
{
- if (\strlen($this->first_name) === 0 || \strlen($this->last_name) === 0 || \strlen($this->email) === 0) {
- return true;
- }
+ // if (\strlen($this->first_name ?? '') === 0 || \strlen($this->last_name ?? '') === 0 || \strlen($this->email ?? '') === 0) {
+ // return true;
+ // }
return false;
}
diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php
index 9311699f31e3..62ba38ee8e41 100644
--- a/app/Models/CompanyGateway.php
+++ b/app/Models/CompanyGateway.php
@@ -159,6 +159,11 @@ class CompanyGateway extends BaseModel
protected $touches = [];
+ public function isPayPal()
+ {
+ return in_array($this->gateway_key, ['80af24a6a691230bbec33e930ab40666','80af24a6a691230bbec33e930ab40665']);
+ }
+
public function getEntityType()
{
return self::class;
diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php
index b4628cf411b7..43bbfe8fa719 100644
--- a/app/Models/Gateway.php
+++ b/app/Models/Gateway.php
@@ -141,23 +141,23 @@ class Gateway extends StaticModel
case 20:
case 56:
return [
- 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', 'charge.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::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', 'charge.refunded', 'payment_intent.payment_failed']],
+ GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.refunded','charge.succeeded', 'customer.source.updated', 'payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']],
+ GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing', 'charge.refunded', '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', 'charge.failed', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated', 'payment_intent.payment_failed']],
- GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
- GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
- GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
- GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
- GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
- GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
- GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
- GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
- GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
- GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
- GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed',]],
+ GatewayType::BACS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated', 'payment_intent.payment_failed']],
+ GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
+ GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
+ GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
+ GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
+ GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
+ GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
+ GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
+ GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
+ GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
+ GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
+ GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed',]],
];
case 39:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout
diff --git a/app/Models/Project.php b/app/Models/Project.php
index d341a3fb8569..c786dcec0836 100644
--- a/app/Models/Project.php
+++ b/app/Models/Project.php
@@ -129,7 +129,7 @@ class Project extends BaseModel
public function invoices(): HasMany
{
- return $this->hasMany(Invoice::class);
+ return $this->hasMany(Invoice::class)->withTrashed();
}
public function quotes(): HasMany
diff --git a/app/PaymentDrivers/Forte/ACH.php b/app/PaymentDrivers/Forte/ACH.php
index aef043d50a9e..8ea313e77200 100644
--- a/app/PaymentDrivers/Forte/ACH.php
+++ b/app/PaymentDrivers/Forte/ACH.php
@@ -170,6 +170,9 @@ class ACH
];
$payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED);
- return redirect('client/invoices')->withSuccess('Invoice paid.');
+ // return redirect('client/invoices')->withSuccess('Invoice paid.');
+
+ return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
+
}
}
diff --git a/app/PaymentDrivers/Forte/CreditCard.php b/app/PaymentDrivers/Forte/CreditCard.php
index 67c404190137..5a317f4ec8a7 100644
--- a/app/PaymentDrivers/Forte/CreditCard.php
+++ b/app/PaymentDrivers/Forte/CreditCard.php
@@ -187,6 +187,8 @@ class CreditCard
'gateway_type_id' => GatewayType::CREDIT_CARD,
];
$payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED);
- return redirect('client/invoices')->withSuccess('Invoice paid.');
+ // return redirect('client/invoices')->withSuccess('Invoice paid.');
+ return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
+
}
}
diff --git a/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php b/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php
index 56824c840269..e7f6fe01c145 100644
--- a/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php
+++ b/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php
@@ -251,11 +251,11 @@ class PayPalBasePaymentDriver extends BaseDriver
[
"address" =>
[
- "address_line_1" => strlen($this->client->shipping_address1) > 1 ? $this->client->shipping_address1 : $this->client->address1,
+ "address_line_1" => strlen($this->client->shipping_address1 ?? '') > 1 ? $this->client->shipping_address1 : $this->client->address1,
"address_line_2" => $this->client->shipping_address2,
- "admin_area_2" => strlen($this->client->shipping_city) > 1 ? $this->client->shipping_city : $this->client->city,
- "admin_area_1" => strlen($this->client->shipping_state) > 1 ? $this->client->shipping_state : $this->client->state,
- "postal_code" => strlen($this->client->shipping_postal_code) > 1 ? $this->client->shipping_postal_code : $this->client->postal_code,
+ "admin_area_2" => strlen($this->client->shipping_city ?? '') > 1 ? $this->client->shipping_city : $this->client->city,
+ "admin_area_1" => strlen($this->client->shipping_state ?? '') > 1 ? $this->client->shipping_state : $this->client->state,
+ "postal_code" => strlen($this->client->shipping_postal_code ?? '') > 1 ? $this->client->shipping_postal_code : $this->client->postal_code,
"country_code" => $this->client->present()->shipping_country_code(),
],
]
diff --git a/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php b/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php
index 36f766dd7303..405f696742e4 100644
--- a/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php
+++ b/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php
@@ -11,18 +11,22 @@
namespace App\PaymentDrivers\Stripe\Jobs;
-use App\Libraries\MultiDB;
use App\Models\Company;
-use App\Models\CompanyGateway;
use App\Models\Payment;
+use App\Libraries\MultiDB;
use App\Models\PaymentHash;
-use App\PaymentDrivers\Stripe\Utilities;
+use App\Services\Email\Email;
use Illuminate\Bus\Queueable;
+use App\Models\CompanyGateway;
+use App\Services\Email\EmailObject;
+use Illuminate\Support\Facades\App;
+use Illuminate\Mail\Mailables\Address;
+use Illuminate\Queue\SerializesModels;
+use App\PaymentDrivers\Stripe\Utilities;
+use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
-use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
-use Illuminate\Queue\SerializesModels;
class ChargeRefunded implements ShouldQueue
{
@@ -36,19 +40,10 @@ class ChargeRefunded implements ShouldQueue
public $deleteWhenMissingModels = true;
- public $stripe_request;
-
- public $company_key;
-
- private $company_gateway_id;
-
public $payment_completed = false;
- public function __construct($stripe_request, $company_key, $company_gateway_id)
+ public function __construct(public array $stripe_request, private string $company_key)
{
- $this->stripe_request = $stripe_request;
- $this->company_key = $company_key;
- $this->company_gateway_id = $company_gateway_id;
}
public function handle()
@@ -64,8 +59,8 @@ class ChargeRefunded implements ShouldQueue
$payment_hash_key = $source['metadata']['payment_hash'] ?? null;
- $company_gateway = CompanyGateway::query()->find($this->company_gateway_id);
$payment_hash = PaymentHash::query()->where('hash', $payment_hash_key)->first();
+ $company_gateway = $payment_hash->payment->company_gateway;
$stripe_driver = $company_gateway->driver()->init();
@@ -79,7 +74,7 @@ class ChargeRefunded implements ShouldQueue
->first();
//don't touch if already refunded
- if(!$payment || in_array($payment->status_id, [Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])) {
+ if(!$payment || $payment->status_id == Payment::STATUS_REFUNDED || $payment->is_deleted){
return;
}
@@ -94,8 +89,19 @@ class ChargeRefunded implements ShouldQueue
return;
}
- if($payment->status_id == Payment::STATUS_COMPLETED) {
+ usleep(rand(200000,300000));
+ $payment = $payment->fresh();
+ if($payment->status_id == Payment::STATUS_PARTIALLY_REFUNDED){
+ //determine the delta in the refunded amount - how much has already been refunded and only apply the delta.
+
+ if(floatval($payment->refunded) >= floatval($amount_refunded))
+ return;
+
+ $amount_refunded -= $payment->refunded;
+
+ }
+
$invoice_collection = $payment->paymentables
->where('paymentable_type', 'invoices')
->map(function ($pivot) {
@@ -117,9 +123,24 @@ class ChargeRefunded implements ShouldQueue
];
});
- } elseif($invoice_collection->sum('amount') != $amount_refunded) {
- //too many edges cases at this point, return early
+ }
+ elseif($invoice_collection->sum('amount') != $amount_refunded) {
+
+ $refund_text = "A partial refund was processed for Payment #{$payment_hash->payment->number}.
This payment is associated with multiple invoices, so you will need to manually apply the refund to the correct invoice/s.";
+
+ App::setLocale($payment_hash->payment->company->getLocale());
+
+ $mo = new EmailObject();
+ $mo->subject = "Refund processed in Stripe for multiple invoices, action required.";
+ $mo->body = $refund_text;
+ $mo->text_body = $refund_text;
+ $mo->company_key = $payment_hash->payment->company->company_key;
+ $mo->html_template = 'email.template.generic';
+ $mo->to = [new Address($payment_hash->payment->company->owner()->email, $payment_hash->payment->company->owner()->present()->name())];
+
+ Email::dispatch($mo, $payment_hash->payment->company);
return;
+
}
$invoices = $invoice_collection->toArray();
@@ -131,20 +152,21 @@ class ChargeRefunded implements ShouldQueue
'date' => now()->format('Y-m-d'),
'gateway_refund' => false,
'email_receipt' => false,
+ 'via_webhook' => true,
];
nlog($data);
$payment->refund($data);
- $payment->private_notes .= 'Refunded via Stripe';
- return;
- }
+ $payment->private_notes .= 'Refunded via Stripe ';
+
+ $payment->saveQuietly();
}
public function middleware()
{
- return [new WithoutOverlapping($this->company_gateway_id)];
+ return [new WithoutOverlapping($this->company_key)];
}
}
diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php
index 69920e31e6b5..d91e39fc3763 100644
--- a/app/PaymentDrivers/StripePaymentDriver.php
+++ b/app/PaymentDrivers/StripePaymentDriver.php
@@ -12,54 +12,55 @@
namespace App\PaymentDrivers;
-use App\Exceptions\PaymentFailed;
-use App\Exceptions\StripeConnectFailure;
-use App\Http\Requests\Payments\PaymentWebhookRequest;
-use App\Http\Requests\Request;
-use App\Jobs\Util\SystemLogger;
-use App\Models\Client;
-use App\Models\ClientGatewayToken;
-use App\Models\GatewayType;
-use App\Models\Payment;
-use App\Models\PaymentHash;
-use App\Models\SystemLog;
-use App\PaymentDrivers\Stripe\ACH;
-use App\PaymentDrivers\Stripe\ACSS;
-use App\PaymentDrivers\Stripe\Alipay;
-use App\PaymentDrivers\Stripe\BACS;
-use App\PaymentDrivers\Stripe\Bancontact;
-use App\PaymentDrivers\Stripe\BankTransfer;
-use App\PaymentDrivers\Stripe\BECS;
-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\EPS;
-use App\PaymentDrivers\Stripe\FPX;
-use App\PaymentDrivers\Stripe\GIROPAY;
-use App\PaymentDrivers\Stripe\iDeal;
-use App\PaymentDrivers\Stripe\ImportCustomers;
-use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook;
-use App\PaymentDrivers\Stripe\Jobs\PaymentIntentPartiallyFundedWebhook;
-use App\PaymentDrivers\Stripe\Jobs\PaymentIntentProcessingWebhook;
-use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook;
-use App\PaymentDrivers\Stripe\Klarna;
-use App\PaymentDrivers\Stripe\PRZELEWY24;
-use App\PaymentDrivers\Stripe\SEPA;
-use App\PaymentDrivers\Stripe\SOFORT;
-use App\PaymentDrivers\Stripe\Utilities;
-use App\Utils\Traits\MakesHash;
use Exception;
-use Illuminate\Http\RedirectResponse;
-use Laracasts\Presenter\Exceptions\PresenterException;
+use Stripe\Stripe;
use Stripe\Account;
use Stripe\Customer;
-use Stripe\Exception\ApiErrorException;
+use App\Models\Client;
+use App\Models\Payment;
+use Stripe\SetupIntent;
+use Stripe\StripeClient;
+use App\Models\SystemLog;
use Stripe\PaymentIntent;
use Stripe\PaymentMethod;
-use Stripe\SetupIntent;
-use Stripe\Stripe;
-use Stripe\StripeClient;
+use App\Models\GatewayType;
+use App\Models\PaymentHash;
+use App\Http\Requests\Request;
+use App\Jobs\Util\SystemLogger;
+use App\Utils\Traits\MakesHash;
+use App\Exceptions\PaymentFailed;
+use App\Models\ClientGatewayToken;
+use App\PaymentDrivers\Stripe\ACH;
+use App\PaymentDrivers\Stripe\EPS;
+use App\PaymentDrivers\Stripe\FPX;
+use App\PaymentDrivers\Stripe\ACSS;
+use App\PaymentDrivers\Stripe\BACS;
+use App\PaymentDrivers\Stripe\BECS;
+use App\PaymentDrivers\Stripe\SEPA;
+use App\PaymentDrivers\Stripe\iDeal;
+use App\PaymentDrivers\Stripe\Alipay;
+use App\PaymentDrivers\Stripe\Charge;
+use App\PaymentDrivers\Stripe\Klarna;
+use App\PaymentDrivers\Stripe\SOFORT;
+use Illuminate\Http\RedirectResponse;
+use App\PaymentDrivers\Stripe\GIROPAY;
+use Stripe\Exception\ApiErrorException;
+use App\Exceptions\StripeConnectFailure;
+use App\PaymentDrivers\Stripe\Utilities;
+use App\PaymentDrivers\Stripe\Bancontact;
+use App\PaymentDrivers\Stripe\BrowserPay;
+use App\PaymentDrivers\Stripe\CreditCard;
+use App\PaymentDrivers\Stripe\PRZELEWY24;
+use App\PaymentDrivers\Stripe\BankTransfer;
+use App\PaymentDrivers\Stripe\Connect\Verify;
+use App\PaymentDrivers\Stripe\ImportCustomers;
+use App\PaymentDrivers\Stripe\Jobs\ChargeRefunded;
+use App\Http\Requests\Payments\PaymentWebhookRequest;
+use Laracasts\Presenter\Exceptions\PresenterException;
+use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook;
+use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook;
+use App\PaymentDrivers\Stripe\Jobs\PaymentIntentProcessingWebhook;
+use App\PaymentDrivers\Stripe\Jobs\PaymentIntentPartiallyFundedWebhook;
class StripePaymentDriver extends BaseDriver
{
@@ -670,31 +671,39 @@ class StripePaymentDriver extends BaseDriver
public function processWebhookRequest(PaymentWebhookRequest $request)
{
+ nlog($request->all());
+
if ($request->type === 'customer.source.updated') {
$ach = new ACH($this);
$ach->updateBankAccount($request->all());
}
if ($request->type === 'payment_intent.processing') {
- PaymentIntentProcessingWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(10, 12)));
+ PaymentIntentProcessingWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(5));
return response()->json([], 200);
}
//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(now()->addSeconds(rand(10, 15)));
+ PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(5));
return response()->json([], 200);
}
if ($request->type === 'payment_intent.partially_funded') {
- PaymentIntentPartiallyFundedWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(10, 15)));
+ PaymentIntentPartiallyFundedWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(5));
return response()->json([], 200);
}
if (in_array($request->type, ['payment_intent.payment_failed', 'charge.failed'])) {
- PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(5, 10)));
+ PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(2));
+
+ return response()->json([], 200);
+ }
+
+ if ($request->type === 'charge.refunded' && $request->data['object']['status'] == 'succeeded') {
+ ChargeRefunded::dispatch($request->data, $request->company_key)->delay(now()->addSeconds(5));
return response()->json([], 200);
}
@@ -702,7 +711,6 @@ class StripePaymentDriver extends BaseDriver
if ($request->type === 'charge.succeeded') {
foreach ($request->data as $transaction) {
-
$payment = Payment::query()
->where('company_id', $this->company_gateway->company_id)
->where(function ($query) use ($transaction) {
diff --git a/app/Services/Client/PaymentMethod.php b/app/Services/Client/PaymentMethod.php
index de5e1622585d..7ca8965d3fc9 100644
--- a/app/Services/Client/PaymentMethod.php
+++ b/app/Services/Client/PaymentMethod.php
@@ -192,6 +192,7 @@ class PaymentMethod
'label' => ctrans('texts.apply_credit'),
'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT,
'gateway_type_id' => GatewayType::CREDIT,
+ 'is_paypal' => false,
];
}
@@ -210,12 +211,14 @@ class PaymentMethod
'label' => $gateway->getConfigField('name').$fee_label,
'company_gateway_id' => $gateway->id,
'gateway_type_id' => GatewayType::CREDIT_CARD,
+ 'is_paypal' => $gateway->isPayPal(),
];
} else {
$this->payment_urls[] = [
'label' => $gateway->getTypeAlias($type).$fee_label,
'company_gateway_id' => $gateway->id,
'gateway_type_id' => $type,
+ 'is_paypal' => $gateway->isPayPal(),
];
}
@@ -236,12 +239,14 @@ class PaymentMethod
'label' => $gateway->getConfigField('name').$fee_label,
'company_gateway_id' => $gateway_id,
'gateway_type_id' => GatewayType::CREDIT_CARD,
+ 'is_paypal' => $gateway->isPayPal(),
];
} else {
$this->payment_urls[] = [
'label' => $gateway->getTypeAlias($gateway_type_id).$fee_label,
'company_gateway_id' => $gateway_id,
'gateway_type_id' => $gateway_type_id,
+ 'is_paypal' => $gateway->isPayPal(),
];
}
}
@@ -259,6 +264,7 @@ class PaymentMethod
'label' => ctrans('texts.apply_credit'),
'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT,
'gateway_type_id' => GatewayType::CREDIT,
+ 'is_paypal' => false,
];
}
diff --git a/app/Services/ClientPortal/InstantPayment.php b/app/Services/ClientPortal/InstantPayment.php
index d46052fafb03..f41e4ff1b45d 100644
--- a/app/Services/ClientPortal/InstantPayment.php
+++ b/app/Services/ClientPortal/InstantPayment.php
@@ -44,17 +44,16 @@ class InstantPayment
public function run()
{
- nlog($this->request->all());
-
/** @var \App\Models\ClientContact $cc */
-
$cc = auth()->guard('contact')->user();
-
$cc->first_name = $this->request->contact_first_name;
$cc->last_name = $this->request->contact_last_name;
$cc->email = $this->request->contact_email;
-
- $cc->save();
+ $cc->client->postal_code = strlen($cc->client->postal_code ?? '') > 1 ? $cc->client->postal_code : $this->request->client_postal_code;
+ $cc->client->city = strlen($cc->client->city ?? '') > 1 ? $cc->client->city : $this->request->client_city;
+ $cc->client->shipping_postal_code = strlen($cc->client->shipping_postal_code ?? '') > 1 ? $cc->client->shipping_postal_code : $cc->client->postal_code;
+ $cc->client->shipping_city = strlen($cc->client->shipping_city ?? '') > 1 ? $cc->client->shipping_city : $cc->client->city;
+ $cc->pushQuietly();
$is_credit_payment = false;
@@ -73,8 +72,6 @@ class InstantPayment
*/
$payable_invoices = collect($this->request->payable_invoices);
- nlog($payable_invoices);
-
$invoices = Invoice::query()->whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get();
$invoices->each(function ($invoice) {
diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php
index a9c9f415396d..53c51145d859 100644
--- a/app/Services/Invoice/AutoBillInvoice.php
+++ b/app/Services/Invoice/AutoBillInvoice.php
@@ -42,6 +42,9 @@ class AutoBillInvoice extends AbstractService
public function __construct(private Invoice $invoice, protected string $db)
{
+
+ $this->client = $this->invoice->client;
+
}
public function run()
@@ -49,8 +52,7 @@ class AutoBillInvoice extends AbstractService
MultiDB::setDb($this->db);
/* @var \App\Modesl\Client $client */
- $this->client = $this->invoice->client;
-
+
$is_partial = false;
/* Is the invoice payable? */
@@ -272,7 +274,7 @@ class AutoBillInvoice extends AbstractService
*
* @return self
*/
- private function applyUnappliedPayment(): self
+ public function applyUnappliedPayment(): self
{
$unapplied_payments = Payment::query()
->where('client_id', $this->client->id)
@@ -284,6 +286,11 @@ class AutoBillInvoice extends AbstractService
->get();
$available_unapplied_balance = $unapplied_payments->sum('amount') - $unapplied_payments->sum('applied');
+
+ nlog($this->client->id);
+ nlog($this->invoice->id);
+ nlog($unapplied_payments->sum('amount'));
+ nlog($unapplied_payments->sum('applied'));
nlog("available unapplied balance = {$available_unapplied_balance}");
@@ -347,7 +354,7 @@ class AutoBillInvoice extends AbstractService
*
* @return $this
*/
- private function applyCreditPayment(): self
+ public function applyCreditPayment(): self
{
$available_credits = Credit::query()->where('client_id', $this->client->id)
->where('is_deleted', false)
diff --git a/app/Services/Payment/RefundPayment.php b/app/Services/Payment/RefundPayment.php
index a17baa67051e..ed7b51deed20 100644
--- a/app/Services/Payment/RefundPayment.php
+++ b/app/Services/Payment/RefundPayment.php
@@ -44,7 +44,6 @@ class RefundPayment
->setStatus() //sets status of payment
->updatePaymentables() //update the paymentable items
->adjustInvoices()
- ->finalize()
->save();
if (array_key_exists('email_receipt', $this->refund_data) && $this->refund_data['email_receipt'] == 'true') {
@@ -52,10 +51,11 @@ class RefundPayment
EmailRefundPayment::dispatch($this->payment, $this->payment->company, $contact);
}
- $notes = ctrans('texts.refunded') . " : {$this->total_refund} - " . ctrans('texts.gateway_refund') . " : ";
- $notes .= $this->refund_data['gateway_refund'] !== false ? ctrans('texts.yes') : ctrans('texts.no');
-
+ $is_gateway_refund = ($this->refund_data['gateway_refund'] !== false || $this->refund_failed || (isset($this->refund_data['via_webhook']) && $this->refund_data['via_webhook'] !== false)) ? ctrans('texts.yes') : ctrans('texts.no');
+ $notes = ctrans('texts.refunded') . " : {$this->total_refund} - " . ctrans('texts.gateway_refund') . " : " . $is_gateway_refund;
+
$this->createActivity($notes);
+ $this->finalize();
return $this->payment;
}
@@ -178,7 +178,7 @@ class RefundPayment
*/
private function setStatus()
{
- if ($this->total_refund == $this->payment->amount) {
+ if ($this->total_refund == $this->payment->amount || floatval($this->payment->amount) == floatval($this->payment->refunded)) {
$this->payment->status_id = Payment::STATUS_REFUNDED;
} else {
$this->payment->status_id = Payment::STATUS_PARTIALLY_REFUNDED;
diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php
index da88156c8d70..af1ea653f64e 100644
--- a/app/Services/Subscription/SubscriptionService.php
+++ b/app/Services/Subscription/SubscriptionService.php
@@ -88,7 +88,7 @@ class SubscriptionService
// if we have a recurring product - then generate a recurring invoice
- if (strlen($this->subscription->recurring_product_ids) >= 1) {
+ if (strlen($this->subscription->recurring_product_ids ?? '') >= 1) {
if (isset($payment_hash->data->billing_context->bundle)) {
$recurring_invoice = $this->convertInvoiceToRecurringBundle($payment_hash->payment->client_id, $payment_hash->data->billing_context->bundle);
} else {
@@ -1024,10 +1024,10 @@ class SubscriptionService
$invoice->subscription_id = $this->subscription->id;
$invoice->is_proforma = true;
- if (strlen($data['coupon']) >= 1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) {
+ if (strlen($data['coupon'] ?? '') >= 1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) {
$invoice->discount = $this->subscription->promo_discount;
$invoice->is_amount_discount = $this->subscription->is_amount_discount;
- } elseif (strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) {
+ } elseif (strlen($this->subscription->promo_code ?? '') == 0 && $this->subscription->promo_discount > 0) {
$invoice->discount = $this->subscription->promo_discount;
$invoice->is_amount_discount = $this->subscription->is_amount_discount;
}
@@ -1118,7 +1118,7 @@ class SubscriptionService
*/
public function triggerWebhook($context)
{
- if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url']) < 1) { //@phpstan-ignore-line
+ if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url'] ?? '') < 1) { //@phpstan-ignore-line
return ["message" => "Success", "status_code" => 200];
}
@@ -1436,7 +1436,7 @@ class SubscriptionService
*/
public function handleNoPaymentFlow(Invoice $invoice, $bundle, ClientContact $contact)
{
- if (strlen($this->subscription->recurring_product_ids) >= 1) {
+ if (strlen($this->subscription->recurring_product_ids ?? '') >= 1) {
$recurring_invoice = $this->convertInvoiceToRecurringBundle($contact->client_id, collect($bundle)->map(function ($bund) {
return (object) $bund;
}));
@@ -1492,7 +1492,7 @@ class SubscriptionService
*/
private function handleRedirect($default_redirect)
{
- if (array_key_exists('return_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['return_url']) >= 1) {
+ if (array_key_exists('return_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['return_url'] ?? '') >= 1) {
return method_exists(redirect(), "send") ? redirect($this->subscription->webhook_configuration['return_url'])->send() : redirect($this->subscription->webhook_configuration['return_url']);
}
diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php
index 5a033c2f5383..13adb454d7f1 100644
--- a/app/Services/Template/TemplateService.php
+++ b/app/Services/Template/TemplateService.php
@@ -1023,7 +1023,8 @@ class TemplateService
'vat_number' => $project->client->vat_number ?? '',
'currency' => $project->client->currency()->code ?? 'USD',
] : [],
- 'user' => $this->userInfo($project->user)
+ 'user' => $this->userInfo($project->user),
+ 'invoices' => $this->processInvoices($project->invoices)
];
}
diff --git a/config/ninja.php b/config/ninja.php
index c832656df1e2..d2ff07a5bb05 100644
--- a/config/ninja.php
+++ b/config/ninja.php
@@ -17,8 +17,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
- 'app_version' => env('APP_VERSION', '5.10.13'),
- 'app_tag' => env('APP_TAG', '5.10.13'),
+ 'app_version' => env('APP_VERSION', '5.10.16'),
+ 'app_tag' => env('APP_TAG', '5.10.16'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),
diff --git a/lang/en/texts.php b/lang/en/texts.php
index 7e8a04578b82..eeab2f9c1673 100644
--- a/lang/en/texts.php
+++ b/lang/en/texts.php
@@ -5124,7 +5124,7 @@ $lang = array(
'all_contacts' => 'All Contacts',
'insert_below' => 'Insert Below',
'nordigen_handler_subtitle' => 'Bank account authentication. Selecting your institution to complete the request with your account credentials.',
- 'nordigen_handler_error_heading_unknown' => 'An error has occured',
+ 'nordigen_handler_error_heading_unknown' => 'An error has occurred',
'nordigen_handler_error_contents_unknown' => 'An unknown error has occurred! Reason:',
'nordigen_handler_error_heading_token_invalid' => 'Invalid Token',
'nordigen_handler_error_contents_token_invalid' => 'The provided token was invalid. Contact support for help, if this issue persists.',
diff --git a/public/build/assets/payment-1bdbd169.js b/public/build/assets/payment-1bdbd169.js
deleted file mode 100644
index c0133cd9aad5..000000000000
--- a/public/build/assets/payment-1bdbd169.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Invoice Ninja (https://invoiceninja.com)
- *
- * @link https://github.com/invoiceninja/invoiceninja source repository
- *
- * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
- *
- * @license https://www.elastic.co/licensing/elastic-license
- */class s{constructor(t,e,a){this.shouldDisplayTerms=t,this.shouldDisplaySignature=e,this.shouldDisplayRff=a,this.submitting=!1,this.steps=new Map,this.shouldDisplayRff&&this.steps.set("rff",{element:document.getElementById("displayRequiredFieldsModal"),nextButton:document.getElementById("rff-next-step"),callback:()=>{const n={firstName:document.querySelector('input[name="rff_first_name"]'),lastName:document.querySelector('input[name="rff_last_name"]'),email:document.querySelector('input[name="rff_email"]')};n.firstName&&(document.querySelector('input[name="contact_first_name"]').value=n.firstName.value),n.lastName&&(document.querySelector('input[name="contact_last_name"]').value=n.lastName.value),n.email&&(document.querySelector('input[name="contact_email"]').value=n.email.value)}}),this.shouldDisplaySignature&&this.steps.set("signature",{element:document.getElementById("displaySignatureModal"),nextButton:document.getElementById("signature-next-step"),boot:()=>this.signaturePad=new SignaturePad(document.getElementById("signature-pad"),{penColor:"rgb(0, 0, 0)"}),callback:()=>document.querySelector('input[name="signature"').value=this.signaturePad.toDataURL()}),this.shouldDisplayTerms&&this.steps.set("terms",{element:document.getElementById("displayTermsModal"),nextButton:document.getElementById("accept-terms-button")})}handleMethodSelect(t){if(document.getElementById("company_gateway_id").value=t.dataset.companyGatewayId,document.getElementById("payment_method_id").value=t.dataset.gatewayTypeId,this.steps.size===0)return this.submitForm();const e=this.steps.values().next().value;e.element.removeAttribute("style"),e.boot&&e.boot(),console.log(e),e.nextButton.addEventListener("click",()=>{e.element.setAttribute("style","display: none;"),this.steps=new Map(Array.from(this.steps.entries()).slice(1)),e.callback&&e.callback(),this.handleMethodSelect(t)})}submitForm(){this.submitting=!0,document.getElementById("payment-form").submit()}handle(){document.querySelectorAll(".dropdown-gateway-button").forEach(t=>{t.addEventListener("click",()=>{this.submitting||this.handleMethodSelect(t)})})}}const i=document.querySelector('meta[name="require-invoice-signature"]').content,o=document.querySelector('meta[name="show-invoice-terms"]').content,l=document.querySelector('meta[name="show-required-fields-form"]').content;new s(!!+o,!!+i,!!+l).handle();
diff --git a/public/build/assets/payment-292ee4d0.js b/public/build/assets/payment-292ee4d0.js
new file mode 100644
index 000000000000..d3f7782bcb95
--- /dev/null
+++ b/public/build/assets/payment-292ee4d0.js
@@ -0,0 +1,9 @@
+/**
+ * Invoice Ninja (https://invoiceninja.com)
+ *
+ * @link https://github.com/invoiceninja/invoiceninja source repository
+ *
+ * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
+ *
+ * @license https://www.elastic.co/licensing/elastic-license
+ */class a{constructor(t,n){this.shouldDisplayTerms=t,this.shouldDisplaySignature=n,this.submitting=!1,this.steps=new Map,this.steps.set("rff",{element:document.getElementById("displayRequiredFieldsModal"),nextButton:document.getElementById("rff-next-step"),callback:()=>{const e={firstName:document.querySelector('input[name="rff_first_name"]'),lastName:document.querySelector('input[name="rff_last_name"]'),email:document.querySelector('input[name="rff_email"]'),city:document.querySelector('input[name="rff_city"]'),postalCode:document.querySelector('input[name="rff_postal_code"]')};e.firstName&&(document.querySelector('input[name="contact_first_name"]').value=e.firstName.value),e.lastName&&(document.querySelector('input[name="contact_last_name"]').value=e.lastName.value),e.email&&(document.querySelector('input[name="contact_email"]').value=e.email.value),e.city&&(document.querySelector('input[name="client_city"]').value=e.city.value),e.postalCode&&(document.querySelector('input[name="client_postal_code"]').value=e.postalCode.value)}}),this.shouldDisplaySignature&&this.steps.set("signature",{element:document.getElementById("displaySignatureModal"),nextButton:document.getElementById("signature-next-step"),boot:()=>this.signaturePad=new SignaturePad(document.getElementById("signature-pad"),{penColor:"rgb(0, 0, 0)"}),callback:()=>document.querySelector('input[name="signature"').value=this.signaturePad.toDataURL()}),this.shouldDisplayTerms&&this.steps.set("terms",{element:document.getElementById("displayTermsModal"),nextButton:document.getElementById("accept-terms-button")})}handleMethodSelect(t){document.getElementById("company_gateway_id").value=t.dataset.companyGatewayId,document.getElementById("payment_method_id").value=t.dataset.gatewayTypeId;const n=document.querySelector('input[name="contact_first_name"').value.length>=1&&document.querySelector('input[name="contact_last_name"').value.length>=1&&document.querySelector('input[name="contact_email"').value.length>=1&&document.querySelector('input[name="client_city"').value.length>=1&&document.querySelector('input[name="client_postal_code"').value.length>=1;if((t.dataset.isPaypal!="1"||n)&&this.steps.delete("rff"),this.steps.size===0)return this.submitForm();const e=this.steps.values().next().value;e.element.removeAttribute("style"),e.boot&&e.boot(),console.log(e),e.nextButton.addEventListener("click",()=>{e.element.setAttribute("style","display: none;"),this.steps=new Map(Array.from(this.steps.entries()).slice(1)),e.callback&&e.callback(),this.handleMethodSelect(t)})}submitForm(){this.submitting=!0,document.getElementById("payment-form").submit()}handle(){document.querySelectorAll(".dropdown-gateway-button").forEach(t=>{t.addEventListener("click",()=>{this.submitting||this.handleMethodSelect(t)})})}}const l=document.querySelector('meta[name="require-invoice-signature"]').content,o=document.querySelector('meta[name="show-invoice-terms"]').content;new a(!!+o,!!+l).handle();
diff --git a/public/build/manifest.json b/public/build/manifest.json
index 5052761badb2..f6047f4d4c3c 100644
--- a/public/build/manifest.json
+++ b/public/build/manifest.json
@@ -23,7 +23,7 @@
"src": "resources/js/clients/invoices/action-selectors.js"
},
"resources/js/clients/invoices/payment.js": {
- "file": "assets/payment-1bdbd169.js",
+ "file": "assets/payment-292ee4d0.js",
"isEntry": true,
"src": "resources/js/clients/invoices/payment.js"
},
@@ -240,7 +240,7 @@
"src": "resources/js/setup/setup.js"
},
"resources/sass/app.scss": {
- "file": "assets/app-02bc3b96.css",
+ "file": "assets/app-039bd735.css",
"isEntry": true,
"src": "resources/sass/app.scss"
}
diff --git a/resources/js/clients/invoices/payment.js b/resources/js/clients/invoices/payment.js
index a384f94dcfa0..1026dadeb593 100644
--- a/resources/js/clients/invoices/payment.js
+++ b/resources/js/clients/invoices/payment.js
@@ -9,39 +9,47 @@
*/
class Payment {
- constructor(displayTerms, displaySignature, displayRff) {
+ constructor(displayTerms, displaySignature) {
this.shouldDisplayTerms = displayTerms;
this.shouldDisplaySignature = displaySignature;
- this.shouldDisplayRff = displayRff;
-
+
this.submitting = false;
this.steps = new Map()
- if (this.shouldDisplayRff) {
- this.steps.set("rff", {
- element: document.getElementById('displayRequiredFieldsModal'),
- nextButton: document.getElementById('rff-next-step'),
- callback: () => {
- const fields = {
- firstName: document.querySelector('input[name="rff_first_name"]'),
- lastName: document.querySelector('input[name="rff_last_name"]'),
- email: document.querySelector('input[name="rff_email"]'),
- }
-
- if (fields.firstName) {
- document.querySelector('input[name="contact_first_name"]').value = fields.firstName.value;
- }
-
- if (fields.lastName) {
- document.querySelector('input[name="contact_last_name"]').value = fields.lastName.value;
- }
-
- if (fields.email) {
- document.querySelector('input[name="contact_email"]').value = fields.email.value;
- }
+ this.steps.set("rff", {
+ element: document.getElementById('displayRequiredFieldsModal'),
+ nextButton: document.getElementById('rff-next-step'),
+ callback: () => {
+ const fields = {
+ firstName: document.querySelector('input[name="rff_first_name"]'),
+ lastName: document.querySelector('input[name="rff_last_name"]'),
+ email: document.querySelector('input[name="rff_email"]'),
+ city: document.querySelector('input[name="rff_city"]'),
+ postalCode: document.querySelector('input[name="rff_postal_code"]'),
}
- });
- }
+
+ if (fields.firstName) {
+ document.querySelector('input[name="contact_first_name"]').value = fields.firstName.value;
+ }
+
+ if (fields.lastName) {
+ document.querySelector('input[name="contact_last_name"]').value = fields.lastName.value;
+ }
+
+ if (fields.email) {
+ document.querySelector('input[name="contact_email"]').value = fields.email.value;
+ }
+
+ if (fields.city) {
+ document.querySelector('input[name="client_city"]').value = fields.city.value;
+ }
+
+ if (fields.postalCode) {
+ document.querySelector('input[name="client_postal_code"]').value = fields.postalCode.value;
+ }
+
+ }
+ });
if (this.shouldDisplaySignature) {
this.steps.set("signature", {
@@ -71,7 +79,17 @@ class Payment {
element.dataset.companyGatewayId;
document.getElementById("payment_method_id").value =
element.dataset.gatewayTypeId;
-
+
+ const filledRff = document.querySelector('input[name="contact_first_name"').value.length >=1 &&
+ document.querySelector('input[name="contact_last_name"').value.length >= 1 &&
+ document.querySelector('input[name="contact_email"').value.length >= 1 &&
+ document.querySelector('input[name="client_city"').value.length >= 1 &&
+ document.querySelector('input[name="client_postal_code"').value.length >= 1;
+
+ if (element.dataset.isPaypal != '1' || filledRff) {
+ this.steps.delete("rff");
+ }
+
if (this.steps.size === 0) {
return this.submitForm();
}
@@ -124,6 +142,5 @@ const signature = document.querySelector(
).content;
const terms = document.querySelector('meta[name="show-invoice-terms"]').content;
-const rff = document.querySelector('meta[name="show-required-fields-form"]').content;
-new Payment(Boolean(+terms), Boolean(+signature), Boolean(+rff)).handle();
+new Payment(Boolean(+terms), Boolean(+signature)).handle();
diff --git a/resources/views/portal/ninja2020/components/livewire/billing-portal-purchase.blade.php b/resources/views/portal/ninja2020/components/livewire/billing-portal-purchase.blade.php
index 3c97e48959be..4af98edc0409 100644
--- a/resources/views/portal/ninja2020/components/livewire/billing-portal-purchase.blade.php
+++ b/resources/views/portal/ninja2020/components/livewire/billing-portal-purchase.blade.php
@@ -141,12 +141,15 @@
+
+
+
@if($steps['started_payment'] == false)
@foreach($this->methods as $method)
@@ -189,27 +192,41 @@
{{ $error }}
+ @endforeach +