feat(billing): renewals, coupons, VAT numbering, billing cycles and invoice payment tracking

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-19 16:37:56 +07:00
co-authored by Claude Sonnet 5.5
parent ea9a53f742
commit 25f23d541e
134 changed files with 4926 additions and 185 deletions
@@ -2,6 +2,7 @@
namespace App\Repositories\Concerns;
use App\Models\InvoiceAttachment;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
@@ -28,6 +29,53 @@ trait FiltersBillingRecords
->when($to, fn (Builder $q) => $q->where('created_at', '<=', Carbon::parse($to)->endOfDay()));
}
/** @param callable(): array<int, mixed> $duplicates */
private function whereIsDuplicate(Builder $query, mixed $flag, callable $duplicates): Builder
{
if (!filter_var($flag, FILTER_VALIDATE_BOOLEAN)) {
return $query;
}
return $query->whereIn($query->getModel()->getQualifiedKeyName(), array_keys($duplicates()));
}
/**
* @param array<int, string> $columns
*/
private function applySort(Builder $query, mixed $sortBy, mixed $sortOrder, array $columns): Builder
{
$direction = strtolower((string) $sortOrder) === 'asc' ? 'asc' : 'desc';
$table = $query->getModel()->getTable();
if ($sortBy === 'customer') {
return $query
->orderBy(
\App\Models\Customer::query()->select('fullname')->whereColumn('customers.id', "{$table}.customer_id")->limit(1),
$direction
)
->orderByDesc("{$table}.id");
}
if ($sortBy === 'outstanding' && in_array($sortBy, $columns, true)) {
return $query
->orderByRaw("GREATEST({$table}.total - COALESCE({$table}.amount_paid, 0), 0) {$direction}")
->orderByDesc("{$table}.id");
}
if (!in_array($sortBy, $columns, true)) {
return $query;
}
return $query->orderBy("{$table}.{$sortBy}", $direction)->orderByDesc("{$table}.id");
}
private function whereContractFileLike(Builder $query, string $attachmentsRelation, string $search): Builder
{
return $query->orWhereHas($attachmentsRelation, fn (Builder $files) => $files
->where('type', InvoiceAttachment::TYPE_CONTRACT)
->where('filename', 'like', "%{$search}%"));
}
private function whereOrderItemNameLike(Builder $query, string $orderItemsRelation, string $search): Builder
{
return $query->orWhereHas($orderItemsRelation, fn (Builder $items) => $items