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:
co-authored by
Claude Sonnet 5.5
parent
ea9a53f742
commit
25f23d541e
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Enums\Product\ProductBillingCycle;
|
||||
use App\Models\Setting;
|
||||
|
||||
final class BillingCycleCatalog
|
||||
{
|
||||
public const SETTING_KEY = 'custom_billing_months';
|
||||
|
||||
public const CODE_PREFIX = 'months_';
|
||||
|
||||
public const MAX_MONTHS = 600;
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function builtInCodes(): array
|
||||
{
|
||||
return array_values(array_map(
|
||||
fn (ProductBillingCycle $cycle) => $cycle->value,
|
||||
array_filter(ProductBillingCycle::cases(), fn (ProductBillingCycle $cycle) => $cycle->months() > 0)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
public static function builtInMonths(): array
|
||||
{
|
||||
return array_values(array_filter(array_map(
|
||||
fn (ProductBillingCycle $cycle) => $cycle->months(),
|
||||
ProductBillingCycle::cases()
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
public static function customMonths(): array
|
||||
{
|
||||
$saved = json_decode((string) Setting::where('key', self::SETTING_KEY)->value('value'), true);
|
||||
|
||||
return self::normalizeMonths(is_array($saved) ? $saved : []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $months
|
||||
* @return list<int>
|
||||
*/
|
||||
public static function saveCustomMonths(array $months): array
|
||||
{
|
||||
$normalized = self::normalizeMonths($months);
|
||||
Setting::updateOrCreate(['key' => self::SETTING_KEY], ['value' => json_encode($normalized)]);
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function codes(): array
|
||||
{
|
||||
$codes = [...self::builtInCodes(), ...array_map(self::codeFor(...), self::customMonths())];
|
||||
usort($codes, fn (string $a, string $b) => self::monthsFor($a) <=> self::monthsFor($b));
|
||||
|
||||
return $codes;
|
||||
}
|
||||
|
||||
public static function codeFor(int $months): string
|
||||
{
|
||||
return self::CODE_PREFIX . $months;
|
||||
}
|
||||
|
||||
public static function monthsFor(?string $code): ?int
|
||||
{
|
||||
if ($code === null || $code === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$builtIn = ProductBillingCycle::tryFrom($code);
|
||||
if ($builtIn) {
|
||||
return $builtIn->months();
|
||||
}
|
||||
|
||||
return preg_match('/^' . self::CODE_PREFIX . '(\d{1,3})$/', $code, $m) && (int) $m[1] > 0
|
||||
? (int) $m[1]
|
||||
: null;
|
||||
}
|
||||
|
||||
public static function labelForMonths(int $months): string
|
||||
{
|
||||
return $months > 0 && $months % 12 === 0 ? ($months / 12) . ' năm' : $months . ' tháng';
|
||||
}
|
||||
|
||||
public static function labelForCode(?string $code): ?string
|
||||
{
|
||||
$months = self::monthsFor($code);
|
||||
|
||||
return $months ? self::labelForMonths($months) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $months
|
||||
* @return list<int>
|
||||
*/
|
||||
private static function normalizeMonths(array $months): array
|
||||
{
|
||||
$valid = array_filter(
|
||||
array_map('intval', $months),
|
||||
fn (int $m) => $m >= 1 && $m <= self::MAX_MONTHS && !in_array($m, self::builtInMonths(), true)
|
||||
);
|
||||
|
||||
$unique = array_values(array_unique($valid));
|
||||
sort($unique);
|
||||
|
||||
return $unique;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user