diff --git a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php index 28d01f5506fb..b5dd220a68b3 100644 --- a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php @@ -32,28 +32,72 @@ class StoreRecurringInvoiceRequest extends Request return auth()->user()->can('create', RecurringInvoice::class); } + public function rules() { - return [ - 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', - 'client_id' => 'required|exists:clients,id,company_id,'.auth()->user()->company()->id, - ]; + $rules = []; + + if ($this->input('documents') && is_array($this->input('documents'))) { + $documents = count($this->input('documents')); + + foreach (range(0, $documents) as $index) { + $rules['documents.' . $index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + } elseif ($this->input('documents')) { + $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + $rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; + + $rules['invitations.*.client_contact_id'] = 'distinct'; + + $rules['frequency_id'] = 'required,integer'; + + return $rules; } + protected function prepareForValidation() { $input = $this->all(); - if ($input['client_id']) { + if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { + $input['design_id'] = $this->decodePrimaryKey($input['design_id']); + } + + if (array_key_exists('client_id', $input) && is_string($input['client_id'])) { $input['client_id'] = $this->decodePrimaryKey($input['client_id']); } - + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); } - + if (isset($input['client_contacts'])) { + foreach ($input['client_contacts'] as $key => $contact) { + if (!array_key_exists('send_email', $contact) || !array_key_exists('id', $contact)) { + unset($input['client_contacts'][$key]); + } + } + } + + if (isset($input['invitations'])) { + foreach ($input['invitations'] as $key => $value) { + if (isset($input['invitations'][$key]['id']) && is_numeric($input['invitations'][$key]['id'])) { + unset($input['invitations'][$key]['id']); + } + + if (isset($input['invitations'][$key]['id']) && is_string($input['invitations'][$key]['id'])) { + $input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']); + } + + if (is_string($input['invitations'][$key]['client_contact_id'])) { + $input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']); + } + } + } + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; //$input['line_items'] = json_encode($input['line_items']); $this->replace($input); diff --git a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php index 2ea44a5d069e..41517772f86e 100644 --- a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php @@ -16,11 +16,13 @@ use App\Utils\Traits\ChecksEntityStatus; use App\Utils\Traits\CleanLineItems; use Illuminate\Support\Facades\Log; use Illuminate\Validation\Rule; +use App\Utils\Traits\MakesHash; class UpdateRecurringInvoiceRequest extends Request { use ChecksEntityStatus; use CleanLineItems; + use MakesHash; /** * Determine if the user is authorized to make this request. * @@ -35,22 +37,56 @@ class UpdateRecurringInvoiceRequest extends Request public function rules() { - return [ - 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', + $rules = []; - ]; + if ($this->input('documents') && is_array($this->input('documents'))) { + $documents = count($this->input('documents')); + + foreach (range(0, $documents) as $index) { + $rules['documents.' . $index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + } elseif ($this->input('documents')) { + $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + return $rules; } protected function prepareForValidation() { $input = $this->all(); +info($input); + if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { + $input['design_id'] = $this->decodePrimaryKey($input['design_id']); + } + + if (isset($input['client_id'])) { + $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + } if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); } + if (isset($input['invitations'])) { + foreach ($input['invitations'] as $key => $value) { + if (is_numeric($input['invitations'][$key]['id'])) { + unset($input['invitations'][$key]['id']); + + } + + if (array_key_exists('id', $input['invitations'][$key]) && is_string($input['invitations'][$key]['id'])) { + $input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']); + } + + if (is_string($input['invitations'][$key]['client_contact_id'])) { + $input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']); + } + } + } + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; - //$input['line_items'] = json_encode($input['line_items']); + $this->replace($input); } } diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index bcf921576e78..3986ea1497e9 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -16,6 +16,7 @@ use App\Exceptions\MigrationValidatorFailed; use App\Exceptions\ResourceDependencyMissing; use App\Exceptions\ResourceNotAvailableForMigration; use App\Factory\ClientFactory; +use App\Factory\CompanyLedgerFactory; use App\Factory\CreditFactory; use App\Factory\InvoiceFactory; use App\Factory\PaymentFactory; @@ -30,6 +31,7 @@ use App\Jobs\Company\CreateCompanyToken; use App\Libraries\MultiDB; use App\Mail\MigrationCompleted; use App\Mail\MigrationFailed; +use App\Models\Activity; use App\Models\Client; use App\Models\ClientContact; use App\Models\ClientGatewayToken; @@ -49,6 +51,8 @@ use App\Repositories\ClientRepository; use App\Repositories\CompanyRepository; use App\Repositories\CreditRepository; use App\Repositories\InvoiceRepository; +use App\Repositories\Migration\InvoiceMigrationRepository; +use App\Repositories\Migration\PaymentMigrationRepository; use App\Repositories\PaymentRepository; use App\Repositories\ProductRepository; use App\Repositories\QuoteRepository; @@ -167,6 +171,8 @@ class Import implements ShouldQueue $this->{$method}($resource); } + $this->setInitialCompanyLedgerBalances(); + Mail::to($this->user)->send(new MigrationCompleted()); info('CompletedπŸš€πŸš€πŸš€πŸš€πŸš€ at '.now()); @@ -174,6 +180,25 @@ class Import implements ShouldQueue return true; } + private function setInitialCompanyLedgerBalances() + { + + Client::cursor()->each(function ($client){ + + $company_ledger = CompanyLedgerFactory::create($client->company_id, $client->user_id); + $company_ledger->client_id = $client->id; + $company_ledger->adjustment = $client->balance; + $company_ledger->notes = 'Migrated Client Balance'; + $company_ledger->balance = $client->balance; + $company_ledger->activity_id = Activity::CREATE_CLIENT; + $company_ledger->save(); + + $client->company_ledger()->save($company_ledger); + + }); + + } + /** * @param array $data * @throws \Exception @@ -448,7 +473,7 @@ class Import implements ShouldQueue throw new MigrationValidatorFailed(json_encode($validator->errors())); } - $invoice_repository = new InvoiceRepository(); + $invoice_repository = new InvoiceMigrationRepository(); foreach ($data as $key => $resource) { @@ -602,7 +627,7 @@ class Import implements ShouldQueue throw new MigrationValidatorFailed(json_encode($validator->errors())); } - $payment_repository = new PaymentRepository(new CreditRepository()); + $payment_repository = new PaymentMigrationRepository(new CreditRepository()); foreach ($data as $resource) { $modified = $resource; @@ -621,8 +646,8 @@ class Import implements ShouldQueue if (isset($modified['invoices'])) { - foreach ($modified['invoices'] as $invoice) { - $invoice['invoice_id'] = $this->transformId('invoices', $invoice['invoice_id']); + foreach ($modified['invoices'] as $key => $invoice) { + $modified['invoices'][$key]['invoice_id'] = $this->transformId('invoices', $invoice['invoice_id']); } } diff --git a/app/Jobs/Util/SubscriptionHandler.php b/app/Jobs/Util/SubscriptionHandler.php index fb944ce7da5e..a35ad135a682 100644 --- a/app/Jobs/Util/SubscriptionHandler.php +++ b/app/Jobs/Util/SubscriptionHandler.php @@ -99,4 +99,10 @@ class SubscriptionHandler implements ShouldQueue $subscription->delete(); } } + + public function failed($exception) + { + $exception->getMessage(); + // etc... + } } diff --git a/app/Models/Client.php b/app/Models/Client.php index 79457ea7d864..42a56cacdb85 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -13,9 +13,11 @@ namespace App\Models; use App\DataMapper\ClientSettings; use App\DataMapper\CompanySettings; +use App\Factory\CompanyLedgerFactory; use App\Factory\CreditFactory; use App\Factory\InvoiceFactory; use App\Factory\QuoteFactory; +use App\Models\Activity; use App\Models\Company; use App\Models\CompanyGateway; use App\Models\Country; @@ -124,6 +126,11 @@ class Client extends BaseModel implements HasLocalePreference return $this->hasMany(CompanyLedger::class)->orderBy('id', 'desc'); } + public function company_ledger() + { + return $this->morphMany(CompanyLedger::class, 'company_ledgerable'); + } + public function gateway_tokens() { return $this->hasMany(ClientGatewayToken::class); diff --git a/app/Models/RecurringInvoice.php b/app/Models/RecurringInvoice.php index 8d16415ca94f..89d89724d42d 100644 --- a/app/Models/RecurringInvoice.php +++ b/app/Models/RecurringInvoice.php @@ -192,15 +192,15 @@ class RecurringInvoice extends BaseModel case RecurringInvoice::FREQUENCY_FOUR_WEEKS: return Carbon::parse($this->next_send_date->addWeeks(4)); case RecurringInvoice::FREQUENCY_MONTHLY: - return Carbon::parse($this->next_send_date->addMonth()); + return Carbon::parse($this->next_send_date->addMonthNoOverflow()); case RecurringInvoice::FREQUENCY_TWO_MONTHS: - return Carbon::parse($this->next_send_date->addMonths(2)); + return Carbon::parse($this->next_send_date->addMonthsNoOverflow(2)); case RecurringInvoice::FREQUENCY_THREE_MONTHS: - return Carbon::parse($this->next_send_date->addMonths(3)); + return Carbon::parse($this->next_send_date->addMonthsNoOverflow(3)); case RecurringInvoice::FREQUENCY_FOUR_MONTHS: - return Carbon::parse($this->next_send_date->addMonths(4)); + return Carbon::parse($this->next_send_date->addMonthsNoOverflow(4)); case RecurringInvoice::FREQUENCY_SIX_MONTHS: - return Carbon::parse($this->next_send_date->addMonths(6)); + return Carbon::parse($this->next_send_date->addMonthsNoOverflow(6)); case RecurringInvoice::FREQUENCY_ANNUALLY: return Carbon::parse($this->next_send_date->addYear()); case RecurringInvoice::FREQUENCY_TWO_YEARS: diff --git a/app/Observers/ClientObserver.php b/app/Observers/ClientObserver.php index 3a5d86512ef7..e4d93ed15807 100644 --- a/app/Observers/ClientObserver.php +++ b/app/Observers/ClientObserver.php @@ -38,7 +38,7 @@ class ClientObserver * @return void */ public function updated(Client $client) - { + { SubscriptionHandler::dispatch(Subscription::EVENT_UPDATE_CLIENT, $client); } diff --git a/app/Repositories/ClientRepository.php b/app/Repositories/ClientRepository.php index 72cdf83b0605..49d46d692024 100644 --- a/app/Repositories/ClientRepository.php +++ b/app/Repositories/ClientRepository.php @@ -78,6 +78,8 @@ class ClientRepository extends BaseRepository $data['name'] = $client->present()->name(); } + info("{$client->present()->name} has a balance of {$client->balance} with a paid to date of {$client->paid_to_date}"); + if (array_key_exists('documents', $data)) { $this->saveDocuments($data['documents'], $client); } diff --git a/app/Repositories/Migration/InvoiceMigrationRepository.php b/app/Repositories/Migration/InvoiceMigrationRepository.php new file mode 100644 index 000000000000..ea3f35e2f14e --- /dev/null +++ b/app/Repositories/Migration/InvoiceMigrationRepository.php @@ -0,0 +1,183 @@ +client_id); + } + + $state = []; + $resource = explode('\\', $class->name)[2]; /** This will extract 'Invoice' from App\Models\Invoice */ + $lcfirst_resource_id = lcfirst($resource) . '_id'; + + if ($class->name == Invoice::class || $class->name == Quote::class) { + $state['starting_amount'] = $model->amount; + } + + if (!$model->id) { + $company_defaults = $client->setCompanyDefaults($data, lcfirst($resource)); + $model->uses_inclusive_taxes = $client->getSetting('inclusive_taxes'); + $data = array_merge($company_defaults, $data); + } + + $tmp_data = $data; + + /* We need to unset some variable as we sometimes unguard the model */ + + if (isset($tmp_data['invitations'])) { + unset($tmp_data['invitations']); + } + + if (isset($tmp_data['client_contacts'])) { + unset($tmp_data['client_contacts']); + } + + $model->fill($tmp_data); + $model->save(); + + if (array_key_exists('documents', $data)) { + $this->saveDocuments($data['documents'], $model); + } + + $invitation_factory_class = sprintf("App\\Factory\\%sInvitationFactory", $resource); + + if (isset($data['client_contacts'])) { + foreach ($data['client_contacts'] as $contact) { + if ($contact['send_email'] == 1 && is_string($contact['id'])) { + $client_contact = ClientContact::find($this->decodePrimaryKey($contact['id'])); + $client_contact->send_email = true; + $client_contact->save(); + } + } + } + + if (isset($data['invitations'])) { + $invitations = collect($data['invitations']); + + /* Get array of Keys which have been removed from the invitations array and soft delete each invitation */ + $model->invitations->pluck('key')->diff($invitations->pluck('key'))->each(function ($invitation) { + $this->getInvitation($invitation, $resource)->delete(); + }); + + foreach ($data['invitations'] as $invitation) { + + //if no invitations are present - create one. + if (! $this->getInvitation($invitation, $resource)) { + if (isset($invitation['id'])) { + unset($invitation['id']); + } + + //make sure we are creating an invite for a contact who belongs to the client only! + $contact = ClientContact::find($invitation['client_contact_id']); + + if ($contact && $model->client_id == $contact->client_id); + { + $new_invitation = $invitation_factory_class::create($model->company_id, $model->user_id); + $new_invitation->{$lcfirst_resource_id} = $model->id; + $new_invitation->client_contact_id = $contact->id; + $new_invitation->save(); + } + } + } + } + + $model->load('invitations'); + + /* If no invitations have been created, this is our fail safe to maintain state*/ + if ($model->invitations->count() == 0) { + $model->service()->createInvitations(); + } + + $model = $model->calc()->getInvoice(); + + $state['finished_amount'] = $model->amount; + + $model = $model->service()->applyNumber()->save(); + + if ($model->company->update_products !== false) { + UpdateOrCreateProduct::dispatch($model->line_items, $model, $model->company); + } + + if ($class->name == Invoice::class) { + + if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) { + + // $model->ledger()->updateInvoiceBalance(($state['finished_amount'] - $state['starting_amount'])); + // $model->client->service()->updateBalance(($state['finished_amount'] - $state['starting_amount']))->save(); + } + + if(!$model->design_id) + $model->design_id = $this->decodePrimaryKey($client->getSetting('invoice_design_id')); + + } + + if ($class->name == Credit::class) { + $model = $model->calc()->getCredit(); + + if(!$model->design_id) + $model->design_id = $this->decodePrimaryKey($client->getSetting('credit_design_id')); + + + } + + if ($class->name == Quote::class) { + $model = $model->calc()->getQuote(); + + if(!$model->design_id) + $model->design_id = $this->decodePrimaryKey($client->getSetting('quote_design_id')); + + + + } + + $model->save(); + + return $model->fresh(); + } + +} diff --git a/app/Repositories/Migration/PaymentMigrationRepository.php b/app/Repositories/Migration/PaymentMigrationRepository.php new file mode 100644 index 000000000000..2141646269c8 --- /dev/null +++ b/app/Repositories/Migration/PaymentMigrationRepository.php @@ -0,0 +1,194 @@ +credit_repo = $credit_repo; + $this->activity_repo = new ActivityRepository(); + } + + public function getClassName() + { + return Payment::class; + } + + /** + * Saves and updates a payment. //todo refactor to handle refunds and payments. + * + * @param array $data the request object + * @param Payment $payment The Payment object + * @return Payment|null Payment $payment + */ + public function save(array $data, Payment $payment): ?Payment + { + if ($payment->amount >= 0) { + return $this->applyPayment($data, $payment); + } + + return $payment; + } + + /** + * Handles a positive payment request + * @param array $data The data object + * @param Payment $payment The $payment entity + * @return Payment The updated/created payment object + */ + private function applyPayment(array $data, Payment $payment): ?Payment + { + + //check currencies here and fill the exchange rate data if necessary + if (!$payment->id) { + $this->processExchangeRates($data, $payment); + + /*We only update the paid to date ONCE per payment*/ + if (array_key_exists('invoices', $data) && is_array($data['invoices']) && count($data['invoices']) > 0) { + + if($data['amount'] == '') + $data['amount'] = array_sum(array_column($data['invoices'], 'amount')); + + } + } + + /*Fill the payment*/ + $payment->fill($data); + $payment->status_id = Payment::STATUS_COMPLETED; + $payment->save(); + + /*Ensure payment number generated*/ + if (!$payment->number || strlen($payment->number) == 0) { + $payment->number = $payment->client->getNextPaymentNumber($payment->client); + } + + $invoice_totals = 0; + $credit_totals = 0; + + /*Iterate through invoices and apply payments*/ + if (array_key_exists('invoices', $data) && is_array($data['invoices']) && count($data['invoices']) > 0) { + + $invoice_totals = array_sum(array_column($data['invoices'], 'amount')); + + $invoices = Invoice::whereIn('id', array_column($data['invoices'], 'invoice_id'))->get(); + + $payment->invoices()->saveMany($invoices); + + $payment->invoices->each(function ($inv) use($invoice_totals){ + $inv->pivot->amount = $invoice_totals; + $inv->pivot->save(); + }); + + } + + $fields = new \stdClass; + + $fields->payment_id = $payment->id; + $fields->user_id = $payment->user_id; + $fields->company_id = $payment->company_id; + $fields->activity_type_id = Activity::CREATE_PAYMENT; + + foreach ($payment->invoices as $invoice) { + $fields->invoice_id = $invoice->id; + + $this->activity_repo->save($fields, $invoice); + } + + if (count($invoices) == 0) { + $this->activity_repo->save($fields, $payment); + } + + if ($invoice_totals == $payment->amount) { + $payment->applied += $payment->amount; + } elseif ($invoice_totals < $payment->amount) { + $payment->applied += $invoice_totals; + } + + $payment->save(); + + return $payment->fresh(); + } + + + /** + * If the client is paying in a currency other than + * the company currency, we need to set a record + */ + private function processExchangeRates($data, $payment) + { + + $client = Client::find($data['client_id']); + + $client_currency = $client->getSetting('currency_id'); + $company_currency = $client->company->settings->currency_id; + + if ($company_currency != $client_currency) { + $currency = $client->currency(); + + $exchange_rate = new CurrencyApi(); + + $payment->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, Carbon::parse($payment->date)); + $payment->exchange_currency_id = $client_currency; + } + + return $payment; + } + + public function delete($payment) + { + //cannot double delete a payment + if($payment->is_deleted) + return; + + $payment->service()->deletePayment(); + + return parent::delete($payment); + + } + + public function restore($payment) + { + //we cannot restore a deleted payment. + if($payment->is_deleted) + return; + + return parent::restore($payment); + } +} diff --git a/app/Repositories/PaymentRepository.php b/app/Repositories/PaymentRepository.php index 02f4dfc5542d..e2b0004c6465 100644 --- a/app/Repositories/PaymentRepository.php +++ b/app/Repositories/PaymentRepository.php @@ -81,6 +81,8 @@ class PaymentRepository extends BaseRepository $data['amount'] = array_sum(array_column($data['invoices'], 'amount')); $client = Client::find($data['client_id']); + info("updating client balance from {$client->balance} by this much ".$data['amount']); + $client->service()->updatePaidToDate($data['amount'])->save(); } @@ -108,6 +110,8 @@ class PaymentRepository extends BaseRepository $invoice_totals = array_sum(array_column($data['invoices'], 'amount')); $invoices = Invoice::whereIn('id', array_column($data['invoices'], 'invoice_id'))->get(); + + info("saving this many invoices to the payment ".$invoices->count()); $payment->invoices()->saveMany($invoices); @@ -126,7 +130,8 @@ class PaymentRepository extends BaseRepository } } else { //payment is made, but not to any invoice, therefore we are applying the payment to the clients paid_to_date only - $payment->client->service()->updatePaidToDate($payment->amount)->save(); + //01-07-2020 i think we were duplicating the paid to date here. + //$payment->client->service()->updatePaidToDate($payment->amount)->save(); } if (array_key_exists('credits', $data) && is_array($data['credits'])) { diff --git a/app/Repositories/RecurringInvoiceRepository.php b/app/Repositories/RecurringInvoiceRepository.php index adb90cf743f7..7df37e577da3 100644 --- a/app/Repositories/RecurringInvoiceRepository.php +++ b/app/Repositories/RecurringInvoiceRepository.php @@ -34,9 +34,6 @@ class RecurringInvoiceRepository extends BaseRepository $invoice_calc = new InvoiceSum($invoice, $invoice->settings); $invoice = $invoice_calc->build()->getInvoice(); - - //fire events here that cascading from the saving of an invoice - //ie. client balance update... return $invoice; } diff --git a/app/Services/Credit/ApplyNumber.php b/app/Services/Credit/ApplyNumber.php index 387b708109d1..d77fef7c88d8 100644 --- a/app/Services/Credit/ApplyNumber.php +++ b/app/Services/Credit/ApplyNumber.php @@ -1,4 +1,14 @@ RecurringInvoice::STATUS_DRAFT, - 'client_id' => $RecurringInvoice->client_id, + 'client_id' => $this->encodePrimaryKey($RecurringInvoice->client_id), ]; $this->assertNotNull($RecurringInvoice);