chore(sales): remove legacy-era sales sources

This commit is contained in:
Darko Gjorgjijoski
2026-08-21 01:26:48 +02:00
parent 7170ba8241
commit e5dc314b68
53 changed files with 0 additions and 4564 deletions
@@ -1,269 +0,0 @@
<?php
namespace App\Domains\Sales\Application;
use App\Domains\Accounts\Models\CompanySetting;
use App\Domains\Contacts\Models\Customer;
class SerialNumberService
{
public const VALID_PLACEHOLDERS = ['CUSTOMER_SERIES', 'SEQUENCE', 'DATE_FORMAT', 'SERIES', 'RANDOM_SEQUENCE', 'DELIMITER', 'CUSTOMER_SEQUENCE'];
private $model;
private $ob;
private $customer;
private $company;
private $settingKey;
private $sequenceScope = [];
/**
* @var string
*/
public $nextSequenceNumber;
/**
* @var string
*/
public $nextCustomerSequenceNumber;
/**
* @return $this
*/
public function setModel($model)
{
$this->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;
}
}
@@ -1,52 +0,0 @@
<?php
namespace App\Domains\Sales\Console;
use App\Domains\Sales\Models\Estimate;
use Carbon\Carbon;
use Illuminate\Console\Command;
class CheckEstimateStatus extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:estimates:status';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check invoices status.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(): void
{
$date = Carbon::now();
$status = [Estimate::STATUS_ACCEPTED, Estimate::STATUS_REJECTED, Estimate::STATUS_EXPIRED];
$estimates = Estimate::whereNotIn('status', $status)->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();
}
}
}
@@ -1,57 +0,0 @@
<?php
namespace App\Domains\Sales\Console;
use App\Domains\Sales\Models\Invoice;
use Carbon\Carbon;
use Illuminate\Console\Command;
class CheckInvoiceStatus extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:invoices:status';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check invoices status.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle(): void
{
$date = Carbon::now();
// Only real invoices can fall overdue: a credit note is never owed, so
// it must never be flagged no matter what date it carries.
$invoices = Invoice::where('type', Invoice::TYPE_INVOICE)
->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();
}
}
}
@@ -1,28 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Controllers\Company;
use App\Domains\Sales\Models\Estimate;
use App\Platform\Http\Controller;
use App\Platform\Pdf\Rendering\PdfTemplateUtils;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class EstimateTemplatesController extends Controller
{
/**
* Handle the incoming request.
*
* @return JsonResponse
*/
public function __invoke(Request $request)
{
$this->authorize('viewAny', Estimate::class);
$estimateTemplates = PdfTemplateUtils::getFormattedTemplates('estimate');
return response()->json([
'estimateTemplates' => $estimateTemplates,
]);
}
}
@@ -1,162 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Controllers\Company;
use App\Domains\Sales\Application\EstimateService;
use App\Domains\Sales\Http\Requests\DeleteEstimatesRequest;
use App\Domains\Sales\Http\Requests\EstimatesRequest;
use App\Domains\Sales\Http\Requests\SendEstimatesRequest;
use App\Domains\Sales\Http\Resources\EstimateResource;
use App\Domains\Sales\Http\Resources\InvoiceResource;
use App\Domains\Sales\Jobs\GenerateEstimatePdfJob;
use App\Domains\Sales\Models\Estimate;
use App\Domains\Sales\Models\Invoice;
use App\Platform\Http\Controller;
use Illuminate\Http\Request;
use Illuminate\Mail\Markdown;
class EstimatesController extends Controller
{
public function __construct(
private readonly EstimateService $estimateService,
) {}
public function index(Request $request)
{
$this->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;
}
}
@@ -1,32 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Controllers\Company;
use App\Domains\Sales\Models\Invoice;
use App\Platform\Http\Controller;
use App\Platform\Pdf\Rendering\PdfTemplateUtils;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class InvoiceTemplatesController extends Controller
{
/**
* Handle the incoming request.
*
*
* @return JsonResponse
*
* @throws AuthorizationException
*/
public function __invoke(Request $request)
{
$this->authorize('viewAny', Invoice::class);
$invoiceTemplates = PdfTemplateUtils::getFormattedTemplates('invoice');
return response()->json([
'invoiceTemplates' => $invoiceTemplates,
]);
}
}
@@ -1,120 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Controllers\Company;
use App\Domains\Sales\Application\RecurringInvoiceService;
use App\Domains\Sales\Http\Requests\RecurringInvoiceRequest;
use App\Domains\Sales\Http\Resources\RecurringInvoiceResource;
use App\Domains\Sales\Models\RecurringInvoice;
use App\Platform\Http\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class RecurringInvoiceController extends Controller
{
public function __construct(
private readonly RecurringInvoiceService $recurringInvoiceService,
) {}
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index(Request $request)
{
$this->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;
}
}
@@ -1,20 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Controllers\Company;
use App\Domains\Sales\Models\RecurringInvoice;
use App\Platform\Http\Controller;
use Illuminate\Http\Request;
class RecurringInvoiceFrequencyController extends Controller
{
public function __invoke(Request $request)
{
$nextInvoiceAt = RecurringInvoice::getNextInvoiceDate($request->frequency, $request->starts_at);
return response()->json([
'success' => true,
'next_invoice_at' => $nextInvoiceAt,
]);
}
}
@@ -1,89 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Controllers\Company;
use App\Domains\Receivables\Models\Payment;
use App\Domains\Sales\Application\SerialNumberService;
use App\Domains\Sales\Models\Estimate;
use App\Domains\Sales\Models\Invoice;
use App\Platform\Http\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SerialNumberController extends Controller
{
public function nextNumber(Request $request, Invoice $invoice, Estimate $estimate, Payment $payment): JsonResponse
{
$key = $request->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,
]);
}
}
@@ -1,36 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Controllers\CustomerPortal;
use App\Domains\Accounts\Models\Company;
use App\Domains\Sales\Http\Resources\CustomerPortal\EstimateResource;
use App\Domains\Sales\Models\Estimate;
use App\Platform\Http\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
class AcceptEstimateController extends Controller
{
/**
* Handle the incoming request.
*
* @param Estimate $estimate
* @return Response
*/
public function __invoke(Request $request, 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);
}
$estimate->update($request->only('status'));
return new EstimateResource($estimate);
}
}
@@ -1,53 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Controllers\CustomerPortal;
use App\Domains\Accounts\Models\CompanySetting;
use App\Domains\Contacts\Models\Customer;
use App\Domains\Sales\Http\Resources\EstimateResource;
use App\Domains\Sales\Mail\EstimateViewedMail;
use App\Domains\Sales\Models\Estimate;
use App\Platform\Http\Controller;
use App\Platform\Mail\Models\EmailLog;
use Illuminate\Http\Request;
class EstimatePdfController extends Controller
{
public function getPdf(EmailLog $emailLog, Request $request)
{
$estimate = $emailLog->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);
}
}
@@ -1,68 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Controllers\CustomerPortal;
use App\Domains\Accounts\Models\Company;
use App\Domains\Sales\Http\Resources\CustomerPortal\EstimateResource;
use App\Domains\Sales\Models\Estimate;
use App\Platform\Http\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
class EstimatesController extends Controller
{
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index(Request $request)
{
$limit = $request->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);
}
}
@@ -1,64 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Controllers\CustomerPortal;
use App\Domains\Accounts\Models\CompanySetting;
use App\Domains\Contacts\Models\Customer;
use App\Domains\Sales\Http\Resources\CustomerPortal\InvoiceResource as CustomerInvoiceResource;
use App\Domains\Sales\Mail\InvoiceViewedMail;
use App\Domains\Sales\Models\Invoice;
use App\Platform\Http\Controller;
use App\Platform\Mail\Models\EmailLog;
use Illuminate\Http\Request;
class InvoicePdfController extends Controller
{
public function getPdf(EmailLog $emailLog, Request $request)
{
// Resolve the document through the morph relation and enforce the
// expected type — a token issued for another mailable type (or whose
// numeric id collides) must not disclose an invoice.
$invoice = $emailLog->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);
}
}
@@ -1,52 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Controllers\CustomerPortal;
use App\Domains\Accounts\Models\Company;
use App\Domains\Sales\Http\Resources\CustomerPortal\InvoiceResource;
use App\Domains\Sales\Models\Invoice;
use App\Platform\Http\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
class InvoicesController extends Controller
{
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index(Request $request)
{
$limit = $request->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);
}
}
@@ -1,33 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class DeleteEstimatesRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'ids' => [
'required',
],
'ids.*' => [
'required',
Rule::exists('estimates', 'id'),
],
];
}
}
@@ -1,38 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Requests;
use App\Domains\Sales\Models\Invoice;
use App\Rules\CreditNoteDeletedTogether;
use App\Rules\RelationNotExist;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class DeleteInvoiceRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'ids' => [
'required',
],
'ids.*' => [
'required',
Rule::exists('invoices', 'id'),
new RelationNotExist(Invoice::class, 'payments'),
new CreditNoteDeletedTogether((array) $this->input('ids', [])),
],
];
}
}
@@ -1,165 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Requests;
use App\Domains\Accounts\Models\CompanySetting;
use App\Domains\Contacts\Models\Customer;
use App\Domains\Sales\Models\Estimate;
use App\Platform\Pdf\Rules\PdfTemplateExists;
use App\Support\DocumentTotals;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class EstimatesRequest extends FormRequest
{
use Concerns\ValidatesDocumentTaxPlaceholders;
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'estimate_date' => [
'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();
}
}
@@ -1,178 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Requests;
use App\Domains\Accounts\Models\CompanySetting;
use App\Domains\Contacts\Models\Customer;
use App\Domains\Sales\Models\Invoice;
use App\Platform\Pdf\Rules\PdfTemplateExists;
use App\Support\DocumentTotals;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class InvoicesRequest extends FormRequest
{
use Concerns\ValidatesDocumentTaxPlaceholders;
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.s
*/
public function rules(): array
{
$rules = [
'invoice_date' => [
'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();
}
}
@@ -1,154 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Requests;
use App\Domains\Accounts\Models\CompanySetting;
use App\Domains\Contacts\Models\Customer;
use App\Domains\Sales\Models\RecurringInvoice;
use App\Support\DocumentTotals;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class RecurringInvoiceRequest extends FormRequest
{
use Concerns\ValidatesDocumentTaxPlaceholders;
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$companyCurrency = CompanySetting::getSetting('currency', $this->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();
}
}
@@ -1,43 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SendEstimatesRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'subject' => [
'required',
],
'body' => [
'required',
],
'from' => [
'required',
],
'to' => [
'required',
],
'cc' => [
'nullable',
],
'bcc' => [
'nullable',
],
];
}
}
@@ -1,43 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SendInvoiceRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'body' => [
'required',
],
'subject' => [
'required',
],
'from' => [
'required',
],
'to' => [
'required',
],
'cc' => [
'nullable',
],
'bcc' => [
'nullable',
],
];
}
}
@@ -1,19 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class EstimateCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return parent::toArray($request);
}
}
@@ -1,19 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class EstimateItemCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return parent::toArray($request);
}
}
@@ -1,47 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
use App\Domains\Metadata\Http\Resources\CustomerPortal\CustomFieldValueResource;
use App\Domains\Taxation\Http\Resources\CustomerPortal\TaxResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class EstimateItemResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return [
'id' => $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);
}),
];
}
}
@@ -1,70 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
use App\Domains\Accounts\Http\Resources\CustomerPortal\CompanyResource;
use App\Domains\Contacts\Http\Resources\CustomerPortal\CustomerResource;
use App\Domains\Metadata\Http\Resources\CustomerPortal\CustomFieldValueResource;
use App\Domains\Money\Http\Resources\CustomerPortal\CurrencyResource;
use App\Domains\Taxation\Http\Resources\CustomerPortal\TaxResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class EstimateResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return [
'id' => $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);
}),
];
}
}
@@ -1,19 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class InvoiceCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return parent::toArray($request);
}
}
@@ -1,19 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class InvoiceItemCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return parent::toArray($request);
}
}
@@ -1,48 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
use App\Domains\Metadata\Http\Resources\CustomerPortal\CustomFieldValueResource;
use App\Domains\Taxation\Http\Resources\CustomerPortal\TaxResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class InvoiceItemResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return [
'id' => $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);
}),
];
}
}
@@ -1,80 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources\CustomerPortal;
use App\Domains\Accounts\Http\Resources\CustomerPortal\CompanyResource;
use App\Domains\Contacts\Http\Resources\CustomerPortal\CustomerResource;
use App\Domains\Metadata\Http\Resources\CustomerPortal\CustomFieldValueResource;
use App\Domains\Money\Http\Resources\CustomerPortal\CurrencyResource;
use App\Domains\Taxation\Http\Resources\CustomerPortal\TaxResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class InvoiceResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return [
'id' => $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);
}),
];
}
}
@@ -1,19 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class EstimateCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return parent::toArray($request);
}
}
@@ -1,19 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class EstimateItemCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return parent::toArray($request);
}
}
@@ -1,47 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources;
use App\Domains\Metadata\Http\Resources\CustomFieldValueResource;
use App\Domains\Taxation\Http\Resources\TaxResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class EstimateItemResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return [
'id' => $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);
}),
];
}
}
@@ -1,79 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources;
use App\Domains\Accounts\Http\Resources\CompanyResource;
use App\Domains\Accounts\Http\Resources\UserResource;
use App\Domains\Contacts\Http\Resources\CustomerResource;
use App\Domains\Metadata\Http\Resources\CustomFieldValueResource;
use App\Domains\Money\Http\Resources\CurrencyResource;
use App\Domains\Taxation\Http\Resources\TaxResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class EstimateResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return [
'id' => $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);
}),
];
}
}
@@ -1,19 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class InvoiceCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return parent::toArray($request);
}
}
@@ -1,19 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class InvoiceItemCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return parent::toArray($request);
}
}
@@ -1,48 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources;
use App\Domains\Metadata\Http\Resources\CustomFieldValueResource;
use App\Domains\Taxation\Http\Resources\TaxResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class InvoiceItemResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return [
'id' => $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);
}),
];
}
}
@@ -1,178 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources;
use App\Domains\Accounts\Http\Resources\CompanyResource;
use App\Domains\Accounts\Http\Resources\UserResource;
use App\Domains\Contacts\Http\Resources\CustomerResource;
use App\Domains\Metadata\Http\Resources\CustomFieldValueResource;
use App\Domains\Money\Http\Resources\CurrencyResource;
use App\Domains\Taxation\Http\Resources\TaxResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class InvoiceResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return [
'id' => $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');
}
}
@@ -1,19 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class RecurringInvoiceCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return parent::toArray($request);
}
}
@@ -1,81 +0,0 @@
<?php
namespace App\Domains\Sales\Http\Resources;
use App\Domains\Accounts\Http\Resources\CompanyResource;
use App\Domains\Accounts\Http\Resources\UserResource;
use App\Domains\Contacts\Http\Resources\CustomerResource;
use App\Domains\Metadata\Http\Resources\CustomFieldValueResource;
use App\Domains\Money\Http\Resources\CurrencyResource;
use App\Domains\Taxation\Http\Resources\TaxResource;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class RecurringInvoiceResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
*/
public function toArray($request): array
{
return [
'id' => $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);
}),
];
}
}
@@ -1,42 +0,0 @@
<?php
namespace App\Domains\Sales\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class GenerateEstimatePdfJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public $estimate;
public $deleteExistingFile;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($estimate, $deleteExistingFile = false)
{
$this->estimate = $estimate;
$this->deleteExistingFile = $deleteExistingFile;
}
/**
* Execute the job.
*/
public function handle(): int
{
$this->estimate->generatePDF('estimate', $this->estimate->estimate_number, $this->deleteExistingFile);
return 0;
}
}
@@ -1,42 +0,0 @@
<?php
namespace App\Domains\Sales\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class GenerateInvoicePdfJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public $invoice;
public $deleteExistingFile;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($invoice, $deleteExistingFile = false)
{
$this->invoice = $invoice;
$this->deleteExistingFile = $deleteExistingFile;
}
/**
* Execute the job.
*/
public function handle(): int
{
$this->invoice->generatePDF('invoice', $this->invoice->invoice_number, $this->deleteExistingFile);
return 0;
}
}
@@ -1,37 +0,0 @@
<?php
namespace App\Domains\Sales\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class EstimateViewedMail extends Mailable
{
use Queueable;
use SerializesModels;
public $data;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($data)
{
$this->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]);
}
}
@@ -1,37 +0,0 @@
<?php
namespace App\Domains\Sales\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class InvoiceViewedMail extends Mailable
{
use Queueable;
use SerializesModels;
public $data;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($data)
{
$this->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]);
}
}
@@ -1,67 +0,0 @@
<?php
namespace App\Domains\Sales\Mail;
use App\Domains\Sales\Models\Estimate;
use App\Facades\Hashids;
use App\Platform\Mail\Models\EmailLog;
use App\Platform\Persistence\ModelIdentityMap;
use App\Support\Hashids\HashidConnection;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class SendEstimateMail extends Mailable
{
use Queueable;
use SerializesModels;
public $data = [];
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($data)
{
$this->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;
}
}
@@ -1,67 +0,0 @@
<?php
namespace App\Domains\Sales\Mail;
use App\Domains\Sales\Models\Invoice;
use App\Facades\Hashids;
use App\Platform\Mail\Models\EmailLog;
use App\Platform\Persistence\ModelIdentityMap;
use App\Support\Hashids\HashidConnection;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class SendInvoiceMail extends Mailable
{
use Queueable;
use SerializesModels;
public $data = [];
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($data)
{
$this->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;
}
}
-343
View File
@@ -1,343 +0,0 @@
<?php
namespace App\Domains\Sales\Models;
use App\Domains\Accounts\Models\Company;
use App\Domains\Accounts\Models\CompanySetting;
use App\Domains\Accounts\Models\User;
use App\Domains\Contacts\Models\Customer;
use App\Domains\Metadata\Concerns\HasCustomFields;
use App\Domains\Money\Models\Currency;
use App\Domains\Sales\Contracts\EstimatePdfDataProvider;
use App\Domains\Taxation\Models\Tax;
use App\Platform\Mail\Models\EmailLog;
use App\Platform\Pdf\Concerns\GeneratesPdf;
use App\Platform\Pdf\Rendering\PdfHtmlSanitizer;
use App\Platform\Pdf\Rendering\PdfTemplateUtils;
use App\Support\SafeOrderBy;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Str;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
class Estimate extends Model implements HasMedia
{
protected $table = 'estimates';
use GeneratesPdf;
use HasCustomFields;
use HasFactory;
use InteractsWithMedia;
public const STATUS_DRAFT = 'DRAFT';
public const STATUS_SENT = 'SENT';
public const STATUS_VIEWED = 'VIEWED';
public const STATUS_EXPIRED = 'EXPIRED';
public const STATUS_ACCEPTED = 'ACCEPTED';
public const STATUS_REJECTED = 'REJECTED';
protected $dates = [
'created_at',
'updated_at',
'deleted_at',
'estimate_date',
'expiry_date',
];
protected $appends = [
'formattedExpiryDate',
'formattedEstimateDate',
'estimatePdfUrl',
];
protected $guarded = ['id'];
protected function casts(): array
{
return [
'total' => '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;
}
}
-56
View File
@@ -1,56 +0,0 @@
<?php
namespace App\Domains\Sales\Models;
use App\Domains\Catalog\Models\Item;
use App\Domains\Metadata\Concerns\HasCustomFields;
use App\Domains\Taxation\Models\Tax;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class EstimateItem extends Model
{
protected $table = 'estimate_items';
use HasCustomFields;
use HasFactory;
protected $guarded = [
'id',
];
protected function casts(): array
{
return [
'price' => '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);
}
}
-538
View File
@@ -1,538 +0,0 @@
<?php
namespace App\Domains\Sales\Models;
use App\Domains\Accounts\Models\Company;
use App\Domains\Accounts\Models\CompanySetting;
use App\Domains\Accounts\Models\User;
use App\Domains\Contacts\Models\Customer;
use App\Domains\Metadata\Concerns\HasCustomFields;
use App\Domains\Money\Models\Currency;
use App\Domains\Receivables\Models\Payment;
use App\Domains\Receivables\Models\PaymentAllocation;
use App\Domains\Receivables\Models\Transaction;
use App\Domains\Sales\Contracts\InvoicePdfDataProvider;
use App\Domains\Taxation\Models\Tax;
use App\Platform\Mail\Models\EmailLog;
use App\Platform\Pdf\Concerns\GeneratesPdf;
use App\Platform\Pdf\Rendering\PdfHtmlSanitizer;
use App\Platform\Pdf\Rendering\PdfTemplateUtils;
use App\Support\SafeOrderBy;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Str;
use Nwidart\Modules\Facades\Module;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
class Invoice extends Model implements HasMedia
{
protected $table = 'invoices';
use GeneratesPdf;
use HasCustomFields;
use HasFactory;
use InteractsWithMedia;
public const STATUS_DRAFT = 'DRAFT';
public const STATUS_SENT = 'SENT';
public const STATUS_VIEWED = 'VIEWED';
public const STATUS_COMPLETED = 'COMPLETED';
public const STATUS_UNPAID = 'UNPAID';
public const STATUS_PARTIALLY_PAID = 'PARTIALLY_PAID';
public const STATUS_PAID = 'PAID';
public const TYPE_INVOICE = 'INVOICE';
public const TYPE_CREDIT_NOTE = 'CREDIT_NOTE';
protected $dates = [
'created_at',
'updated_at',
'deleted_at',
'invoice_date',
'due_date',
];
protected $guarded = [
'id',
];
protected $appends = [
'formattedCreatedAt',
'formattedInvoiceDate',
'formattedDueDate',
'formattedDueAmount',
'invoicePdfUrl',
];
protected function casts(): array
{
return [
'total' => '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();
}
}
}
-91
View File
@@ -1,91 +0,0 @@
<?php
namespace App\Domains\Sales\Models;
use App\Domains\Catalog\Models\Item;
use App\Domains\Metadata\Concerns\HasCustomFields;
use App\Domains\Taxation\Models\Tax;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\DB;
class InvoiceItem extends Model
{
protected $table = 'invoice_items';
use HasCustomFields;
use HasFactory;
protected $guarded = [
'id',
];
protected function casts(): array
{
return [
'price' => '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');
}
}
@@ -1,225 +0,0 @@
<?php
namespace App\Domains\Sales\Models;
use App\Domains\Accounts\Models\Company;
use App\Domains\Accounts\Models\CompanySetting;
use App\Domains\Accounts\Models\User;
use App\Domains\Contacts\Models\Customer;
use App\Domains\Metadata\Concerns\HasCustomFields;
use App\Domains\Money\Models\Currency;
use App\Domains\Taxation\Models\Tax;
use App\Support\SafeOrderBy;
use Carbon\Carbon;
use Cron;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class RecurringInvoice extends Model
{
protected $table = 'recurring_invoices';
use HasCustomFields;
use HasFactory;
protected $guarded = [
'id',
];
protected $dates = [
'starts_at',
];
public const NONE = 'NONE';
public const COUNT = 'COUNT';
public const DATE = 'DATE';
public const COMPLETED = 'COMPLETED';
public const ON_HOLD = 'ON_HOLD';
public const ACTIVE = 'ACTIVE';
protected $appends = [
'formattedCreatedAt',
'formattedStartsAt',
'formattedNextInvoiceAt',
'formattedLimitDate',
];
protected function casts(): array
{
return [
'exchange_rate' => '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();
}
}
@@ -1,140 +0,0 @@
<?php
namespace App\Domains\Sales\Policies;
use App\Domains\Accounts\Models\User;
use App\Domains\Sales\Models\Estimate;
use Illuminate\Auth\Access\HandlesAuthorization;
use Silber\Bouncer\BouncerFacade;
class EstimatePolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* @return mixed
*/
public function viewAny(User $user): bool
{
if (BouncerFacade::can('view-estimate', Estimate::class)) {
return true;
}
return false;
}
/**
* Determine whether the user can view the model.
*
* @return mixed
*/
public function view(User $user, Estimate $estimate): bool
{
if (BouncerFacade::can('view-estimate', $estimate) && $user->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;
}
}
@@ -1,148 +0,0 @@
<?php
namespace App\Domains\Sales\Policies;
use App\Domains\Accounts\Models\User;
use App\Domains\Receivables\Models\Payment;
use App\Domains\Sales\Models\Invoice;
use Illuminate\Auth\Access\HandlesAuthorization;
use Silber\Bouncer\BouncerFacade;
class InvoicePolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* @return mixed
*/
public function viewAny(User $user): bool
{
if (BouncerFacade::can('view-invoice', Invoice::class)) {
return true;
}
return false;
}
/**
* Determine whether the user can view the model.
*
* @return mixed
*/
public function view(User $user, Invoice $invoice): bool
{
if (BouncerFacade::can('view-invoice', $invoice) && $user->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;
}
}
@@ -1,126 +0,0 @@
<?php
namespace App\Domains\Sales\Policies;
use App\Domains\Accounts\Models\User;
use App\Domains\Sales\Models\RecurringInvoice;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Auth\Access\Response;
use Silber\Bouncer\BouncerFacade;
class RecurringInvoicePolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* @return Response|bool
*/
public function viewAny(User $user): bool
{
if (BouncerFacade::can('view-recurring-invoice', RecurringInvoice::class)) {
return true;
}
return false;
}
/**
* Determine whether the user can view the model.
*
* @return Response|bool
*/
public function view(User $user, RecurringInvoice $recurringInvoice): bool
{
if (BouncerFacade::can('view-recurring-invoice', $recurringInvoice) && $user->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;
}
}