diff --git a/app/Domains/Sales/Application/SerialNumberService.php b/app/Domains/Sales/Application/SerialNumberService.php deleted file mode 100644 index aa648322..00000000 --- a/app/Domains/Sales/Application/SerialNumberService.php +++ /dev/null @@ -1,269 +0,0 @@ -model = $model; - - return $this; - } - - public function setModelObject($id = null) - { - $this->ob = $this->model::find($id); - - if ($this->ob && $this->ob->sequence_number) { - $this->nextSequenceNumber = $this->ob->sequence_number; - } - - if (isset($this->ob) && isset($this->ob->customer_sequence_number) && isset($this->customer) && $this->ob->customer_id == $this->customer->id) { - $this->nextCustomerSequenceNumber = $this->ob->customer_sequence_number; - } - - return $this; - } - - /** - * @return $this - */ - public function setCompany($company) - { - $this->company = $company; - - return $this; - } - - /** - * @return $this - */ - public function setCustomer($customer = null) - { - $this->customer = Customer::find($customer); - - return $this; - } - - /** - * Override the company setting the number format is read from. - * - * Without this the key is derived from the model class name, which is not - * enough for documents that share a table (credit notes are Invoice rows - * but carry their own format). - * - * @return $this - */ - public function setSettingKey(string $key) - { - $this->settingKey = $key; - - return $this; - } - - /** - * Restrict the sequence lookups to a subset of the model's rows. - * - * Takes column => value constraints that are applied on top of the company - * (and customer) filters, so documents sharing a table can each keep an - * independent, gapless sequence. - * - * @return $this - */ - public function setSequenceScope(array $constraints) - { - $this->sequenceScope = $constraints; - - return $this; - } - - /** - * @return string - */ - public function getNextNumber(?string $format = null) - { - $modelName = strtolower(class_basename($this->model)); - $settingKey = $this->settingKey ?: $modelName.'_number_format'; - $companyId = $this->company; - - if ($format === null) { - $format = CompanySetting::getSetting( - $settingKey, - $companyId - ); - } - $this->setNextNumbers(); - - $serialNumber = $this->generateSerialNumber( - $format - ); - - return $serialNumber; - } - - public function setNextNumbers() - { - $this->nextSequenceNumber ? - $this->nextSequenceNumber : $this->setNextSequenceNumber(); - - $this->nextCustomerSequenceNumber ? - $this->nextCustomerSequenceNumber : $this->setNextCustomerSequenceNumber(); - - return $this; - } - - /** - * @return $this - */ - public function setNextSequenceNumber() - { - $companyId = $this->company; - - $query = $this->model::orderBy('sequence_number', 'desc') - ->where('company_id', $companyId) - ->where('sequence_number', '<>', null); - - foreach ($this->sequenceScope as $column => $value) { - $query->where($column, $value); - } - - $last = $query->take(1)->first(); - - $this->nextSequenceNumber = ($last) ? $last->sequence_number + 1 : 1; - - return $this; - } - - /** - * @return self - */ - public function setNextCustomerSequenceNumber() - { - $customer_id = ($this->customer) ? $this->customer->id : 1; - - $query = $this->model::orderBy('customer_sequence_number', 'desc') - ->where('company_id', $this->company) - ->where('customer_id', $customer_id) - ->where('customer_sequence_number', '<>', null); - - foreach ($this->sequenceScope as $column => $value) { - $query->where($column, $value); - } - - $last = $query->take(1)->first(); - - $this->nextCustomerSequenceNumber = ($last) ? $last->customer_sequence_number + 1 : 1; - - return $this; - } - - public static function getPlaceholders(string $format) - { - $regex = '/{{([A-Z_]{1,})(?::)?([a-zA-Z0-9_]{1,6}|.{1})?}}/'; - - preg_match_all($regex, $format, $placeholders); - array_shift($placeholders); - $validPlaceholders = collect(); - - /** @var array */ - $mappedPlaceholders = array_map( - null, - current($placeholders), - end($placeholders) - ); - - foreach ($mappedPlaceholders as $placeholder) { - $name = current($placeholder); - $value = end($placeholder); - - if (in_array($name, self::VALID_PLACEHOLDERS)) { - $validPlaceholders->push([ - 'name' => $name, - 'value' => $value, - ]); - } - } - - return $validPlaceholders; - } - - /** - * @return string - */ - private function generateSerialNumber(string $format) - { - $serialNumber = ''; - - $placeholders = self::getPlaceholders($format); - - foreach ($placeholders as $placeholder) { - $name = $placeholder['name']; - $value = $placeholder['value']; - - switch ($name) { - case 'SEQUENCE': - $value = $value ? $value : 6; - $serialNumber .= str_pad($this->nextSequenceNumber, $value, 0, STR_PAD_LEFT); - - break; - case 'DATE_FORMAT': - $value = $value ? $value : 'Y'; - $serialNumber .= date($value); - - break; - case 'RANDOM_SEQUENCE': - $value = $value ? $value : 6; - $serialNumber .= substr(bin2hex(random_bytes($value)), 0, $value); - - break; - case 'CUSTOMER_SERIES': - if (isset($this->customer)) { - $serialNumber .= $this->customer->prefix ?? 'CST'; - } else { - $serialNumber .= 'CST'; - } - - break; - case 'CUSTOMER_SEQUENCE': - $serialNumber .= str_pad($this->nextCustomerSequenceNumber, $value, 0, STR_PAD_LEFT); - - break; - default: - $serialNumber .= $value; - } - } - - return $serialNumber; - } -} diff --git a/app/Domains/Sales/Console/CheckEstimateStatus.php b/app/Domains/Sales/Console/CheckEstimateStatus.php deleted file mode 100644 index 55a5aed9..00000000 --- a/app/Domains/Sales/Console/CheckEstimateStatus.php +++ /dev/null @@ -1,52 +0,0 @@ -whereDate('expiry_date', '<', $date)->get(); - - foreach ($estimates as $estimate) { - $estimate->status = Estimate::STATUS_EXPIRED; - printf("Estimate %s is EXPIRED \n", $estimate->estimate_number); - $estimate->save(); - } - } -} diff --git a/app/Domains/Sales/Console/CheckInvoiceStatus.php b/app/Domains/Sales/Console/CheckInvoiceStatus.php deleted file mode 100644 index 65433073..00000000 --- a/app/Domains/Sales/Console/CheckInvoiceStatus.php +++ /dev/null @@ -1,57 +0,0 @@ -whereNotIn('status', [Invoice::STATUS_COMPLETED, Invoice::STATUS_DRAFT]) - ->where('overdue', false) - ->whereDate('due_date', '<', $date) - ->get(); - - foreach ($invoices as $invoice) { - $invoice->overdue = true; - printf("Invoice %s is OVERDUE \n", $invoice->invoice_number); - $invoice->save(); - } - } -} diff --git a/app/Domains/Sales/Http/Controllers/Company/EstimateTemplatesController.php b/app/Domains/Sales/Http/Controllers/Company/EstimateTemplatesController.php deleted file mode 100644 index b54ef0a7..00000000 --- a/app/Domains/Sales/Http/Controllers/Company/EstimateTemplatesController.php +++ /dev/null @@ -1,28 +0,0 @@ -authorize('viewAny', Estimate::class); - - $estimateTemplates = PdfTemplateUtils::getFormattedTemplates('estimate'); - - return response()->json([ - 'estimateTemplates' => $estimateTemplates, - ]); - } -} diff --git a/app/Domains/Sales/Http/Controllers/Company/EstimatesController.php b/app/Domains/Sales/Http/Controllers/Company/EstimatesController.php deleted file mode 100644 index 51e400b1..00000000 --- a/app/Domains/Sales/Http/Controllers/Company/EstimatesController.php +++ /dev/null @@ -1,162 +0,0 @@ -authorize('viewAny', Estimate::class); - - $limit = $request->has('limit') ? $request->limit : 10; - - $estimates = Estimate::whereCompany() - ->join('customers', 'customers.id', '=', 'estimates.customer_id') - ->applyFilters($request->all()) - ->select('estimates.*', 'customers.name') - ->latest() - ->paginateData($limit); - - return EstimateResource::collection($estimates) - ->additional(['meta' => [ - 'estimate_total_count' => Estimate::whereCompany()->count(), - ]]); - } - - public function store(EstimatesRequest $request) - { - $this->authorize('create', Estimate::class); - - $estimate = $this->estimateService->create( - attributes: $request->getEstimatePayload(), - items: $request->input('items'), - taxes: $request->has('taxes') ? $request->input('taxes') : null, - customFields: $this->customFields($request), - ); - - if ($request->has('estimateSend')) { - $this->estimateService->send($estimate, $request->only(['title', 'body'])); - } - - GenerateEstimatePdfJob::dispatch($estimate); - - return new EstimateResource($estimate); - } - - public function show(Request $request, Estimate $estimate) - { - $this->authorize('view', $estimate); - - return new EstimateResource($estimate); - } - - public function update(EstimatesRequest $request, Estimate $estimate) - { - $this->authorize('update', $estimate); - - $estimate = $this->estimateService->update( - estimate: $estimate, - attributes: $request->getEstimatePayload(), - items: $request->input('items'), - taxes: $request->has('taxes') ? $request->input('taxes') : null, - customFields: $this->customFields($request), - ); - - GenerateEstimatePdfJob::dispatch($estimate, true); - - return new EstimateResource($estimate); - } - - public function delete(DeleteEstimatesRequest $request) - { - $this->authorize('delete multiple estimates'); - - $ids = Estimate::whereCompany() - ->whereIn('id', $request->ids) - ->pluck('id'); - - Estimate::destroy($ids); - - return response()->json([ - 'success' => true, - ]); - } - - public function send(SendEstimatesRequest $request, Estimate $estimate) - { - $this->authorize('send estimate', $estimate); - - $response = $this->estimateService->send($estimate, $request->all()); - - return response()->json($response); - } - - public function sendPreview(SendEstimatesRequest $request, Estimate $estimate) - { - $this->authorize('send estimate', $estimate); - - $markdown = new Markdown(view(), config('mail.markdown')); - - $data = $this->estimateService->sendEstimateData($estimate, $request->all()); - $data['url'] = $estimate->estimatePdfUrl; - - return $markdown->render('emails.send.estimate', ['data' => $data]); - } - - public function clone(Request $request, Estimate $estimate) - { - $this->authorize('view', $estimate); - $this->authorize('create', Estimate::class); - - $newEstimate = $this->estimateService->clone($estimate); - - return new EstimateResource($newEstimate); - } - - public function convertToInvoice(Request $request, Estimate $estimate) - { - // Authorize access to the source estimate (tenant isolation) in addition - // to the ability to create an invoice. - $this->authorize('view', $estimate); - $this->authorize('create', Invoice::class); - - $invoice = $this->estimateService->convertToInvoice($estimate); - - return new InvoiceResource($invoice); - } - - public function changeStatus(Request $request, Estimate $estimate) - { - $this->authorize('send estimate', $estimate); - - $this->estimateService->changeStatus($estimate, $request->status); - - return response()->json([ - 'success' => true, - ]); - } - - private function customFields(EstimatesRequest $request): ?iterable - { - $customFields = $request->input('customFields'); - - return is_iterable($customFields) ? $customFields : null; - } -} diff --git a/app/Domains/Sales/Http/Controllers/Company/InvoiceTemplatesController.php b/app/Domains/Sales/Http/Controllers/Company/InvoiceTemplatesController.php deleted file mode 100644 index fe64452e..00000000 --- a/app/Domains/Sales/Http/Controllers/Company/InvoiceTemplatesController.php +++ /dev/null @@ -1,32 +0,0 @@ -authorize('viewAny', Invoice::class); - - $invoiceTemplates = PdfTemplateUtils::getFormattedTemplates('invoice'); - - return response()->json([ - 'invoiceTemplates' => $invoiceTemplates, - ]); - } -} diff --git a/app/Domains/Sales/Http/Controllers/Company/RecurringInvoiceController.php b/app/Domains/Sales/Http/Controllers/Company/RecurringInvoiceController.php deleted file mode 100644 index 2bdf7f2c..00000000 --- a/app/Domains/Sales/Http/Controllers/Company/RecurringInvoiceController.php +++ /dev/null @@ -1,120 +0,0 @@ -authorize('viewAny', RecurringInvoice::class); - - $limit = $request->has('limit') ? $request->limit : 10; - - $recurringInvoices = RecurringInvoice::whereCompany() - ->applyFilters($request->all()) - ->paginateData($limit); - - return RecurringInvoiceResource::collection($recurringInvoices) - ->additional(['meta' => [ - 'recurring_invoice_total_count' => RecurringInvoice::whereCompany()->count(), - ]]); - } - - /** - * Store a newly created resource in storage. - * - * @param Request $request - * @return Response - */ - public function store(RecurringInvoiceRequest $request) - { - $this->authorize('create', RecurringInvoice::class); - - $recurringInvoice = $this->recurringInvoiceService->create( - attributes: $request->getRecurringInvoicePayload(), - items: $request->input('items'), - taxes: $request->has('taxes') ? $request->input('taxes') : null, - customFields: $this->customFields($request), - ); - - return new RecurringInvoiceResource($recurringInvoice); - } - - /** - * Display the specified resource. - * - * @return Response - */ - public function show(RecurringInvoice $recurringInvoice) - { - $this->authorize('view', $recurringInvoice); - - return new RecurringInvoiceResource($recurringInvoice); - } - - /** - * Update the specified resource in storage. - * - * @param Request $request - * @return Response - */ - public function update(RecurringInvoiceRequest $request, RecurringInvoice $recurringInvoice) - { - $this->authorize('update', $recurringInvoice); - - $this->recurringInvoiceService->update( - recurringInvoice: $recurringInvoice, - attributes: $request->getRecurringInvoicePayload(), - items: $request->input('items'), - taxes: $request->has('taxes') ? $request->input('taxes') : null, - customFields: $this->customFields($request), - ); - - return new RecurringInvoiceResource($recurringInvoice); - } - - /** - * Remove the specified resource from storage. - * - * @param RecurringInvoice $recurringInvoice - * @return Response - */ - public function delete(Request $request) - { - $this->authorize('delete multiple recurring invoices'); - - $ids = RecurringInvoice::whereCompany() - ->whereIn('id', $request->ids) - ->pluck('id'); - - $this->recurringInvoiceService->delete($ids); - - return response()->json([ - 'success' => true, - ]); - } - - private function customFields(RecurringInvoiceRequest $request): ?iterable - { - $customFields = $request->input('customFields'); - - return is_iterable($customFields) ? $customFields : null; - } -} diff --git a/app/Domains/Sales/Http/Controllers/Company/RecurringInvoiceFrequencyController.php b/app/Domains/Sales/Http/Controllers/Company/RecurringInvoiceFrequencyController.php deleted file mode 100644 index dbc58f48..00000000 --- a/app/Domains/Sales/Http/Controllers/Company/RecurringInvoiceFrequencyController.php +++ /dev/null @@ -1,20 +0,0 @@ -frequency, $request->starts_at); - - return response()->json([ - 'success' => true, - 'next_invoice_at' => $nextInvoiceAt, - ]); - } -} diff --git a/app/Domains/Sales/Http/Controllers/Company/SerialNumberController.php b/app/Domains/Sales/Http/Controllers/Company/SerialNumberController.php deleted file mode 100644 index 48e56264..00000000 --- a/app/Domains/Sales/Http/Controllers/Company/SerialNumberController.php +++ /dev/null @@ -1,89 +0,0 @@ -key; - $nextNumber = null; - $serial = (new SerialNumberService) - ->setCompany($request->header('company')) - ->setCustomer($request->userId); - - try { - switch ($key) { - case 'invoice': - // Scoped exactly like every invoice create path, so the - // settings preview can never count credit-note rows. - $nextNumber = $serial->setModel($invoice) - ->setSequenceScope(['type' => Invoice::TYPE_INVOICE]) - ->setModelObject($request->model_id) - ->getNextNumber($request->input('format')); - - break; - - case 'credit_note': - $nextNumber = $serial->setModel($invoice) - ->setSettingKey('credit_note_number_format') - ->setSequenceScope(['type' => Invoice::TYPE_CREDIT_NOTE]) - ->setModelObject($request->model_id) - ->getNextNumber($request->input('format')); - - break; - - case 'estimate': - $nextNumber = $serial->setModel($estimate) - ->setModelObject($request->model_id) - ->getNextNumber($request->input('format')); - - break; - - case 'payment': - $nextNumber = $serial->setModel($payment) - ->setModelObject($request->model_id) - ->getNextNumber($request->input('format')); - - break; - - default: - return response()->json([ - 'success' => false, - ]); - } - } catch (\Exception $exception) { - return response()->json([ - 'success' => false, - 'message' => $exception->getMessage(), - ]); - } - - return response()->json([ - 'success' => true, - 'nextNumber' => $nextNumber, - ]); - } - - public function placeholders(Request $request): JsonResponse - { - if ($request->input('format')) { - $placeholders = SerialNumberService::getPlaceholders($request->input('format')); - } else { - $placeholders = []; - } - - return response()->json([ - 'success' => true, - 'placeholders' => $placeholders, - ]); - } -} diff --git a/app/Domains/Sales/Http/Controllers/CustomerPortal/AcceptEstimateController.php b/app/Domains/Sales/Http/Controllers/CustomerPortal/AcceptEstimateController.php deleted file mode 100644 index 60c0d429..00000000 --- a/app/Domains/Sales/Http/Controllers/CustomerPortal/AcceptEstimateController.php +++ /dev/null @@ -1,36 +0,0 @@ -estimates() - ->whereCustomer(Auth::guard('customer')->id()) - ->where('id', $id) - ->first(); - - if (! $estimate) { - return response()->json(['error' => 'estimate_not_found'], 404); - } - - $estimate->update($request->only('status')); - - return new EstimateResource($estimate); - } -} diff --git a/app/Domains/Sales/Http/Controllers/CustomerPortal/EstimatePdfController.php b/app/Domains/Sales/Http/Controllers/CustomerPortal/EstimatePdfController.php deleted file mode 100644 index 3c96a508..00000000 --- a/app/Domains/Sales/Http/Controllers/CustomerPortal/EstimatePdfController.php +++ /dev/null @@ -1,53 +0,0 @@ -mailable; - abort_unless($estimate instanceof Estimate, 404); - abort_if($emailLog->isExpired(), 403, 'Link Expired.'); - - if ($estimate->status == Estimate::STATUS_SENT || $estimate->status == Estimate::STATUS_DRAFT) { - $estimate->status = Estimate::STATUS_VIEWED; - $estimate->save(); - $notifyEstimateViewed = CompanySetting::getSetting( - 'notify_estimate_viewed', - $estimate->company_id - ); - - if ($notifyEstimateViewed == 'YES') { - $data['estimate'] = Estimate::findOrFail($estimate->id)->toArray(); - $data['user'] = Customer::find($estimate->customer_id)->toArray(); - $notificationEmail = CompanySetting::getSetting( - 'notification_email', - $estimate->company_id - ); - - \Mail::to($notificationEmail)->send(new EstimateViewedMail($data)); - } - } - - return $estimate->getGeneratedPDFOrStream('estimate'); - } - - public function getEstimate(EmailLog $emailLog) - { - $estimate = $emailLog->mailable; - abort_unless($estimate instanceof Estimate, 404); - abort_if($emailLog->isExpired(), 403, 'Link Expired.'); - - return new EstimateResource($estimate); - } -} diff --git a/app/Domains/Sales/Http/Controllers/CustomerPortal/EstimatesController.php b/app/Domains/Sales/Http/Controllers/CustomerPortal/EstimatesController.php deleted file mode 100644 index 7f8e3e3d..00000000 --- a/app/Domains/Sales/Http/Controllers/CustomerPortal/EstimatesController.php +++ /dev/null @@ -1,68 +0,0 @@ -has('limit') ? $request->limit : 10; - - $estimates = Estimate::with([ - 'items', - 'customer', - 'taxes', - 'creator', - ]) - ->where('status', '<>', 'DRAFT') - ->whereCustomer(Auth::guard('customer')->id()) - ->applyFilters($request->only([ - 'status', - 'estimate_number', - 'from_date', - 'to_date', - 'orderByField', - 'orderBy', - ])) - ->latest() - ->paginateData($limit); - - return EstimateResource::collection($estimates) - ->additional(['meta' => [ - 'estimateTotalCount' => Estimate::where('status', '<>', 'DRAFT')->whereCustomer(Auth::guard('customer')->id())->count(), - ]]); - } - - /** - * Display the specified resource. - * - * @param Estimate $estimate - * @return Response - */ - public function show(Company $company, $id) - { - $estimate = $company->estimates() - ->whereCustomer(Auth::guard('customer')->id()) - ->where('id', $id) - ->first(); - - if (! $estimate) { - return response()->json(['error' => 'estimate_not_found'], 404); - } - - return new EstimateResource($estimate); - } -} diff --git a/app/Domains/Sales/Http/Controllers/CustomerPortal/InvoicePdfController.php b/app/Domains/Sales/Http/Controllers/CustomerPortal/InvoicePdfController.php deleted file mode 100644 index a0708002..00000000 --- a/app/Domains/Sales/Http/Controllers/CustomerPortal/InvoicePdfController.php +++ /dev/null @@ -1,64 +0,0 @@ -mailable; - abort_unless($invoice instanceof Invoice, 404); - abort_if($emailLog->isExpired(), 403, 'Link Expired.'); - - if ($invoice->status == Invoice::STATUS_SENT || $invoice->status == Invoice::STATUS_DRAFT) { - $invoice->status = Invoice::STATUS_VIEWED; - $invoice->viewed = true; - $invoice->save(); - $notifyInvoiceViewed = CompanySetting::getSetting( - 'notify_invoice_viewed', - $invoice->company_id - ); - - if ($notifyInvoiceViewed == 'YES') { - $data['invoice'] = Invoice::findOrFail($invoice->id)->toArray(); - $data['user'] = Customer::find($invoice->customer_id)->toArray(); - $notificationEmail = CompanySetting::getSetting( - 'notification_email', - $invoice->company_id - ); - - \Mail::to($notificationEmail)->send(new InvoiceViewedMail($data)); - } - } - - if ($request->has('pdf')) { - return $invoice->getGeneratedPDFOrStream('invoice'); - } - - return view('app')->with([ - 'customer_logo' => get_company_setting('customer_portal_logo', $invoice->company_id), - 'current_theme' => get_company_setting('customer_portal_theme', $invoice->company_id), - ]); - } - - public function getInvoice(EmailLog $emailLog) - { - $invoice = $emailLog->mailable; - abort_unless($invoice instanceof Invoice, 404); - abort_if($emailLog->isExpired(), 403, 'Link Expired.'); - - return new CustomerInvoiceResource($invoice); - } -} diff --git a/app/Domains/Sales/Http/Controllers/CustomerPortal/InvoicesController.php b/app/Domains/Sales/Http/Controllers/CustomerPortal/InvoicesController.php deleted file mode 100644 index 4e6f3d0e..00000000 --- a/app/Domains/Sales/Http/Controllers/CustomerPortal/InvoicesController.php +++ /dev/null @@ -1,52 +0,0 @@ -has('limit') ? $request->limit : 10; - - $invoices = Invoice::with(['items', 'customer', 'creator', 'taxes']) - ->where('status', '<>', 'DRAFT') - ->applyFilters($request->all()) - ->whereCustomer(Auth::guard('customer')->id()) - ->latest() - ->paginateData($limit); - - return InvoiceResource::collection($invoices) - ->additional(['meta' => [ - // Issued invoices only: a credit note is a reversal document, - // not another invoice the customer received. - 'invoiceTotalCount' => Invoice::where('type', Invoice::TYPE_INVOICE)->where('status', '<>', 'DRAFT')->whereCustomer(Auth::guard('customer')->id())->count(), - ]]); - } - - public function show(Company $company, $id) - { - $invoice = $company->invoices() - ->whereCustomer(Auth::guard('customer')->id()) - ->where('id', $id) - ->first(); - - if (! $invoice) { - return response()->json(['error' => 'invoice_not_found'], 404); - } - - return new InvoiceResource($invoice); - } -} diff --git a/app/Domains/Sales/Http/Requests/DeleteEstimatesRequest.php b/app/Domains/Sales/Http/Requests/DeleteEstimatesRequest.php deleted file mode 100644 index 197cb182..00000000 --- a/app/Domains/Sales/Http/Requests/DeleteEstimatesRequest.php +++ /dev/null @@ -1,33 +0,0 @@ - [ - 'required', - ], - 'ids.*' => [ - 'required', - Rule::exists('estimates', 'id'), - ], - ]; - } -} diff --git a/app/Domains/Sales/Http/Requests/DeleteInvoiceRequest.php b/app/Domains/Sales/Http/Requests/DeleteInvoiceRequest.php deleted file mode 100644 index f74e93dc..00000000 --- a/app/Domains/Sales/Http/Requests/DeleteInvoiceRequest.php +++ /dev/null @@ -1,38 +0,0 @@ - [ - 'required', - ], - 'ids.*' => [ - 'required', - Rule::exists('invoices', 'id'), - new RelationNotExist(Invoice::class, 'payments'), - new CreditNoteDeletedTogether((array) $this->input('ids', [])), - ], - ]; - } -} diff --git a/app/Domains/Sales/Http/Requests/EstimatesRequest.php b/app/Domains/Sales/Http/Requests/EstimatesRequest.php deleted file mode 100644 index f6e703d9..00000000 --- a/app/Domains/Sales/Http/Requests/EstimatesRequest.php +++ /dev/null @@ -1,165 +0,0 @@ - [ - 'required', - ], - 'expiry_date' => [ - 'nullable', - ], - 'customer_id' => [ - 'required', - ], - 'estimate_number' => [ - 'required', - Rule::unique('estimates')->where('company_id', $this->header('company')), - ], - 'exchange_rate' => [ - 'nullable', - ], - 'discount' => [ - 'numeric', - 'required', - ], - 'discount_val' => [ - 'integer', - 'required', - ], - 'sub_total' => [ - 'integer', - 'required', - ], - 'total' => [ - 'integer', - 'numeric', - 'max:999999999999', - 'required', - ], - 'tax' => [ - 'required', - ], - 'template_name' => [ - 'required', - new PdfTemplateExists('estimate'), - ], - 'items' => [ - 'required', - 'array', - ], - 'items.*.description' => [ - 'nullable', - ], - 'items.*' => [ - 'required', - 'max:255', - ], - 'items.*.name' => [ - 'required', - ], - 'items.*.quantity' => [ - 'numeric', - 'required', - ], - 'items.*.price' => [ - 'integer', - 'required', - ], - ]; - - $companyCurrency = CompanySetting::getSetting('currency', $this->header('company')); - - $customer = Customer::find($this->customer_id); - - if ($companyCurrency && $customer) { - if ((string) $customer->currency_id !== $companyCurrency) { - $rules['exchange_rate'] = [ - 'required', - ]; - } - } - - if ($this->isMethod('PUT')) { - $rules['estimate_number'] = [ - 'required', - Rule::unique('estimates') - ->ignore($this->route('estimate')->id) - ->where('company_id', $this->header('company')), - ]; - } - - return $rules; - } - - public function withValidator(Validator $validator): void - { - $this->validateDocumentTaxPlaceholders($validator); - } - - public function getEstimatePayload() - { - $company_currency = CompanySetting::getSetting('currency', $this->header('company')); - $current_currency = $this->currency_id; - $exchange_rate = $company_currency != $current_currency ? $this->exchange_rate : 1; - $currency = Customer::find($this->customer_id)->currency_id; - - $tax_per_item = CompanySetting::getSetting('tax_per_item', $this->header('company')) ?? 'NO '; - $discount_per_item = CompanySetting::getSetting('discount_per_item', $this->header('company')) ?? 'NO'; - - // Recompute totals server-side from the line items (GHSA-8c69). - $totals = DocumentTotals::compute( - $this->items ?? [], - $this->taxes ?? [], - $this->discount_val, - $tax_per_item, - (bool) $this->tax_included, - $discount_per_item - ); - - return collect($this->except('items', 'taxes')) - ->merge([ - 'creator_id' => $this->user()->id ?? null, - 'status' => $this->has('estimateSend') ? Estimate::STATUS_SENT : Estimate::STATUS_DRAFT, - 'company_id' => $this->header('company'), - 'tax_per_item' => $tax_per_item, - 'discount_per_item' => $discount_per_item, - 'sub_total' => $totals['sub_total'], - 'total' => $totals['total'], - 'tax' => $totals['tax'], - 'exchange_rate' => $exchange_rate, - 'base_discount_val' => $this->discount_val * $exchange_rate, - 'base_sub_total' => $totals['sub_total'] * $exchange_rate, - 'base_total' => $totals['total'] * $exchange_rate, - 'base_tax' => $totals['tax'] * $exchange_rate, - 'currency_id' => $currency, - ]) - ->toArray(); - } -} diff --git a/app/Domains/Sales/Http/Requests/InvoicesRequest.php b/app/Domains/Sales/Http/Requests/InvoicesRequest.php deleted file mode 100644 index 536cab0c..00000000 --- a/app/Domains/Sales/Http/Requests/InvoicesRequest.php +++ /dev/null @@ -1,178 +0,0 @@ - [ - 'required', - ], - 'due_date' => [ - 'nullable', - ], - 'customer_id' => [ - 'required', - ], - 'invoice_number' => [ - 'required', - Rule::unique('invoices')->where('company_id', $this->header('company')), - ], - 'exchange_rate' => [ - 'nullable', - ], - 'discount' => [ - 'numeric', - 'required', - ], - 'discount_val' => [ - 'integer', - 'required', - ], - 'sub_total' => [ - 'numeric', - 'required', - ], - 'total' => [ - 'numeric', - 'max:999999999999', - 'required', - ], - 'tax' => [ - 'required', - ], - 'template_name' => [ - 'required', - new PdfTemplateExists('invoice'), - ], - 'items' => [ - 'required', - 'array', - ], - 'items.*' => [ - 'required', - 'max:255', - ], - 'items.*.description' => [ - 'nullable', - ], - 'items.*.name' => [ - 'required', - ], - 'items.*.quantity' => [ - 'numeric', - 'required', - ], - 'items.*.price' => [ - 'numeric', - 'required', - ], - ]; - - $companyCurrency = CompanySetting::getSetting('currency', $this->header('company')); - - $customer = Customer::find($this->customer_id); - - if ($customer && $companyCurrency) { - if ((string) $customer->currency_id !== $companyCurrency) { - $rules['exchange_rate'] = [ - 'required', - ]; - } - } - - if ($this->isMethod('PUT')) { - $rules['invoice_number'] = [ - 'required', - Rule::unique('invoices') - ->ignore($this->route('invoice')->id) - ->where('company_id', $this->header('company')), - ]; - } - - return $rules; - } - - public function withValidator(Validator $validator): void - { - $this->validateDocumentTaxPlaceholders($validator); - } - - public function getInvoicePayload(): array - { - $company_currency = CompanySetting::getSetting('currency', $this->header('company')); - $current_currency = $this->currency_id; - $exchange_rate = $company_currency != $current_currency ? $this->exchange_rate : 1; - $currency = Customer::find($this->customer_id)->currency_id; - - $tax_per_item = CompanySetting::getSetting('tax_per_item', $this->header('company')) ?? 'NO '; - $discount_per_item = CompanySetting::getSetting('discount_per_item', $this->header('company')) ?? 'NO'; - - // Recompute the document totals server-side from the line items so a - // tampered total/sub_total/tax/due_amount in the request is ignored - // (GHSA-8c69). - $totals = DocumentTotals::compute( - $this->items ?? [], - $this->taxes ?? [], - $this->discount_val, - $tax_per_item, - (bool) $this->tax_included, - $discount_per_item - ); - - return collect($this->except('items', 'taxes')) - ->merge([ - 'creator_id' => $this->user()->id ?? null, - // Credit notes are minted only by CreditNoteService::create(); - // this payload feeds Invoice::create/update, so a client must never - // be able to declare a document a reversal, re-point its origin, - // or write the reason a reversal was issued for. - 'type' => Invoice::TYPE_INVOICE, - 'related_invoice_id' => null, - 'credit_reason' => null, - 'status' => $this->has('invoiceSend') ? Invoice::STATUS_SENT : Invoice::STATUS_DRAFT, - 'paid_status' => Invoice::STATUS_UNPAID, - 'company_id' => $this->header('company'), - 'tax_per_item' => $tax_per_item, - 'discount_per_item' => $discount_per_item, - 'sub_total' => $totals['sub_total'], - 'total' => $totals['total'], - 'tax' => $totals['tax'], - 'due_amount' => $totals['total'], - 'sent' => (bool) $this->sent ?? false, - 'viewed' => (bool) $this->viewed ?? false, - 'exchange_rate' => $exchange_rate, - 'base_total' => $totals['total'] * $exchange_rate, - 'base_discount_val' => $this->discount_val * $exchange_rate, - 'base_sub_total' => $totals['sub_total'] * $exchange_rate, - 'base_tax' => $totals['tax'] * $exchange_rate, - 'base_due_amount' => $totals['total'] * $exchange_rate, - 'currency_id' => $currency, - ]) - ->toArray(); - } -} diff --git a/app/Domains/Sales/Http/Requests/RecurringInvoiceRequest.php b/app/Domains/Sales/Http/Requests/RecurringInvoiceRequest.php deleted file mode 100644 index c4f513ff..00000000 --- a/app/Domains/Sales/Http/Requests/RecurringInvoiceRequest.php +++ /dev/null @@ -1,154 +0,0 @@ -header('company')); - - $rules = [ - 'starts_at' => [ - 'required', - ], - 'send_automatically' => [ - 'required', - 'boolean', - ], - 'customer_id' => [ - 'required', - ], - 'exchange_rate' => [ - 'nullable', - ], - 'discount' => [ - 'numeric', - 'required', - ], - 'discount_val' => [ - 'integer', - 'required', - ], - 'sub_total' => [ - 'integer', - 'required', - ], - 'total' => [ - 'integer', - 'max:999999999999', - 'required', - ], - 'tax' => [ - 'required', - ], - 'status' => [ - 'required', - ], - 'exchange_rate' => [ - 'nullable', - ], - 'frequency' => [ - 'required', - ], - 'limit_by' => [ - 'required', - ], - 'limit_count' => [ - 'required_if:limit_by,COUNT', - ], - 'limit_date' => [ - 'required_if:limit_by,DATE', - ], - 'items' => [ - 'required', - ], - 'items.*' => [ - 'required', - ], - 'items.*.description' => [ - 'nullable', - ], - ]; - - $customer = Customer::find($this->customer_id); - - if ($customer && $companyCurrency) { - if ((string) $customer->currency_id !== $companyCurrency) { - $rules['exchange_rate'] = [ - 'required', - ]; - } - } - - return $rules; - } - - public function withValidator(Validator $validator): void - { - $this->validateDocumentTaxPlaceholders($validator); - } - - public function getRecurringInvoicePayload() - { - $company_currency = CompanySetting::getSetting('currency', $this->header('company')); - $current_currency = $this->currency_id; - $exchange_rate = $company_currency != $current_currency ? $this->exchange_rate : 1; - $currency = Customer::find($this->customer_id)->currency_id; - - $nextInvoiceAt = RecurringInvoice::getNextInvoiceDate($this->frequency, $this->starts_at); - - $tax_per_item = CompanySetting::getSetting('tax_per_item', $this->header('company')) ?? 'NO '; - $discount_per_item = CompanySetting::getSetting('discount_per_item', $this->header('company')) ?? 'NO'; - - // Recompute totals server-side from the line items (GHSA-8c69). The - // recurring template totals propagate to every generated invoice. - $totals = DocumentTotals::compute( - $this->items ?? [], - $this->taxes ?? [], - $this->discount_val, - $tax_per_item, - (bool) $this->tax_included, - $discount_per_item - ); - - return collect($this->except('items', 'taxes')) - ->merge([ - 'creator_id' => $this->user()->id, - 'company_id' => $this->header('company'), - 'next_invoice_at' => $nextInvoiceAt, - 'tax_per_item' => $tax_per_item, - 'discount_per_item' => $discount_per_item, - 'sub_total' => $totals['sub_total'], - 'total' => $totals['total'], - 'tax' => $totals['tax'], - 'due_amount' => $totals['total'], - 'exchange_rate' => $exchange_rate, - 'base_sub_total' => $totals['sub_total'] * $exchange_rate, - 'base_total' => $totals['total'] * $exchange_rate, - 'base_tax' => $totals['tax'] * $exchange_rate, - 'currency_id' => $currency, - ]) - ->toArray(); - } -} diff --git a/app/Domains/Sales/Http/Requests/SendEstimatesRequest.php b/app/Domains/Sales/Http/Requests/SendEstimatesRequest.php deleted file mode 100644 index 93bc6ccc..00000000 --- a/app/Domains/Sales/Http/Requests/SendEstimatesRequest.php +++ /dev/null @@ -1,43 +0,0 @@ - [ - 'required', - ], - 'body' => [ - 'required', - ], - 'from' => [ - 'required', - ], - 'to' => [ - 'required', - ], - 'cc' => [ - 'nullable', - ], - 'bcc' => [ - 'nullable', - ], - ]; - } -} diff --git a/app/Domains/Sales/Http/Requests/SendInvoiceRequest.php b/app/Domains/Sales/Http/Requests/SendInvoiceRequest.php deleted file mode 100644 index 5e2a5f60..00000000 --- a/app/Domains/Sales/Http/Requests/SendInvoiceRequest.php +++ /dev/null @@ -1,43 +0,0 @@ - [ - 'required', - ], - 'subject' => [ - 'required', - ], - 'from' => [ - 'required', - ], - 'to' => [ - 'required', - ], - 'cc' => [ - 'nullable', - ], - 'bcc' => [ - 'nullable', - ], - ]; - } -} diff --git a/app/Domains/Sales/Http/Resources/CustomerPortal/EstimateCollection.php b/app/Domains/Sales/Http/Resources/CustomerPortal/EstimateCollection.php deleted file mode 100644 index e7be6c23..00000000 --- a/app/Domains/Sales/Http/Resources/CustomerPortal/EstimateCollection.php +++ /dev/null @@ -1,19 +0,0 @@ - $this->id, - 'name' => $this->name, - 'description' => $this->description, - 'discount_type' => $this->discount_type, - 'quantity' => $this->quantity, - 'unit_name' => $this->unit_name, - 'discount' => $this->discount, - 'discount_val' => $this->discount_val, - 'price' => $this->price, - 'tax' => $this->tax, - 'total' => $this->total, - 'item_id' => $this->item_id, - 'estimate_id' => $this->estimate_id, - 'company_id' => $this->company_id, - 'exchange_rate' => $this->exchange_rate, - 'base_discount_val' => $this->base_discount_val, - 'base_price' => $this->base_price, - 'base_tax' => $this->base_tax, - 'base_total' => $this->base_total, - 'taxes' => $this->when($this->taxes()->exists(), function () { - return TaxResource::collection($this->taxes); - }), - 'fields' => $this->when($this->fields()->exists(), function () { - return CustomFieldValueResource::collection($this->fields); - }), - ]; - } -} diff --git a/app/Domains/Sales/Http/Resources/CustomerPortal/EstimateResource.php b/app/Domains/Sales/Http/Resources/CustomerPortal/EstimateResource.php deleted file mode 100644 index 7d907bd3..00000000 --- a/app/Domains/Sales/Http/Resources/CustomerPortal/EstimateResource.php +++ /dev/null @@ -1,70 +0,0 @@ - $this->id, - 'estimate_date' => $this->estimate_date, - 'expiry_date' => $this->expiry_date, - 'estimate_number' => $this->estimate_number, - 'status' => $this->status, - 'reference_number' => $this->reference_number, - 'tax_per_item' => $this->tax_per_item, - 'discount_per_item' => $this->discount_per_item, - 'notes' => $this->notes, - 'discount' => $this->discount, - 'discount_type' => $this->discount_type, - 'discount_val' => $this->discount_val, - 'sub_total' => $this->sub_total, - 'total' => $this->total, - 'tax' => $this->tax, - 'unique_hash' => $this->unique_hash, - 'template_name' => $this->template_name, - 'customer_id' => $this->customer_id, - 'exchange_rate' => $this->exchange_rate, - 'base_discount_val' => $this->base_discount_val, - 'base_sub_total' => $this->base_sub_total, - 'base_total' => $this->base_total, - 'base_tax' => $this->base_tax, - 'currency_id' => $this->currency_id, - 'formatted_expiry_date' => $this->formattedExpiryDate, - 'formatted_estimate_date' => $this->formattedEstimateDate, - 'estimate_pdf_url' => $this->estimatePdfUrl, - 'items' => $this->when($this->items()->exists(), function () { - return EstimateItemResource::collection($this->items); - }), - 'customer' => $this->when($this->customer()->exists(), function () { - return new CustomerResource($this->customer); - }), - 'taxes' => $this->when($this->taxes()->exists(), function () { - return TaxResource::collection($this->taxes); - }), - 'fields' => $this->when($this->fields()->exists(), function () { - return CustomFieldValueResource::collection($this->fields); - }), - 'company' => $this->when($this->company()->exists(), function () { - return new CompanyResource($this->company); - }), - 'currency' => $this->when($this->currency()->exists(), function () { - return new CurrencyResource($this->currency); - }), - ]; - } -} diff --git a/app/Domains/Sales/Http/Resources/CustomerPortal/InvoiceCollection.php b/app/Domains/Sales/Http/Resources/CustomerPortal/InvoiceCollection.php deleted file mode 100644 index 9e730054..00000000 --- a/app/Domains/Sales/Http/Resources/CustomerPortal/InvoiceCollection.php +++ /dev/null @@ -1,19 +0,0 @@ - $this->id, - 'name' => $this->name, - 'description' => $this->description, - 'discount_type' => $this->discount_type, - 'price' => $this->price, - 'quantity' => $this->quantity, - 'unit_name' => $this->unit_name, - 'discount' => $this->discount, - 'discount_val' => $this->discount_val, - 'tax' => $this->tax, - 'total' => $this->total, - 'invoice_id' => $this->invoice_id, - 'item_id' => $this->item_id, - 'company_id' => $this->company_id, - 'base_price' => $this->base_price, - 'exchange_rate' => $this->exchange_rate, - 'base_discount_val' => $this->base_discount_val, - 'base_tax' => $this->base_tax, - 'base_total' => $this->base_total, - 'recurring_invoice_id' => $this->recurring_invoice_id, - 'taxes' => $this->when($this->taxes()->exists(), function () { - return TaxResource::collection($this->taxes); - }), - 'fields' => $this->when($this->fields()->exists(), function () { - return CustomFieldValueResource::collection($this->fields); - }), - ]; - } -} diff --git a/app/Domains/Sales/Http/Resources/CustomerPortal/InvoiceResource.php b/app/Domains/Sales/Http/Resources/CustomerPortal/InvoiceResource.php deleted file mode 100644 index 30dcb1d5..00000000 --- a/app/Domains/Sales/Http/Resources/CustomerPortal/InvoiceResource.php +++ /dev/null @@ -1,80 +0,0 @@ - $this->id, - 'invoice_date' => $this->invoice_date, - 'due_date' => $this->due_date, - 'invoice_number' => $this->invoice_number, - 'reference_number' => $this->reference_number, - 'status' => $this->status, - 'paid_status' => $this->paid_status, - 'tax_per_item' => $this->tax_per_item, - 'discount_per_item' => $this->discount_per_item, - 'notes' => $this->getNotes(), - 'discount_type' => $this->discount_type, - 'discount' => $this->discount, - 'discount_val' => $this->discount_val, - 'sub_total' => $this->sub_total, - 'total' => $this->total, - 'tax' => $this->tax, - 'due_amount' => $this->due_amount, - 'sent' => $this->sent, - 'viewed' => $this->viewed, - 'unique_hash' => $this->unique_hash, - 'template_name' => $this->template_name, - 'customer_id' => $this->customer_id, - 'recurring_invoice_id' => $this->recurring_invoice_id, - 'sequence_number' => $this->sequence_number, - 'base_discount_val' => $this->base_discount_val, - 'base_sub_total' => $this->base_sub_total, - 'base_total' => $this->base_total, - 'base_tax' => $this->base_tax, - 'base_due_amount' => $this->base_due_amount, - 'currency_id' => $this->currency_id, - 'formatted_created_at' => $this->formattedCreatedAt, - 'formatted_notes' => $this->formattedNotes, - 'invoice_pdf_url' => $this->invoicePdfUrl, - 'formatted_invoice_date' => $this->formattedInvoiceDate, - 'formatted_due_date' => $this->formattedDueDate, - 'payment_module_enabled' => $this->payment_module_enabled, - 'overdue' => $this->overdue, - 'items' => $this->when($this->items()->exists(), function () { - return InvoiceItemResource::collection($this->items); - }), - 'customer' => $this->when($this->customer()->exists(), function () { - return new CustomerResource($this->customer); - }), - 'taxes' => $this->when($this->taxes()->exists(), function () { - return TaxResource::collection($this->taxes); - }), - 'fields' => $this->when($this->fields()->exists(), function () { - return CustomFieldValueResource::collection($this->fields); - }), - 'company' => $this->when($this->company()->exists(), function () { - return new CompanyResource($this->company); - }), - 'currency' => $this->when($this->currency()->exists(), function () { - return new CurrencyResource($this->currency); - }), - ]; - } -} diff --git a/app/Domains/Sales/Http/Resources/EstimateCollection.php b/app/Domains/Sales/Http/Resources/EstimateCollection.php deleted file mode 100644 index 48f1e48a..00000000 --- a/app/Domains/Sales/Http/Resources/EstimateCollection.php +++ /dev/null @@ -1,19 +0,0 @@ - $this->id, - 'name' => $this->name, - 'description' => $this->description, - 'discount_type' => $this->discount_type, - 'quantity' => $this->quantity, - 'unit_name' => $this->unit_name, - 'discount' => $this->discount, - 'discount_val' => $this->discount_val, - 'price' => $this->price, - 'tax' => $this->tax, - 'total' => $this->total, - 'item_id' => $this->item_id, - 'estimate_id' => $this->estimate_id, - 'company_id' => $this->company_id, - 'exchange_rate' => $this->exchange_rate, - 'base_discount_val' => $this->base_discount_val, - 'base_price' => $this->base_price, - 'base_tax' => $this->base_tax, - 'base_total' => $this->base_total, - 'taxes' => $this->when($this->taxes()->exists(), function () { - return TaxResource::collection($this->taxes); - }), - 'fields' => $this->when($this->fields()->exists(), function () { - return CustomFieldValueResource::collection($this->fields); - }), - ]; - } -} diff --git a/app/Domains/Sales/Http/Resources/EstimateResource.php b/app/Domains/Sales/Http/Resources/EstimateResource.php deleted file mode 100644 index 269f5ca4..00000000 --- a/app/Domains/Sales/Http/Resources/EstimateResource.php +++ /dev/null @@ -1,79 +0,0 @@ - $this->id, - 'estimate_date' => $this->estimate_date, - 'expiry_date' => $this->expiry_date, - 'estimate_number' => $this->estimate_number, - 'status' => $this->status, - 'reference_number' => $this->reference_number, - 'tax_per_item' => $this->tax_per_item, - 'tax_included' => $this->tax_included, - 'discount_per_item' => $this->discount_per_item, - 'notes' => $this->getNotes(), - 'discount' => $this->discount, - 'discount_type' => $this->discount_type, - 'discount_val' => $this->discount_val, - 'sub_total' => $this->sub_total, - 'total' => $this->total, - 'tax' => $this->tax, - 'unique_hash' => $this->unique_hash, - 'creator_id' => $this->creator_id, - 'template_name' => $this->template_name, - 'customer_id' => $this->customer_id, - 'exchange_rate' => $this->exchange_rate, - 'base_discount_val' => $this->base_discount_val, - 'base_sub_total' => $this->base_sub_total, - 'base_total' => $this->base_total, - 'base_tax' => $this->base_tax, - 'sequence_number' => $this->sequence_number, - 'currency_id' => $this->currency_id, - 'formatted_expiry_date' => $this->formattedExpiryDate, - 'formatted_estimate_date' => $this->formattedEstimateDate, - 'estimate_pdf_url' => $this->estimatePdfUrl, - 'sales_tax_type' => $this->sales_tax_type, - 'sales_tax_address_type' => $this->sales_tax_address_type, - 'items' => $this->when($this->items()->exists(), function () { - return EstimateItemResource::collection($this->items); - }), - 'customer' => $this->when($this->customer()->exists(), function () { - return new CustomerResource($this->customer); - }), - 'creator' => $this->when($this->creator()->exists(), function () { - return new UserResource($this->creator); - }), - 'taxes' => $this->when($this->taxes()->exists(), function () { - return TaxResource::collection($this->taxes); - }), - 'fields' => $this->when($this->fields()->exists(), function () { - return CustomFieldValueResource::collection($this->fields); - }), - 'company' => $this->when($this->company()->exists(), function () { - return new CompanyResource($this->company); - }), - 'currency' => $this->when($this->currency()->exists(), function () { - return new CurrencyResource($this->currency); - }), - ]; - } -} diff --git a/app/Domains/Sales/Http/Resources/InvoiceCollection.php b/app/Domains/Sales/Http/Resources/InvoiceCollection.php deleted file mode 100644 index 2e596c16..00000000 --- a/app/Domains/Sales/Http/Resources/InvoiceCollection.php +++ /dev/null @@ -1,19 +0,0 @@ - $this->id, - 'name' => $this->name, - 'description' => $this->description, - 'discount_type' => $this->discount_type, - 'price' => $this->price, - 'quantity' => $this->quantity, - 'unit_name' => $this->unit_name, - 'discount' => $this->discount, - 'discount_val' => $this->discount_val, - 'tax' => $this->tax, - 'total' => $this->total, - 'invoice_id' => $this->invoice_id, - 'item_id' => $this->item_id, - 'company_id' => $this->company_id, - 'base_price' => $this->base_price, - 'exchange_rate' => $this->exchange_rate, - 'base_discount_val' => $this->base_discount_val, - 'base_tax' => $this->base_tax, - 'base_total' => $this->base_total, - 'recurring_invoice_id' => $this->recurring_invoice_id, - 'taxes' => $this->when($this->taxes()->exists(), function () { - return TaxResource::collection($this->taxes); - }), - 'fields' => $this->when($this->fields()->exists(), function () { - return CustomFieldValueResource::collection($this->fields); - }), - ]; - } -} diff --git a/app/Domains/Sales/Http/Resources/InvoiceResource.php b/app/Domains/Sales/Http/Resources/InvoiceResource.php deleted file mode 100644 index fdba9890..00000000 --- a/app/Domains/Sales/Http/Resources/InvoiceResource.php +++ /dev/null @@ -1,178 +0,0 @@ - $this->id, - 'invoice_date' => $this->invoice_date, - 'due_date' => $this->due_date, - 'invoice_number' => $this->invoice_number, - 'reference_number' => $this->reference_number, - 'type' => $this->type, - 'related_invoice_id' => $this->related_invoice_id, - 'status' => $this->status, - 'paid_status' => $this->paid_status, - 'tax_per_item' => $this->tax_per_item, - 'tax_included' => $this->tax_included, - 'discount_per_item' => $this->discount_per_item, - 'notes' => $this->notes, - 'discount_type' => $this->discount_type, - 'discount' => $this->discount, - 'discount_val' => $this->discount_val, - 'sub_total' => $this->sub_total, - 'total' => $this->total, - 'tax' => $this->tax, - 'due_amount' => $this->due_amount, - 'sent' => $this->sent, - 'viewed' => $this->viewed, - 'unique_hash' => $this->unique_hash, - 'template_name' => $this->template_name, - 'customer_id' => $this->customer_id, - 'recurring_invoice_id' => $this->recurring_invoice_id, - 'sequence_number' => $this->sequence_number, - 'exchange_rate' => $this->exchange_rate, - 'base_discount_val' => $this->base_discount_val, - 'base_sub_total' => $this->base_sub_total, - 'base_total' => $this->base_total, - 'creator_id' => $this->creator_id, - 'base_tax' => $this->base_tax, - 'base_due_amount' => $this->base_due_amount, - 'currency_id' => $this->currency_id, - 'formatted_created_at' => $this->formattedCreatedAt, - 'invoice_pdf_url' => $this->invoicePdfUrl, - 'formatted_invoice_date' => $this->formattedInvoiceDate, - 'formatted_due_date' => $this->formattedDueDate, - 'allow_edit' => $this->allow_edit, - 'payment_module_enabled' => $this->payment_module_enabled, - 'sales_tax_type' => $this->sales_tax_type, - 'sales_tax_address_type' => $this->sales_tax_address_type, - 'overdue' => $this->overdue, - // Credit notes reversing this invoice (minimal reference so the - // UI can flag the invoice as cancelled and link to the storno - // document, mirroring the related_invoice back-link). Emitted only - // where the relation was eager-loaded: probing it per row costs two - // queries each, and this resource is serialized in paginated lists. - 'credit_notes' => $this->when( - $this->relationLoaded('creditNotes') && $this->creditNotes->isNotEmpty(), - fn () => $this->creditNotes->map(fn ($creditNote) => [ - 'id' => $creditNote->id, - 'invoice_number' => $creditNote->invoice_number, - ])->values() - ), - // Why this invoice was credited, if it was. Set only by the - // credit-note flow, never by the invoice form. - 'credit_reason' => $this->credit_reason, - // How much of the invoice has been credited off it, as a positive - // number of cents (credit notes store negative totals), and whether - // that covers the whole document. Both are read off the same loaded - // relation the banner uses, so they cost no extra query. - 'credited_total' => $this->when( - $this->relationLoaded('creditNotes'), - fn () => $this->creditedTotal() - ), - 'credited_status' => $this->when( - $this->relationLoaded('creditNotes'), - function () { - $credited = $this->creditedTotal(); - - if ($credited === 0) { - return 'NONE'; - } - - return $credited === (int) $this->total ? 'FULL' : 'PARTIAL'; - } - ), - // Credited quantity per ORIGINAL line, which is what a partial - // credit form needs to offer the remaining quantities. Emitted only - // when the credit notes' items came along. - 'credited_quantities' => $this->when( - $this->relationLoaded('creditNotes') - && $this->creditNotes->every(fn ($creditNote) => $creditNote->relationLoaded('items')), - function () { - $quantities = []; - - foreach ($this->creditNotes as $creditNote) { - foreach ($creditNote->items as $item) { - if (! $item->source_invoice_item_id) { - continue; - } - - $quantities[$item->source_invoice_item_id] = - ($quantities[$item->source_invoice_item_id] ?? 0) + (float) $item->quantity; - } - } - - // Cast to an object because the item ids are the keys: a - // nested array whose keys are all numeric is re-indexed to a - // list by the resource filter, which would throw the ids away. - return (object) $quantities; - } - ), - // Allocation rows explain how this invoice was settled without - // reintroducing the removed singular payment.invoice relation. - // They are loaded for the detail response only, so index listings - // remain free of per-row payment queries. - 'payment_allocations' => $this->when( - $this->relationLoaded('allocations'), - fn () => $this->allocations->map(fn ($allocation) => [ - 'id' => $allocation->id, - 'payment_id' => $allocation->payment_id, - 'amount' => $allocation->amount, - 'base_amount' => $allocation->base_amount, - 'payment' => $allocation->relationLoaded('payment') && $allocation->payment ? [ - 'id' => $allocation->payment->id, - 'payment_number' => $allocation->payment->payment_number, - 'formatted_payment_date' => $allocation->payment->formattedPaymentDate, - ] : null, - ])->values() - ), - 'items' => $this->when($this->items()->exists(), function () { - return InvoiceItemResource::collection($this->items); - }), - 'customer' => $this->when($this->customer()->exists(), function () { - return new CustomerResource($this->customer); - }), - 'creator' => $this->when($this->creator()->exists(), function () { - return new UserResource($this->creator); - }), - 'taxes' => $this->when($this->taxes()->exists(), function () { - return TaxResource::collection($this->taxes); - }), - 'fields' => $this->when($this->fields()->exists(), function () { - return CustomFieldValueResource::collection($this->fields); - }), - 'company' => $this->when($this->company()->exists(), function () { - return new CompanyResource($this->company); - }), - 'currency' => $this->when($this->currency()->exists(), function () { - return new CurrencyResource($this->currency); - }), - ]; - } - - /** - * Sum of the loaded credit notes as a positive number of cents. - */ - protected function creditedTotal(): int - { - return -(int) $this->creditNotes->sum('total'); - } -} diff --git a/app/Domains/Sales/Http/Resources/RecurringInvoiceCollection.php b/app/Domains/Sales/Http/Resources/RecurringInvoiceCollection.php deleted file mode 100644 index 8ef50c61..00000000 --- a/app/Domains/Sales/Http/Resources/RecurringInvoiceCollection.php +++ /dev/null @@ -1,19 +0,0 @@ - $this->id, - 'starts_at' => $this->starts_at, - 'formatted_starts_at' => $this->formattedStartsAt, - 'formatted_created_at' => $this->formattedCreatedAt, - 'formatted_next_invoice_at' => $this->formattedNextInvoiceAt, - 'formatted_limit_date' => $this->formattedLimitDate, - 'send_automatically' => $this->send_automatically, - 'customer_id' => $this->customer_id, - 'company_id' => $this->company_id, - 'creator_id' => $this->creator_id, - 'status' => $this->status, - 'next_invoice_at' => $this->next_invoice_at, - 'frequency' => $this->frequency, - 'limit_by' => $this->limit_by, - 'limit_count' => $this->limit_count, - 'limit_date' => $this->limit_date, - 'exchange_rate' => $this->exchange_rate, - 'tax_per_item' => $this->tax_per_item, - 'tax_included' => $this->tax_included, - 'discount_per_item' => $this->discount_per_item, - 'notes' => $this->notes, - 'discount_type' => $this->discount_type, - 'discount' => $this->discount, - 'discount_val' => $this->discount_val, - 'sub_total' => $this->sub_total, - 'total' => $this->total, - 'tax' => $this->tax, - 'due_amount' => $this->due_amount, - 'template_name' => $this->template_name, - 'sales_tax_type' => $this->sales_tax_type, - 'sales_tax_address_type' => $this->sales_tax_address_type, - 'fields' => $this->when($this->fields()->exists(), function () { - return CustomFieldValueResource::collection($this->fields); - }), - 'items' => $this->when($this->items()->exists(), function () { - return InvoiceItemResource::collection($this->items); - }), - 'customer' => $this->when($this->customer()->exists(), function () { - return new CustomerResource($this->customer); - }), - 'company' => $this->when($this->company()->exists(), function () { - return new CompanyResource($this->company); - }), - 'invoices' => $this->when($this->invoices()->exists(), function () { - return InvoiceResource::collection($this->invoices); - }), - 'taxes' => $this->when($this->taxes()->exists(), function () { - return TaxResource::collection($this->taxes); - }), - 'creator' => $this->when($this->creator()->exists(), function () { - return new UserResource($this->creator); - }), - 'currency' => $this->when($this->currency()->exists(), function () { - return new CurrencyResource($this->currency); - }), - ]; - } -} diff --git a/app/Domains/Sales/Jobs/GenerateEstimatePdfJob.php b/app/Domains/Sales/Jobs/GenerateEstimatePdfJob.php deleted file mode 100644 index 2e468eef..00000000 --- a/app/Domains/Sales/Jobs/GenerateEstimatePdfJob.php +++ /dev/null @@ -1,42 +0,0 @@ -estimate = $estimate; - $this->deleteExistingFile = $deleteExistingFile; - } - - /** - * Execute the job. - */ - public function handle(): int - { - $this->estimate->generatePDF('estimate', $this->estimate->estimate_number, $this->deleteExistingFile); - - return 0; - } -} diff --git a/app/Domains/Sales/Jobs/GenerateInvoicePdfJob.php b/app/Domains/Sales/Jobs/GenerateInvoicePdfJob.php deleted file mode 100644 index fd1560fc..00000000 --- a/app/Domains/Sales/Jobs/GenerateInvoicePdfJob.php +++ /dev/null @@ -1,42 +0,0 @@ -invoice = $invoice; - $this->deleteExistingFile = $deleteExistingFile; - } - - /** - * Execute the job. - */ - public function handle(): int - { - $this->invoice->generatePDF('invoice', $this->invoice->invoice_number, $this->deleteExistingFile); - - return 0; - } -} diff --git a/app/Domains/Sales/Mail/EstimateViewedMail.php b/app/Domains/Sales/Mail/EstimateViewedMail.php deleted file mode 100644 index b99acfc7..00000000 --- a/app/Domains/Sales/Mail/EstimateViewedMail.php +++ /dev/null @@ -1,37 +0,0 @@ -data = $data; - } - - /** - * Build the message. - * - * @return $this - */ - public function build() - { - return $this->from(config('mail.from.address'), config('mail.from.name')) - ->subject(__('notification_view_estimate')) - ->markdown('emails.viewed.estimate', ['data', $this->data]); - } -} diff --git a/app/Domains/Sales/Mail/InvoiceViewedMail.php b/app/Domains/Sales/Mail/InvoiceViewedMail.php deleted file mode 100644 index 0dfccb4c..00000000 --- a/app/Domains/Sales/Mail/InvoiceViewedMail.php +++ /dev/null @@ -1,37 +0,0 @@ -data = $data; - } - - /** - * Build the message. - * - * @return $this - */ - public function build() - { - return $this->from(config('mail.from.address'), config('mail.from.name')) - ->subject(__('notification_view_invoice')) - ->markdown('emails.viewed.invoice', ['data', $this->data]); - } -} diff --git a/app/Domains/Sales/Mail/SendEstimateMail.php b/app/Domains/Sales/Mail/SendEstimateMail.php deleted file mode 100644 index a4e25f4b..00000000 --- a/app/Domains/Sales/Mail/SendEstimateMail.php +++ /dev/null @@ -1,67 +0,0 @@ -data = $data; - } - - /** - * Build the message. - * - * @return $this - */ - public function build() - { - $log = EmailLog::create([ - 'from' => $this->data['from'], - 'to' => $this->data['to'], - 'cc' => $this->data['cc'] ?? null, - 'bcc' => $this->data['bcc'] ?? null, - 'subject' => $this->data['subject'], - 'body' => $this->data['body'], - 'mailable_type' => ModelIdentityMap::aliasFor(Estimate::class), - 'mailable_id' => $this->data['estimate']['id'], - ]); - - $log->token = Hashids::connection(HashidConnection::EmailLog->value)->encode($log->id); - $log->save(); - - $this->data['url'] = route('estimate', ['email_log' => $log->token]); - - $mailContent = $this->from($this->data['from'], config('mail.from.name')) - ->subject($this->data['subject']) - ->markdown('emails.send.estimate', ['data', $this->data]); - - if ($this->data['attach']['data']) { - $mailContent->attachData( - $this->data['attach']['data']->output(), - $this->data['estimate']['estimate_number'].'.pdf' - ); - } - - return $mailContent; - } -} diff --git a/app/Domains/Sales/Mail/SendInvoiceMail.php b/app/Domains/Sales/Mail/SendInvoiceMail.php deleted file mode 100644 index e9dccbab..00000000 --- a/app/Domains/Sales/Mail/SendInvoiceMail.php +++ /dev/null @@ -1,67 +0,0 @@ -data = $data; - } - - /** - * Build the message. - * - * @return $this - */ - public function build() - { - $log = EmailLog::create([ - 'from' => $this->data['from'], - 'to' => $this->data['to'], - 'cc' => $this->data['cc'] ?? null, - 'bcc' => $this->data['bcc'] ?? null, - 'subject' => $this->data['subject'], - 'body' => $this->data['body'], - 'mailable_type' => ModelIdentityMap::aliasFor(Invoice::class), - 'mailable_id' => $this->data['invoice']['id'], - ]); - - $log->token = Hashids::connection(HashidConnection::EmailLog->value)->encode($log->id); - $log->save(); - - $this->data['url'] = route('invoice', ['email_log' => $log->token]); - - $mailContent = $this->from($this->data['from'], config('mail.from.name')) - ->subject($this->data['subject']) - ->markdown('emails.send.invoice', ['data', $this->data]); - - if ($this->data['attach']['data']) { - $mailContent->attachData( - $this->data['attach']['data']->output(), - $this->data['invoice']['invoice_number'].'.pdf' - ); - } - - return $mailContent; - } -} diff --git a/app/Domains/Sales/Models/Estimate.php b/app/Domains/Sales/Models/Estimate.php deleted file mode 100644 index fdb39d4c..00000000 --- a/app/Domains/Sales/Models/Estimate.php +++ /dev/null @@ -1,343 +0,0 @@ - 'integer', - 'tax' => 'integer', - 'sub_total' => 'integer', - 'discount' => 'float', - 'discount_val' => 'integer', - 'exchange_rate' => 'float', - ]; - } - - public function getEstimatePdfUrlAttribute() - { - return url('/estimates/pdf/'.$this->unique_hash); - } - - public function emailLogs(): MorphMany - { - return $this->morphMany(EmailLog::class, 'mailable'); - } - - public function items(): HasMany - { - return $this->hasMany(EstimateItem::class); - } - - public function customer(): BelongsTo - { - return $this->belongsTo(Customer::class, 'customer_id'); - } - - public function creator(): BelongsTo - { - return $this->belongsTo(User::class, 'creator_id'); - } - - public function company(): BelongsTo - { - return $this->belongsTo(Company::class); - } - - public function currency(): BelongsTo - { - return $this->belongsTo(Currency::class); - } - - public function taxes(): HasMany - { - return $this->hasMany(Tax::class); - } - - public function getFormattedExpiryDateAttribute($value) - { - $dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id); - - return Carbon::parse($this->expiry_date)->translatedFormat($dateFormat); - } - - public function getFormattedEstimateDateAttribute($value) - { - $dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id); - - return Carbon::parse($this->estimate_date)->translatedFormat($dateFormat); - } - - public function scopeEstimatesBetween($query, $start, $end) - { - return $query->whereBetween( - 'estimates.estimate_date', - [$start->format('Y-m-d'), $end->format('Y-m-d')] - ); - } - - public function scopeWhereStatus($query, $status) - { - return $query->where('estimates.status', $status); - } - - public function scopeWhereEstimateNumber($query, $estimateNumber) - { - return $query->where('estimates.estimate_number', 'LIKE', '%'.$estimateNumber.'%'); - } - - public function scopeWhereEstimate($query, $estimate_id) - { - $query->orWhere('id', $estimate_id); - } - - public function scopeWhereSearch($query, $search) - { - foreach (explode(' ', $search) as $term) { - $query->whereHas('customer', function ($query) use ($term) { - $query->where('name', 'LIKE', '%'.$term.'%') - ->orWhere('contact_name', 'LIKE', '%'.$term.'%') - ->orWhere('company_name', 'LIKE', '%'.$term.'%'); - }); - } - } - - public function scopeApplyFilters($query, array $filters) - { - $filters = collect($filters); - - if ($filters->get('search')) { - $query->whereSearch($filters->get('search')); - } - - if ($filters->get('estimate_number')) { - $query->whereEstimateNumber($filters->get('estimate_number')); - } - - if ($filters->get('status')) { - $query->whereStatus($filters->get('status')); - } - - if ($filters->get('estimate_id')) { - $query->whereEstimate($filters->get('estimate_id')); - } - - if ($filters->get('from_date') && $filters->get('to_date')) { - $start = Carbon::createFromFormat('Y-m-d', $filters->get('from_date')); - $end = Carbon::createFromFormat('Y-m-d', $filters->get('to_date')); - $query->estimatesBetween($start, $end); - } - - if ($filters->get('customer_id')) { - $query->whereCustomer($filters->get('customer_id')); - } - - if ($filters->get('orderByField') || $filters->get('orderBy')) { - $field = $filters->get('orderByField') ? $filters->get('orderByField') : 'sequence_number'; - $orderBy = $filters->get('orderBy') ? $filters->get('orderBy') : 'desc'; - $query->whereOrder($field, $orderBy); - } - } - - public function scopeWhereOrder($query, $orderByField, $orderBy) - { - SafeOrderBy::apply($query, $orderByField, $orderBy); - } - - public function scopeWhereCompany($query) - { - $query->where('estimates.company_id', request()->header('company')); - } - - public function scopeWhereCustomer($query, $customer_id) - { - $query->where('estimates.customer_id', $customer_id); - } - - public function scopePaginateData($query, $limit) - { - if ($limit == 'all') { - return $query->get(); - } - - return $query->paginate($limit); - } - - public function getPDFData(): mixed - { - return app(EstimatePdfDataProvider::class)->getPdfData($this); - } - - public function getCompanyAddress(): string|false - { - if ($this->company && (! $this->company->address()->exists())) { - return false; - } - - $format = CompanySetting::getSetting('estimate_company_address_format', $this->company_id); - - return $this->getFormattedString($format); - } - - public function getCustomerShippingAddress(): string|false - { - if ($this->customer && (! $this->customer->shippingAddress()->exists())) { - return false; - } - - $format = CompanySetting::getSetting('estimate_shipping_address_format', $this->company_id); - - return $this->getFormattedString($format); - } - - public function getCustomerBillingAddress(): string|false - { - if ($this->customer && (! $this->customer->billingAddress()->exists())) { - return false; - } - - $format = CompanySetting::getSetting('estimate_billing_address_format', $this->company_id); - - return $this->getFormattedString($format); - } - - public function getNotes(): string - { - return PdfHtmlSanitizer::sanitize($this->getFormattedString($this->notes)); - } - - public function getEmailAttachmentSetting(): bool - { - $estimateAsAttachment = CompanySetting::getSetting('estimate_email_attachment', $this->company_id); - - if ($estimateAsAttachment == 'NO') { - return false; - } - - return true; - } - - public function getEmailBody(string $body): string - { - $values = array_merge($this->getFieldsArray(), $this->getExtraFields()); - - $body = strtr($body, $values); - - return preg_replace('/{(.*?)}/', '', $body); - } - - public function getExtraFields(): array - { - return [ - '{ESTIMATE_DATE}' => $this->formattedEstimateDate, - '{ESTIMATE_EXPIRY_DATE}' => $this->formattedExpiryDate, - '{ESTIMATE_NUMBER}' => $this->estimate_number, - '{ESTIMATE_REF_NUMBER}' => $this->reference_number, - ]; - } - - /** - * Map the estimate's template name to the corresponding invoice template name. - * - * Falls back to 'invoice1' if the mapped name does not exist in available templates. - */ - public function getInvoiceTemplateName(): string - { - $templateName = Str::replace('estimate', 'invoice', $this->template_name); - - // Empty image format: only the names are wanted here, and the default - // builds a base64 preview for every template to answer that. - $name = array_column(PdfTemplateUtils::getFormattedTemplates('invoice', ''), 'name'); - - if (in_array($templateName, $name) == false) { - $templateName = 'invoice1'; - } - - return $templateName; - } - - /** - * Handle the post-conversion action for this estimate based on company settings. - * - * Either deletes the estimate or marks it as accepted, depending on the - * 'estimate_convert_action' company setting. - */ - public function checkForEstimateConvertAction(): bool - { - $convertEstimateAction = CompanySetting::getSetting( - 'estimate_convert_action', - $this->company_id - ); - - if ($convertEstimateAction === 'delete_estimate') { - $this->delete(); - } - - if ($convertEstimateAction === 'mark_estimate_as_accepted') { - $this->status = self::STATUS_ACCEPTED; - $this->save(); - } - - return true; - } -} diff --git a/app/Domains/Sales/Models/EstimateItem.php b/app/Domains/Sales/Models/EstimateItem.php deleted file mode 100644 index d98d5ba4..00000000 --- a/app/Domains/Sales/Models/EstimateItem.php +++ /dev/null @@ -1,56 +0,0 @@ - 'integer', - 'total' => 'integer', - 'discount' => 'float', - 'quantity' => 'float', - 'discount_val' => 'integer', - 'tax' => 'integer', - ]; - } - - public function estimate(): BelongsTo - { - return $this->belongsTo(Estimate::class); - } - - public function item(): BelongsTo - { - return $this->belongsTo(Item::class); - } - - public function taxes(): HasMany - { - return $this->hasMany(Tax::class); - } - - public function scopeWhereCompany(Builder $query, int $company_id): void - { - $query->where('company_id', $company_id); - } -} diff --git a/app/Domains/Sales/Models/Invoice.php b/app/Domains/Sales/Models/Invoice.php deleted file mode 100644 index 5fc01ac8..00000000 --- a/app/Domains/Sales/Models/Invoice.php +++ /dev/null @@ -1,538 +0,0 @@ - 'integer', - 'tax' => 'integer', - 'sub_total' => 'integer', - 'discount' => 'float', - 'discount_val' => 'integer', - 'exchange_rate' => 'float', - ]; - } - - public function transactions(): HasMany - { - return $this->hasMany(Transaction::class); - } - - public function emailLogs(): MorphMany - { - return $this->morphMany(EmailLog::class, 'mailable'); - } - - public function items(): HasMany - { - return $this->hasMany(InvoiceItem::class); - } - - public function taxes(): HasMany - { - return $this->hasMany(Tax::class); - } - - public function allocations(): HasMany - { - return $this->hasMany(PaymentAllocation::class); - } - - public function payments(): BelongsToMany - { - return $this->belongsToMany(Payment::class, 'payment_allocations') - ->withPivot(['amount', 'base_amount']) - ->withTimestamps(); - } - - public function currency(): BelongsTo - { - return $this->belongsTo(Currency::class); - } - - public function company(): BelongsTo - { - return $this->belongsTo(Company::class); - } - - public function customer(): BelongsTo - { - return $this->belongsTo(Customer::class, 'customer_id'); - } - - public function recurringInvoice(): BelongsTo - { - return $this->belongsTo(RecurringInvoice::class); - } - - public function creator(): BelongsTo - { - return $this->belongsTo(User::class, 'creator_id'); - } - - /** - * The original invoice this credit note reverses (null for normal invoices). - */ - public function relatedInvoice(): BelongsTo - { - return $this->belongsTo(Invoice::class, 'related_invoice_id'); - } - - /** - * Credit notes that reverse this invoice. - */ - public function creditNotes(): HasMany - { - return $this->hasMany(Invoice::class, 'related_invoice_id') - ->where('type', self::TYPE_CREDIT_NOTE); - } - - public function isCreditNote(): bool - { - return $this->type === self::TYPE_CREDIT_NOTE; - } - - public function getInvoicePdfUrlAttribute() - { - return url('/invoices/pdf/'.$this->unique_hash); - } - - public function getPaymentModuleEnabledAttribute() - { - if (Module::has('Payments')) { - return Module::isEnabled('Payments'); - } - - return false; - } - - public function getAllowEditAttribute() - { - // A credited invoice is immutable: its line item ids anchor the lines of - // every credit note that reverses it. - $hasCreditNotes = $this->relationLoaded('creditNotes') - ? $this->creditNotes->isNotEmpty() - : $this->creditNotes()->exists(); - - if ($hasCreditNotes) { - return false; - } - - $retrospective_edit = CompanySetting::getSetting('retrospective_edits', $this->company_id); - - $allowed = true; - - $status = [ - self::STATUS_DRAFT, - self::STATUS_SENT, - self::STATUS_VIEWED, - self::STATUS_COMPLETED, - ]; - - if ($retrospective_edit == 'disable_on_invoice_sent' && (in_array($this->status, $status)) && ($this->paid_status === Invoice::STATUS_PARTIALLY_PAID || $this->paid_status === Invoice::STATUS_PAID)) { - $allowed = false; - } elseif ($retrospective_edit == 'disable_on_invoice_partial_paid' && ($this->paid_status === Invoice::STATUS_PARTIALLY_PAID || $this->paid_status === Invoice::STATUS_PAID)) { - $allowed = false; - } elseif ($retrospective_edit == 'disable_on_invoice_paid' && $this->paid_status === Invoice::STATUS_PAID) { - $allowed = false; - } - - return $allowed; - } - - public function getPreviousStatus(): string - { - if ($this->viewed) { - return self::STATUS_VIEWED; - } elseif ($this->sent) { - return self::STATUS_SENT; - } else { - return self::STATUS_DRAFT; - } - } - - public function getFormattedNotesAttribute($value) - { - return $this->getNotes(); - } - - public function getFormattedCreatedAtAttribute($value) - { - $dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id); - - return Carbon::parse($this->created_at)->format($dateFormat); - } - - public function getFormattedDueDateAttribute($value) - { - $dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id); - - return Carbon::parse($this->due_date)->translatedFormat($dateFormat); - } - - public function getFormattedDueAmountAttribute($value) - { - $currency = $this->currency; - - if (! $currency) { - $currency = Currency::findOrFail(CompanySetting::getSetting('currency', $this->company_id)); - } - - return format_money_pdf($this->due_amount, $currency); - } - - public function getFormattedInvoiceDateAttribute($value) - { - $dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id); - $timeFormat = CompanySetting::getSetting('carbon_time_format', $this->company_id); - $invoiceTimeEnabled = CompanySetting::getSetting('invoice_use_time', $this->company_id); - - if ($invoiceTimeEnabled === 'YES') { - $dateFormat .= ' '.$timeFormat; - } - - return Carbon::parse($this->invoice_date)->translatedFormat($dateFormat); - } - - public function scopeWhereStatus($query, $status) - { - return $query->where('invoices.status', $status); - } - - public function scopeWherePaidStatus($query, $status) - { - return $query->where('invoices.paid_status', $status); - } - - public function scopeWhereDueStatus($query, $status) - { - return $query->whereIn('invoices.paid_status', [ - self::STATUS_UNPAID, - self::STATUS_PARTIALLY_PAID, - ]); - } - - public function scopeWhereInvoiceNumber($query, $invoiceNumber) - { - return $query->where('invoices.invoice_number', 'LIKE', '%'.$invoiceNumber.'%'); - } - - public function scopeInvoicesBetween($query, $start, $end) - { - return $query->whereBetween( - 'invoices.invoice_date', - [$start->format('Y-m-d'), $end->format('Y-m-d')] - ); - } - - public function scopeWhereSearch($query, $search) - { - foreach (explode(' ', $search) as $term) { - $query->whereHas('customer', function ($query) use ($term) { - $query->where('name', 'LIKE', '%'.$term.'%') - ->orWhere('contact_name', 'LIKE', '%'.$term.'%') - ->orWhere('company_name', 'LIKE', '%'.$term.'%'); - }); - } - } - - public function scopeWhereOrder($query, $orderByField, $orderBy) - { - SafeOrderBy::apply($query, $orderByField, $orderBy); - } - - public function scopeApplyFilters($query, array $filters) - { - $filters = collect($filters)->filter()->all(); - - return $query->when($filters['search'] ?? null, function ($query, $search) { - $query->whereSearch($search); - })->when($filters['status'] ?? null, function ($query, $status) { - match ($status) { - self::STATUS_UNPAID, self::STATUS_PARTIALLY_PAID, self::STATUS_PAID => $query->wherePaidStatus($status), - 'DUE' => $query->whereDueStatus($status), - default => $query->whereStatus($status), - }; - })->when($filters['paid_status'] ?? null, function ($query, $paidStatus) { - $query->wherePaidStatus($paidStatus); - })->when($filters['invoice_id'] ?? null, function ($query, $invoiceId) { - $query->whereInvoice($invoiceId); - })->when($filters['invoice_number'] ?? null, function ($query, $invoiceNumber) { - $query->whereInvoiceNumber($invoiceNumber); - })->when(($filters['from_date'] ?? null) && ($filters['to_date'] ?? null), function ($query) use ($filters) { - $start = Carbon::parse($filters['from_date']); - $end = Carbon::parse($filters['to_date']); - $query->invoicesBetween($start, $end); - })->when($filters['customer_id'] ?? null, function ($query, $customerId) { - $query->where('customer_id', $customerId); - })->when($filters['orderByField'] ?? null, function ($query, $orderByField) use ($filters) { - $orderBy = $filters['orderBy'] ?? 'desc'; - - SafeOrderBy::apply($query, $orderByField, $orderBy); - }, function ($query) { - $query->orderBy('sequence_number', 'desc'); - }); - } - - public function scopeWhereInvoice($query, $invoice_id) - { - $query->orWhere('id', $invoice_id); - } - - public function getEstimateTemplateName(): string - { - $templateName = Str::replace('invoice', 'estimate', $this->template_name); - - // Empty image format: only the names are wanted here, and the default - // builds a base64 preview for every template to answer that. - $names = array_column(PdfTemplateUtils::getFormattedTemplates('estimate', ''), 'name'); - - if (! in_array($templateName, $names)) { - $templateName = 'estimate1'; - } - - return $templateName; - } - - public function scopeWhereCompany($query) - { - $query->where('invoices.company_id', request()->header('company')); - } - - public function scopeWhereCompanyId($query, $company) - { - $query->where('invoices.company_id', $company); - } - - public function scopeWhereCustomer($query, $customer_id) - { - $query->where('invoices.customer_id', $customer_id); - } - - public function scopePaginateData($query, $limit) - { - if ($limit == 'all') { - return $query->get(); - } - - return $query->paginate($limit); - } - - public function getPDFData(): mixed - { - return app(InvoicePdfDataProvider::class)->getPdfData($this); - } - - public function getEmailAttachmentSetting(): bool - { - $invoiceAsAttachment = CompanySetting::getSetting('invoice_email_attachment', $this->company_id); - - if ($invoiceAsAttachment == 'NO') { - return false; - } - - return true; - } - - public function getCompanyAddress(): string|false - { - if ($this->company && (! $this->company->address()->exists())) { - return false; - } - - $format = CompanySetting::getSetting('invoice_company_address_format', $this->company_id); - - return $this->getFormattedString($format); - } - - public function getCustomerShippingAddress(): string|false - { - if ($this->customer && (! $this->customer->shippingAddress()->exists())) { - return false; - } - - $format = CompanySetting::getSetting('invoice_shipping_address_format', $this->company_id); - - return $this->getFormattedString($format); - } - - public function getCustomerBillingAddress(): string|false - { - if ($this->customer && (! $this->customer->billingAddress()->exists())) { - return false; - } - - $format = CompanySetting::getSetting('invoice_billing_address_format', $this->company_id); - - return $this->getFormattedString($format); - } - - public function getNotes(): string - { - return PdfHtmlSanitizer::sanitize($this->getFormattedString($this->notes)); - } - - public function getEmailString(string $body): string - { - $values = array_merge($this->getFieldsArray(), $this->getExtraFields()); - - $body = strtr($body, $values); - - return preg_replace('/{(.*?)}/', '', $body); - } - - public function getExtraFields(): array - { - return [ - '{INVOICE_DATE}' => $this->formattedInvoiceDate, - '{INVOICE_DUE_DATE}' => $this->formattedDueDate, - '{INVOICE_NUMBER}' => $this->invoice_number, - '{INVOICE_REF_NUMBER}' => $this->reference_number, - ]; - } - - /** - * Add an amount to the invoice's due balance and recalculate the paid status. - */ - public function addInvoicePayment(int $amount): void - { - $this->due_amount += $amount; - $this->base_due_amount = $this->due_amount * $this->exchange_rate; - - $this->changeInvoiceStatus($this->due_amount); - } - - /** - * Subtract an amount from the invoice's due balance and recalculate the paid status. - */ - public function subtractInvoicePayment(int $amount): void - { - $this->due_amount -= $amount; - $this->base_due_amount = $this->due_amount * $this->exchange_rate; - - $this->changeInvoiceStatus($this->due_amount); - } - - /** - * Determine the invoice status and paid_status based on the remaining due amount. - * - * Returns an empty array for negative amounts, marks as paid when zero, - * unpaid when equal to total, or partially paid otherwise. - */ - public function getInvoiceStatusByAmount(int $amount): array - { - if ($amount < 0) { - return []; - } - - if ($amount == 0) { - $data = [ - 'status' => Invoice::STATUS_COMPLETED, - 'paid_status' => Invoice::STATUS_PAID, - 'overdue' => false, - ]; - } elseif ($amount == $this->total) { - $data = [ - 'status' => $this->getPreviousStatus(), - 'paid_status' => Invoice::STATUS_UNPAID, - ]; - } else { - $data = [ - 'status' => $this->getPreviousStatus(), - 'paid_status' => Invoice::STATUS_PARTIALLY_PAID, - ]; - } - - return $data; - } - - /** - * Persist the invoice status change immediately based on the given due amount. - */ - public function changeInvoiceStatus(int $amount): void - { - $status = $this->getInvoiceStatusByAmount($amount); - if (! empty($status)) { - foreach ($status as $key => $value) { - $this->setAttribute($key, $value); - } - $this->save(); - } - } -} diff --git a/app/Domains/Sales/Models/InvoiceItem.php b/app/Domains/Sales/Models/InvoiceItem.php deleted file mode 100644 index dc4ecbde..00000000 --- a/app/Domains/Sales/Models/InvoiceItem.php +++ /dev/null @@ -1,91 +0,0 @@ - 'integer', - 'total' => 'integer', - 'discount' => 'float', - 'quantity' => 'float', - 'discount_val' => 'integer', - 'tax' => 'integer', - ]; - } - - public function invoice(): BelongsTo - { - return $this->belongsTo(Invoice::class); - } - - public function item(): BelongsTo - { - return $this->belongsTo(Item::class); - } - - public function taxes(): HasMany - { - return $this->hasMany(Tax::class); - } - - public function recurringInvoice(): BelongsTo - { - return $this->belongsTo(RecurringInvoice::class); - } - - public function scopeWhereCompany(Builder $query, int $company_id): void - { - $query->where('company_id', $company_id); - } - - public function scopeInvoicesBetween(Builder $query, Carbon $start, Carbon $end): void - { - $query->whereHas('invoice', function ($query) use ($start, $end) { - $query->whereBetween( - 'invoice_date', - [$start->format('Y-m-d'), $end->format('Y-m-d')] - ); - }); - } - - public function scopeApplyInvoiceFilters(Builder $query, array $filters): void - { - $filters = collect($filters); - - if ($filters->get('from_date') && $filters->get('to_date')) { - $start = Carbon::createFromFormat('Y-m-d', $filters->get('from_date')); - $end = Carbon::createFromFormat('Y-m-d', $filters->get('to_date')); - $query->invoicesBetween($start, $end); - } - } - - public function scopeItemAttributes(Builder $query): void - { - $query->select( - DB::raw('sum(quantity) as total_quantity, sum(base_total) as total_amount, invoice_items.name') - )->groupBy('invoice_items.name'); - } -} diff --git a/app/Domains/Sales/Models/RecurringInvoice.php b/app/Domains/Sales/Models/RecurringInvoice.php deleted file mode 100644 index fda157c7..00000000 --- a/app/Domains/Sales/Models/RecurringInvoice.php +++ /dev/null @@ -1,225 +0,0 @@ - 'float', - 'send_automatically' => 'boolean', - ]; - } - - public function getFormattedStartsAtAttribute() - { - $dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id); - - return Carbon::parse($this->starts_at)->translatedFormat($dateFormat); - } - - public function getFormattedNextInvoiceAtAttribute() - { - $dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id); - - return Carbon::parse($this->next_invoice_at)->translatedFormat($dateFormat); - } - - public function getFormattedLimitDateAttribute() - { - $dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id); - - return Carbon::parse($this->limit_date)->format($dateFormat); - } - - public function getFormattedCreatedAtAttribute() - { - $dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id); - - return Carbon::parse($this->created_at)->format($dateFormat); - } - - public function invoices(): HasMany - { - return $this->hasMany(Invoice::class); - } - - public function taxes(): HasMany - { - return $this->hasMany(Tax::class); - } - - public function items(): HasMany - { - return $this->hasMany(InvoiceItem::class); - } - - public function customer(): BelongsTo - { - return $this->belongsTo(Customer::class); - } - - public function company(): BelongsTo - { - return $this->belongsTo(Company::class); - } - - public function creator(): BelongsTo - { - return $this->belongsTo(User::class, 'creator_id'); - } - - public function currency(): BelongsTo - { - return $this->belongsTo(Currency::class); - } - - public function scopeWhereCompany($query) - { - $query->where('recurring_invoices.company_id', request()->header('company')); - } - - public function scopePaginateData($query, $limit) - { - if ($limit == 'all') { - return $query->get(); - } - - return $query->paginate($limit); - } - - public function scopeWhereOrder($query, $orderByField, $orderBy) - { - SafeOrderBy::apply($query, $orderByField, $orderBy); - } - - public function scopeWhereStatus($query, $status) - { - return $query->where('recurring_invoices.status', $status); - } - - public function scopeWhereCustomer($query, $customer_id) - { - $query->where('customer_id', $customer_id); - } - - public function scopeRecurringInvoicesStartBetween($query, $start, $end) - { - return $query->whereBetween( - 'starts_at', - [$start->format('Y-m-d'), $end->format('Y-m-d')] - ); - } - - public function scopeWhereSearch($query, $search) - { - foreach (explode(' ', $search) as $term) { - $query->whereHas('customer', function ($query) use ($term) { - $query->where('name', 'LIKE', '%'.$term.'%') - ->orWhere('contact_name', 'LIKE', '%'.$term.'%') - ->orWhere('company_name', 'LIKE', '%'.$term.'%'); - }); - } - } - - public function scopeApplyFilters($query, array $filters) - { - $filters = collect($filters); - - if ($filters->get('status') && $filters->get('status') !== 'ALL') { - $query->whereStatus($filters->get('status')); - } - - if ($filters->get('search')) { - $query->whereSearch($filters->get('search')); - } - - if ($filters->get('from_date') && $filters->get('to_date')) { - $start = Carbon::createFromFormat('Y-m-d', $filters->get('from_date')); - $end = Carbon::createFromFormat('Y-m-d', $filters->get('to_date')); - $query->recurringInvoicesStartBetween($start, $end); - } - - if ($filters->get('customer_id')) { - $query->whereCustomer($filters->get('customer_id')); - } - - if ($filters->get('orderByField') || $filters->get('orderBy')) { - $field = $filters->get('orderByField') ? $filters->get('orderByField') : 'created_at'; - $orderBy = $filters->get('orderBy') ? $filters->get('orderBy') : 'asc'; - $query->whereOrder($field, $orderBy); - } - } - - public function markStatusAsCompleted(): void - { - if ($this->status == $this->status) { - $this->status = self::COMPLETED; - $this->save(); - } - } - - public static function getNextInvoiceDate(string $frequency, string $starts_at): string - { - $cron = new Cron\CronExpression($frequency); - $timezone = config('app.timezone', 'UTC'); - - return $cron->getNextRunDate($starts_at, 0, false, $timezone)->format('Y-m-d H:i:s'); - } - - public function updateNextInvoiceDate(): void - { - $nextInvoiceAt = self::getNextInvoiceDate($this->frequency, $this->starts_at); - - $this->next_invoice_at = $nextInvoiceAt; - $this->save(); - } -} diff --git a/app/Domains/Sales/Policies/EstimatePolicy.php b/app/Domains/Sales/Policies/EstimatePolicy.php deleted file mode 100644 index 85a71d16..00000000 --- a/app/Domains/Sales/Policies/EstimatePolicy.php +++ /dev/null @@ -1,140 +0,0 @@ -hasCompany($estimate->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can create models. - * - * @return mixed - */ - public function create(User $user): bool - { - if (BouncerFacade::can('create-estimate', Estimate::class)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can update the model. - * - * @return mixed - */ - public function update(User $user, Estimate $estimate): bool - { - if (BouncerFacade::can('edit-estimate', $estimate) && $user->hasCompany($estimate->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can delete the model. - * - * @return mixed - */ - public function delete(User $user, Estimate $estimate): bool - { - if (BouncerFacade::can('delete-estimate', $estimate) && $user->hasCompany($estimate->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can restore the model. - * - * @return mixed - */ - public function restore(User $user, Estimate $estimate): bool - { - if (BouncerFacade::can('delete-estimate', $estimate) && $user->hasCompany($estimate->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can permanently delete the model. - * - * @return mixed - */ - public function forceDelete(User $user, Estimate $estimate): bool - { - if (BouncerFacade::can('delete-estimate', $estimate) && $user->hasCompany($estimate->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can send email of the model. - * - * @param Estimate $payment - * @return mixed - */ - public function send(User $user, Estimate $estimate) - { - if (BouncerFacade::can('send-estimate', $estimate) && $user->hasCompany($estimate->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can delete models. - * - * @return mixed - */ - public function deleteMultiple(User $user) - { - if (BouncerFacade::can('delete-estimate', Estimate::class)) { - return true; - } - - return false; - } -} diff --git a/app/Domains/Sales/Policies/InvoicePolicy.php b/app/Domains/Sales/Policies/InvoicePolicy.php deleted file mode 100644 index 5d2b4cde..00000000 --- a/app/Domains/Sales/Policies/InvoicePolicy.php +++ /dev/null @@ -1,148 +0,0 @@ -hasCompany($invoice->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can create models. - * - * @return mixed - */ - public function create(User $user): bool - { - if (BouncerFacade::can('create-invoice', Invoice::class)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can update the model. - * - * @return mixed - */ - public function update(User $user, Invoice $invoice): bool - { - // A credit note is a reversal document: it is immutable once minted, - // because saving it back through the invoice form would recompute its - // totals positive. - if ($invoice->isCreditNote()) { - return false; - } - - if (BouncerFacade::can('edit-invoice', $invoice) && $user->hasCompany($invoice->company_id)) { - return $invoice->allow_edit; - } - - return false; - } - - /** - * Determine whether the user can delete the model. - * - * @return mixed - */ - public function delete(User $user, Invoice $invoice): bool - { - if (BouncerFacade::can('delete-invoice', $invoice) && $user->hasCompany($invoice->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can restore the model. - * - * @return mixed - */ - public function restore(User $user, Invoice $invoice): bool - { - if (BouncerFacade::can('delete-invoice', $invoice) && $user->hasCompany($invoice->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can permanently delete the model. - * - * @return mixed - */ - public function forceDelete(User $user, Invoice $invoice): bool - { - if (BouncerFacade::can('delete-invoice', $invoice) && $user->hasCompany($invoice->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can send email of the model. - * - * @param Payment $payment - * @return mixed - */ - public function send(User $user, Invoice $invoice) - { - if (BouncerFacade::can('send-invoice', $invoice) && $user->hasCompany($invoice->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can delete models. - * - * @return mixed - */ - public function deleteMultiple(User $user) - { - if (BouncerFacade::can('delete-invoice', Invoice::class)) { - return true; - } - - return false; - } -} diff --git a/app/Domains/Sales/Policies/RecurringInvoicePolicy.php b/app/Domains/Sales/Policies/RecurringInvoicePolicy.php deleted file mode 100644 index 805730a8..00000000 --- a/app/Domains/Sales/Policies/RecurringInvoicePolicy.php +++ /dev/null @@ -1,126 +0,0 @@ -hasCompany($recurringInvoice->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can create models. - * - * @return Response|bool - */ - public function create(User $user): bool - { - if (BouncerFacade::can('create-recurring-invoice', RecurringInvoice::class)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can update the model. - * - * @return Response|bool - */ - public function update(User $user, RecurringInvoice $recurringInvoice): bool - { - if (BouncerFacade::can('edit-recurring-invoice', $recurringInvoice) && $user->hasCompany($recurringInvoice->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can delete the model. - * - * @return Response|bool - */ - public function delete(User $user, RecurringInvoice $recurringInvoice): bool - { - if (BouncerFacade::can('delete-recurring-invoice', $recurringInvoice) && $user->hasCompany($recurringInvoice->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can restore the model. - * - * @return Response|bool - */ - public function restore(User $user, RecurringInvoice $recurringInvoice): bool - { - if (BouncerFacade::can('delete-recurring-invoice', $recurringInvoice) && $user->hasCompany($recurringInvoice->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can permanently delete the model. - * - * @return Response|bool - */ - public function forceDelete(User $user, RecurringInvoice $recurringInvoice): bool - { - if (BouncerFacade::can('delete-recurring-invoice', $recurringInvoice) && $user->hasCompany($recurringInvoice->company_id)) { - return true; - } - - return false; - } - - /** - * Determine whether the user can delete models. - * - * @return mixed - */ - public function deleteMultiple(User $user) - { - if (BouncerFacade::can('delete-recurring-invoice', RecurringInvoice::class)) { - return true; - } - - return false; - } -}