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
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Support;
use App\Models\ActivityLog;
use App\Models\Service;
use Carbon\Carbon;
final class RenewalPeriodGuard
{
public static function violation(Service $service, int $months): ?string
{
$start = $service->expires_at && $service->expires_at->isFuture() ? $service->expires_at->copy() : now();
$end = $start->copy()->addMonths($months);
if ($end->lte(now())) {
return 'Không thể gia hạn cho khoảng thời gian trong quá khứ.';
}
$overlapping = ActivityLog::query()
->where('subject_type', Service::class)
->where('subject_id', $service->id)
->where('event', 'renewed')
->get(['old_values', 'new_values'])
->first(fn (ActivityLog $log) => self::overlaps($log, $start, $end));
if (! $overlapping) {
return null;
}
return sprintf(
'Khoảng thời gian %s – %s đã được gia hạn trước đó, không thể gia hạn trùng.',
$start->format('d/m/Y'),
$end->format('d/m/Y'),
);
}
private static function overlaps(ActivityLog $log, Carbon $start, Carbon $end): bool
{
$loggedEnd = $log->new_values['expires_at'] ?? null;
if (! $loggedEnd) {
return false;
}
$loggedEnd = Carbon::parse($loggedEnd);
$loggedStart = ($log->old_values['expires_at'] ?? null)
? Carbon::parse($log->old_values['expires_at'])
: $loggedEnd->copy()->subMonths((int) ($log->new_values['months'] ?? 1));
return $loggedStart->lt($end) && $loggedEnd->gt($start);
}
}