Enterprise редакция
Billing — глубокий справочник
Эта страница — подробный справочник по поверхности биллинга NextPDF Enterprise. У поверхности два слоя. Модель биллинга в NextPDF\Enterprise\Billing определяет тарифные уровни, квоты, политики превышения и дедуплицированные оповещения об использовании. Субстрат принуждения в NextPDF\Enterprise\Billing\Substrate размещает эту модель на живом пути запроса — отказоустойчиво (fail-closed) и безопасно при конкурентности. Точки входа: PlanRegistry, QuotaManager, OverageCalculator, BillingAlertService и QuotaEnforcementGuard. Руководство уровня рабочего процесса см. на странице возможности «Биллинг».
Доступность и лицензирование
Заголовок раздела «Доступность и лицензирование»Эта возможность поставляется в NextPDF Enterprise (nextpdf/enterprise) и активируется лицензионным конвертом уровня Enterprise. Развёртывание без этого права не загружает классы возможности. Сравните редакции и получите лицензию.
Биллинг — это базовая возможность Enterprise без отдельного флага для каждой функции; она доступна, как только пакет Enterprise установлен рядом с пакетом Core. У NextPDF Core (Apache-2.0) и NextPDF Pro нет модели тарифов, квот или превышения; у этой поверхности нет эквивалента более низкого уровня. Включения тарифов, квоты и коммерческие условия регулируются лицензионным соглашением, а не принуждением во время выполнения; этот справочник не является юридическим или договорным заключением.
Публичная поверхность API
Заголовок раздела «Публичная поверхность API»Все символы находятся в NextPDF\Enterprise\Billing. Строки, помеченные substrate, находятся в NextPDF\Enterprise\Billing\Substrate. TenantContext — это тип аутентифицированного арендатора из NextPDF\Enterprise\SaaS.
| Символ | Параметры | Поведение по умолчанию | Возвращает | Бросает или завершается с | Примечания |
|---|---|---|---|---|---|
SaaSPlan (enum) | — | Строковые тарифные уровни: standard, advanced, high_control | — | Не бросает | label() возвращает отображаемое имя |
PlanDefinition::__construct | SaaSPlan $plan, float $includedCuQuota, list<CapabilityCode> $capabilities, non-empty-string $priceTier, bool $intelligencePackIncluded, bool $privacyPackIncluded | Неизменяемый объект-значение тарифа; хранит входные данные как есть | Новый экземпляр | Не бросает | final readonly; продвинутые публичные свойства |
PlanDefinition::includesCapability | CapabilityCode $capability | Проверка вхождения по строгой идентичности | bool | Не бросает | — |
PlanRegistry::__construct | list<PlanDefinition> $definitions | Индексирует определения по уровню; последнее определение для уровня побеждает | Новый реестр | Не бросает | Для тестов и white-label наборов тарифов |
PlanRegistry::get | SaaSPlan $plan | Канонический поиск тарифа | PlanDefinition | InvalidArgumentException, когда тариф не зарегистрирован | — |
PlanRegistry::has | SaaSPlan $plan | Проверка регистрации | bool | Не бросает | — |
PlanRegistry::defaultRegistry (static) | — | Продакшен-умолчания: Standard 1,000 CU; Advanced 5,000 CU плюс Intelligence Pack; High Control 20,000 CU плюс Intelligence и Privacy Pack | PlanRegistry | Не бросает | Используйте, если договорные условия не требуют пользовательских определений |
OveragePolicy (enum) | — | hard_stop, soft_stop, budget_alert | — | Не бросает | httpStatusCode() сопоставляет 402 / 429 / 200; isBlocking() истинно только для hard и soft stop |
QuotaManager::__construct | PlanRegistry $planRegistry, OveragePolicy $overagePolicy | Связывает реестр с одной политикой | Новый экземпляр | Не бросает | — |
QuotaManager::checkQuota | TenantContext $tenant, SaaSPlan $plan, float $currentCu | Молча возвращает при потреблении на уровне квоты или ниже, либо при неблокирующей политике | void | QuotaExceededException при строгом превышении под блокирующей политикой; InvalidArgumentException из реестра при незарегистрированном тарифе | resetsAt = первый день следующего месяца, полночь UTC |
QuotaManager::remainingQuota | SaaSPlan $plan, float $currentCu | Чистое чтение; никогда не блокирует | float | InvalidArgumentException реестра | Отрицательно при превышении |
QuotaManager::usagePercentage | SaaSPlan $plan, float $currentCu | Чистое чтение; никогда не блокирует | float | InvalidArgumentException реестра | 0.0, когда включённая квота неположительна; выше 1.0 при превышении |
OverageCalculator::calculate | PlanDefinition $plan, float $currentCu | Вычисляет неизменяемый снимок превышения | OverageResult | Не бросает | final readonly, без состояния |
OverageResult | includedCu, usedCu, overageCu, usageRatio, isOverage | Неизменяемый результат вычисления | — | Не бросает | overageCu = max(0, used - included); isOverage требует строгого превышения |
BillingAlertType (enum) | — | quota_warning_80, quota_warning_100, budget_exceeded, monthly_cap_reached | — | Не бросает | threshold() 0.8 / 1.0 / 1.0 / 1.0; severity() warning / critical / critical / critical |
BillingAlertService::__construct | AlertStateRepositoryInterface $alertState | Связывает хранилище дедупликации | Новый экземпляр | Не бросает | — |
BillingAlertService::evaluate | TenantContext $tenant, SaaSPlan $plan, PlanDefinition $planDef, float $currentCu | Срабатывают ещё не сработавшие оповещения в порядке возрастания порогов и записываются | list<BillingAlertType> | InvalidArgumentException при несоответствии тарифа/определения | Ключ дедупликации: арендатор, тип, период UTC YYYY-MM |
BillingAlertService::clearAlerts | TenantContext $tenant | Очищает состояние срабатывания арендатора за текущий период UTC | void | Ошибки, определённые репозиторием, распространяются | Перевзводит оповещения в том же периоде |
AlertStateRepositoryInterface | hasAlertFired(), markAlertFired(), clearForPeriod() | Контракт долговечного хранения дедупликации оповещений | По методу | Определяется реализацией | Оператор владеет долговечностью между репликами |
InMemoryAlertStateRepository | — | Состояние срабатывания на основе массива | По интерфейсу | Не бросает | Только для жизненных циклов одного запроса и тестов |
QuotaExceededException | Readonly currentCu, limitCu, resetsAt, tenantId, isSaaS | Отказ по квоте с учётом режима развёртывания | — | Является исключением | httpStatusCode() 402 SaaS / 403 on-prem; specCode() SPEC-BILLING-003 / SPEC-LIC-001; toErrorEnvelope() возвращает структурированное тело ошибки |
DeploymentMode (enum) | — | saas, self_hosted_oss, local_development | — | Не бросает | Substrate. enforcesQuota() истинно только для Saas; отказ от участия всегда явный |
QuotaEnforcementGuard::__construct | DeploymentMode, PlanResolverInterface, QuotaManager, UsageCounterStoreInterface | Собирает живой шлюз квот | Новый экземпляр | Не бросает | Substrate. final readonly |
QuotaEnforcementGuard::enforce | ?TenantContext $tenant, non-empty-string $featureKey, float $amount = 1.0 | Отказоустойчивый (fail-closed) шлюз квот с атомарным резервированием | QuotaDecision (только разрешённые исходы) | См. таксономию отказов ниже | Substrate. Монтируйте после аутентификации арендатора, перед оплачиваемым обработчиком |
PlanResolverInterface::resolve | TenantContext $tenant | Разрешает арендатора в его тариф и политики для каждой функции | ResolvedPlan | NoPlanForTenantException | Substrate. Резервный тариф по умолчанию для неизвестных арендаторов — это дефект |
RegistryPlanResolver | array<non-empty-string, ResolvedPlan> $plansByTenant | Резолвер на основе отображения | ResolvedPlan | NoPlanForTenantException для неотображённых арендаторов | Substrate. Отказоустойчивый по построению |
ResolvedPlan::policyFor | non-empty-string $featureKey | Поиск политики в разрешённом тарифе | ?QuotaPolicy | Не бросает | Substrate. null означает неизвестную функцию; шлюз отклоняет её |
QuotaPolicy | non-empty-string $featureKey, float $limit, OveragePolicy $overagePolicy | Лимит для каждой функции и политика нарушения | — | Не бросает | Substrate. UNLIMITED = -1.0; лимит 0.0 — это нулевое разрешение, а не безлимит; isUnlimited(), isBlocking() |
QuotaDecision | Статические bypassed(), unlimited(), consumed() | Объект-значение разрешённого исхода | QuotaDecision | Не бросает | Substrate. isAllowed() всегда истинно; каждый отказ вместо этого бросает исключение |
UsageCounter | Снимок строки: арендатор, функция, границы периода, used, limit, updatedAt | Неизменяемая строка использования | — | Не бросает | Substrate. remaining() может быть отрицательным; wouldExceed() строгий |
UsageCounterStoreInterface::get | Арендатор, функция, границы периода, float $limit | Читает строку использования, создавая её с used = 0, когда отсутствует | UsageCounter | UsageStoreUnavailableException | Substrate. Никогда не возвращает ложное значение при сбое бэкенда |
UsageCounterStoreInterface::tryConsume | Арендатор, функция, границы периода, float $amount, float $limit | Атомарное резервирование compare-and-set в пределах лимита | ?UsageCounter (null, когда резервирование нарушило бы лимит) | UsageStoreUnavailableException | Substrate. Должно быть единственной атомарной операцией над хранилищем |
InMemoryUsageCounterStore | — | Внутрипроцессная эталонная реализация контракта хранилища | По интерфейсу | По интерфейсу | Substrate. Только один процесс; документирует инвариант атомарности |
QuotaEnforcementException (abstract) | — | Базовый тип каждого отказа substrate | — | Является семейством исключений | Substrate. Каждый подтип объявляет httpStatusCode() |
public function checkQuota(TenantContext $tenant, SaaSPlan $plan, float $currentCu): voidpublic function evaluate( TenantContext $tenant, SaaSPlan $plan, PlanDefinition $planDef, float $currentCu,): arraypublic function enforce(?TenantContext $tenant, string $featureKey, float $amount = 1.0): QuotaDecisionpublic function tryConsume( string $tenantId, string $featureKey, DateTimeImmutable $periodStart, DateTimeImmutable $periodEnd, float $amount, float $limit,): ?UsageCounter;Таксономия отказов QuotaEnforcementGuard::enforce
| Исключение | HTTP-статус | Возникает, когда |
|---|---|---|
MissingTenantContextException | 401 | Режим SaaS без аутентифицированного контекста арендатора |
NoPlanForTenantException | 402 | Резолвер не находит тарифа, назначенного арендатору |
UnknownFeatureException | 402 | Разрешённый тариф не определяет политику для ключа функции |
UsageStoreUnavailableException | 503 | Хранилище использования нельзя прочитать или атомарно обновить; также возникает при неположительном $amount |
QuotaExceededException | 402 (SaaS) / 403 (on-prem) | Квота блокирующей политики превышена, или конкурентное резервирование израсходовало последний запас |
Контракт поведения
Заголовок раздела «Контракт поведения»- Реестр по умолчанию поставляет три уровня (Standard / Advanced / High Control) с растущими квотами CU и наборами возможностей. Запрос незарегистрированного тарифа завершается явным
InvalidArgumentException. QuotaManager::checkQuota()вызывает исключение только тогда, когда выполняются оба условия: политика блокирующая и текущее потребление строго выше включённой квоты. Политика budget-alert никогда не вызывает исключения; превышение сигнализируется через оповещения.remainingQuota()иusagePercentage()— это чистые чтения и никогда не блокируют. Остаток квоты уходит в минус при превышении; процент использования превышает1.0при превышении.- Оповещения вычисляются в порядке возрастания порогов: предупреждение 80%, предупреждение 100% (critical), затем budget-exceeded (critical). Budget-exceeded ограничено строгим превышением; потребление ровно на 100% запускает предупреждение 100%, а не budget-exceeded.
- Каждый тип оповещения срабатывает не более одного раза на арендатора за расчётный период. Состояние срабатывания записывается через
AlertStateRepositoryInterface, поэтому дедупликация настолько долговечна, насколько долговечна выбранная реализация. - Ключ дедупликации встраивает период UTC
YYYY-MM. Поэтому новый календарный месяц автоматически перевзводит каждый тип оповещения; для перевзвода при переходе периода не нужен вызов очистки.clearAlerts()очищает текущий период, что перевзводит оповещения в середине периода, например после апгрейда тарифа. - Защита от несоответствия тарифа в
evaluate()отклоняет вызов, где переданный тариф и определение тарифа расходятся, защищая от определения уровня, отличного от тарифа арендатора. - Вся арифметика периодов привязана к UTC. Момент сброса превышения квоты — первый день следующего календарного месяца в полночь UTC; ответ soft-stop должен указывать его как горизонт повтора.
QuotaEnforcementGuardотказоустойчив (fail-closed) в режиме SaaS. Отсутствующий арендатор, отсутствующий тариф, неизвестная функция, сбой хранилища и нарушение квоты — всё отклоняется; ничто не проваливается к неявному разрешению. Развёртывания вне SaaS отказываются только через конструирование шлюза с не-SaaSDeploymentMode.- Блокирующие политики резервируют использование через
UsageCounterStoreInterface::tryConsume— атомарный compare-and-set. Конкурентные запросы не могут совместно вытолкнуть использование за лимит; проигравший в гонке получаетQuotaExceededException, даже если предварительная проверка прошла. - При политике budget-alert шлюз записывает потребление по мере возможности (best-effort) и никогда не отклоняет; резервирование сверх мягкого потолка всё равно записывает строку по лимиту.
QuotaExceededExceptionучитывает режим развёртывания: отказы SaaS сопоставляются с HTTP 402 со спец-кодомSPEC-BILLING-003и помечаются как повторяемые; отказы on-prem сопоставляются с HTTP 403 сSPEC-LIC-001.- Библиотека сама не выдаёт HTTP-ответы. Объявленные коды статусов — это контракт для граничного слоя, который сопоставляет брошенный отказ с ответом и не должен вызывать оплачиваемый обработчик.
Граничные случаи и режимы отказа
Заголовок раздела «Граничные случаи и режимы отказа»- Неположительная включённая квота.
usagePercentage(),evaluate()иOverageCalculator::calculate()дают коэффициент использования0.0вместо деления на ноль. Пороговые оповещения тогда никогда не срабатывают только по коэффициенту. - Budget-alert плюс большое превышение. И менеджер, и шлюз возвращают разрешённые исходы. Не трактуйте отсутствие исключения как доказательство нахождения в пределах квоты; сверяйтесь с
OverageResultили потоком оповещений. - Ровно на лимите.
checkQuota()приcurrentCu == includedCuQuotaпроходит.BudgetExceededтребует строгого превышения.UsageCounter::wouldExceed()тоже строгий. MonthlyCapReached. Enum объявляет этот четвёртый тип оповещения, ноBillingAlertService::evaluate()никогда его не выдаёт; его список кандидатов покрывает только три пороговых оповещения. Он зарезервирован для эмиттеров отслеживания лимита вне этого модуля.- Дублирующиеся определения уровней.
PlanRegistryиндексирует по значению уровня; последнее определение для уровня молча заменяет более ранние. Стройте реестры из дедуплицированного списка. - Нулевое разрешение против безлимита. Лимит
QuotaPolicy, равный0.0, означает, что любое потребление в периоде является превышением. Только отрицательный маркерUNLIMITEDотключает учёт;isUnlimited()никогда не блокирует. - Неположительная величина резервирования.
enforce()отклоняет неположительный$amountотказоустойчиво сUsageStoreUnavailableException(503). Это дефект вызывающей стороны, а не сбой хранилища. - Сбой хранилища. Любой сбой чтения или резервирования проявляется как
UsageStoreUnavailableExceptionи отклоняет. Шлюз никогда не разрешает неучтённую работу, пока счётчик недоступен. - Реализации в памяти.
InMemoryAlertStateRepositoryиInMemoryUsageCounterStoreкорректны только в пределах одного процесса PHP. Развёртывания с несколькими репликами должны предоставлять реализации на основе хранилища с настоящей атомарностью; хранилище с чтением-затем-записью — это дефект, допускающий превышение квоты под нагрузкой. - Режим FIPS. Биллинг не выполняет собственных криптографических операций и не имеет специфичного для FIPS поведения. Личность арендатора, которую он потребляет, должна исходить из аутентифицированного контекста, чья позиция по FIPS задокументирована вместе с поверхностью SaaS.
Соответствие
Заголовок раздела «Соответствие»| Утверждение | Стандарт | Пункт |
|---|---|---|
| Код статуса 402 зарезервирован для будущего использования; сам по себе он не несёт нормативной семантики запроса. | RFC 9110 | §15.5.3 |
| 429 указывает, что клиент отправил слишком много запросов за отведённое время («ограничение частоты»). | RFC 6585 | §4 |
| Retry-After указывает, сколько пользовательский агент должен ждать перед повторным запросом. | RFC 9110 | §10.2.3 |
Все пункты изложены своими словами; NextPDF не воспроизводит нормативный текст. NextPDF не заявляет о соответствии HTTP-протоколу или сертификации для этой поверхности. Сопоставление 402 / 429 / 200, объявленное OveragePolicy::httpStatusCode(), и коды отказов шлюза 401 / 402 / 503 — это продуктовая конвенция, согласованная с приведёнными выше пунктами: RFC 9110 резервирует 402, поэтому его использование здесь для отказа по оплате — общепринятая отраслевая конвенция, а не семантика, определённая IETF. Горизонт повтора soft-stop (resetsAt) — это значение, которое граничный слой должен предоставлять в качестве рекомендации Retry-After. Выдача фактических HTTP-ответов, заголовков и поведения кэширования — это ответственность хостящего приложения.
Заметки по разработке
Заголовок раздела «Заметки по разработке»- Составьте модель из
PlanRegistry::defaultRegistry(), однойOveragePolicyиQuotaManager; добавьтеBillingAlertServiceс долговечной реализациейAlertStateRepositoryInterfaceдля оповещений. - Монтируйте
QuotaEnforcementGuardв конвейере запросов после аутентификации арендатора и перед оплачиваемым обработчиком. ЛовитеQuotaEnforcementExceptionи биллинговыйQuotaExceededExceptionна границе и сопоставляйтеhttpStatusCode()с ответом. - Определения тарифов в этом модуле — единственный источник истины для биллинга; не поддерживайте параллельное определение биллинга где-либо ещё в вашем развёртывании.
- Реализации в памяти делают всю поверхность юнит-тестируемой без ввода-вывода. Рекомендуемые граничные тесты: потребление ровно на квоте, на одну единицу выше, пороги коэффициента на 0.8 и 1.0, защита от несоответствия тарифа, гонка CAS (два резервирования против последней единицы запаса) и отказ при сбое хранилища.
- Классы основной модели несут
@since 2.2.0; субстрат несёт@since 2.3.0. Текущая линия пакета — 3.1.0. - Оператор владеет реализациями репозитория состояния оповещений и хранилища использования, их долговечностью между репликами и любым перевзводом оповещений в середине периода через
clearAlerts().
Граница публикации
Заголовок раздела «Граница публикации»Эта страница документирует только внешне наблюдаемое поведение и поддерживаемую публичную поверхность API. Внутренние пути пространств имён, вспомогательные классы, таблицы механизмов, имена файлов runbook и префиксы заявок находятся вне области охвата.