Enterprise edição
Billing — Referência Profunda
Visão geral
Seção intitulada “Visão geral”Esta página é a referência detalhada da superfície de billing do NextPDF Enterprise. A superfície tem duas camadas. O modelo de billing em NextPDF\Enterprise\Billing define níveis de plano, cotas, políticas de excedente e alertas de uso deduplicados. O substrate de imposição em NextPDF\Enterprise\Billing\Substrate coloca esse modelo no caminho de requisição ativo, fail-closed e seguro para concorrência. Os pontos de entrada são PlanRegistry, QuotaManager, OverageCalculator, BillingAlertService e QuotaEnforcementGuard. Para o guia em nível de fluxo de trabalho, consulte a página de capacidade de Billing.
Disponibilidade e licenciamento
Seção intitulada “Disponibilidade e licenciamento”Esta capacidade é entregue no NextPDF Enterprise (nextpdf/enterprise) e é ativada com um envelope de licença de nível Enterprise. Uma implantação sem esse direito não carrega as classes da capacidade. Compare edições e obtenha uma licença.
O Billing é uma capacidade base do Enterprise, sem uma flag por recurso separada; ele fica disponível assim que o pacote Enterprise é instalado ao lado do pacote Core. O NextPDF Core (Apache-2.0) e o NextPDF Pro não têm modelo de plano, cota ou excedente; esta superfície não tem equivalente em nível inferior. As inclusões de plano, as cotas e os termos comerciais são regidos pelo contrato de licença, não pela imposição em tempo de execução; esta referência não é uma opinião jurídica ou contratual.
Superfície pública da API
Seção intitulada “Superfície pública da API”Todos os símbolos residem em NextPDF\Enterprise\Billing. As linhas marcadas com substrate residem em NextPDF\Enterprise\Billing\Substrate. TenantContext é o tipo de tenant autenticado de NextPDF\Enterprise\SaaS.
| Símbolo | Parâmetros | Comportamento padrão | Retorna | Lança ou falha com | Notas |
|---|---|---|---|---|---|
SaaSPlan (enum) | — | Níveis de plano com backing de string: standard, advanced, high_control | — | Não lança | label() retorna o nome de exibição |
PlanDefinition::__construct | SaaSPlan $plan, float $includedCuQuota, list<CapabilityCode> $capabilities, non-empty-string $priceTier, bool $intelligencePackIncluded, bool $privacyPackIncluded | Objeto de valor de plano imutável; armazena as entradas como fornecidas | Nova instância | Não lança | final readonly; propriedades públicas promovidas |
PlanDefinition::includesCapability | CapabilityCode $capability | Verificação de pertencimento por identidade estrita | bool | Não lança | — |
PlanRegistry::__construct | list<PlanDefinition> $definitions | Indexa definições por nível; a última definição por nível prevalece | Novo registro | Não lança | Para testes e conjuntos de planos white-label |
PlanRegistry::get | SaaSPlan $plan | Consulta canônica de plano | PlanDefinition | InvalidArgumentException quando o plano não está registrado | — |
PlanRegistry::has | SaaSPlan $plan | Sondagem de registro | bool | Não lança | — |
PlanRegistry::defaultRegistry (static) | — | Padrões de produção: Standard 1,000 CU; Advanced 5,000 CU mais Intelligence Pack; High Control 20,000 CU mais Intelligence e Privacy Packs | PlanRegistry | Não lança | Use a menos que os termos contratuais exijam definições personalizadas |
OveragePolicy (enum) | — | hard_stop, soft_stop, budget_alert | — | Não lança | httpStatusCode() mapeia 402 / 429 / 200; isBlocking() é true apenas para hard stop e soft stop |
QuotaManager::__construct | PlanRegistry $planRegistry, OveragePolicy $overagePolicy | Vincula o registro a uma política | Nova instância | Não lança | — |
QuotaManager::checkQuota | TenantContext $tenant, SaaSPlan $plan, float $currentCu | Retorna silenciosamente com uso igual ou abaixo da cota, ou sob uma política de não bloqueio | void | QuotaExceededException em excedente estrito sob uma política de bloqueio; InvalidArgumentException do registro em um plano não registrado | resetsAt = primeiro dia do mês seguinte, meia-noite UTC |
QuotaManager::remainingQuota | SaaSPlan $plan, float $currentCu | Leitura pura; nunca bloqueia | float | InvalidArgumentException do registro | Negativa em excedente |
QuotaManager::usagePercentage | SaaSPlan $plan, float $currentCu | Leitura pura; nunca bloqueia | float | InvalidArgumentException do registro | 0.0 quando a cota incluída é não positiva; acima de 1.0 em excedente |
OverageCalculator::calculate | PlanDefinition $plan, float $currentCu | Calcula um snapshot imutável de excedente | OverageResult | Não lança | final readonly, sem estado |
OverageResult | includedCu, usedCu, overageCu, usageRatio, isOverage | Resultado de cálculo imutável | — | Não lança | overageCu = max(0, used - included); isOverage requer excedente estrito |
BillingAlertType (enum) | — | quota_warning_80, quota_warning_100, budget_exceeded, monthly_cap_reached | — | Não lança | threshold() 0.8 / 1.0 / 1.0 / 1.0; severity() warning / critical / critical / critical |
BillingAlertService::__construct | AlertStateRepositoryInterface $alertState | Vincula o armazenamento de deduplicação | Nova instância | Não lança | — |
BillingAlertService::evaluate | TenantContext $tenant, SaaSPlan $plan, PlanDefinition $planDef, float $currentCu | Dispara alertas ainda não disparados em ordem crescente de limiar e os registra | list<BillingAlertType> | InvalidArgumentException em divergência de plano/definição | Chave de dedup: tenant, tipo, período UTC YYYY-MM |
BillingAlertService::clearAlerts | TenantContext $tenant | Limpa o estado de disparo do tenant para o período UTC atual | void | Falhas definidas pelo repositório se propagam | Rearma os alertas dentro do mesmo período |
AlertStateRepositoryInterface | hasAlertFired(), markAlertFired(), clearForPeriod() | Contrato de persistência durável de deduplicação de alertas | Por método | Definido pela implementação | O operador é dono da durabilidade entre réplicas |
InMemoryAlertStateRepository | — | Estado de disparo com backing de array | Conforme a interface | Não lança | Apenas ciclos de vida de requisição única e testes |
QuotaExceededException | Somente leitura: currentCu, limitCu, resetsAt, tenantId, isSaaS | Negação de cota ciente do modo de implantação | — | É o throwable | httpStatusCode() 402 SaaS / 403 on-prem; specCode() SPEC-BILLING-003 / SPEC-LIC-001; toErrorEnvelope() produz um corpo de erro estruturado |
DeploymentMode (enum) | — | saas, self_hosted_oss, local_development | — | Não lança | Substrate. enforcesQuota() é true apenas para Saas; o opt-out é sempre explícito |
QuotaEnforcementGuard::__construct | DeploymentMode, PlanResolverInterface, QuotaManager, UsageCounterStoreInterface | Monta o portão de cota ativo | Nova instância | Não lança | Substrate. final readonly |
QuotaEnforcementGuard::enforce | ?TenantContext $tenant, non-empty-string $featureKey, float $amount = 1.0 | Portão de cota fail-closed com reserva atômica | QuotaDecision (apenas resultados permitidos) | Consulte a taxonomia de negação abaixo | Substrate. Monte após a autenticação do tenant, antes do handler faturável |
PlanResolverInterface::resolve | TenantContext $tenant | Resolve um tenant para seu plano e políticas por recurso | ResolvedPlan | NoPlanForTenantException | Substrate. Um fallback de plano padrão para tenants desconhecidos é um defeito |
RegistryPlanResolver | array<non-empty-string, ResolvedPlan> $plansByTenant | Resolvedor com backing de mapa | ResolvedPlan | NoPlanForTenantException para tenants não mapeados | Substrate. Fail-closed por construção |
ResolvedPlan::policyFor | non-empty-string $featureKey | Consulta de política no plano resolvido | ?QuotaPolicy | Não lança | Substrate. null significa recurso desconhecido; o guarda o nega |
QuotaPolicy | non-empty-string $featureKey, float $limit, OveragePolicy $overagePolicy | Limite por recurso e política de violação | — | Não lança | Substrate. UNLIMITED = -1.0; um limite 0.0 é permissão zero, não ilimitado; isUnlimited(), isBlocking() |
QuotaDecision | Estáticos bypassed(), unlimited(), consumed() | Objeto de valor de resultado permitido | QuotaDecision | Não lança | Substrate. isAllowed() é sempre true; toda negação lança em vez disso |
UsageCounter | Snapshot de linha: tenant, recurso, limites de período, used, limit, updatedAt | Linha de uso imutável | — | Não lança | Substrate. remaining() pode ser negativo; wouldExceed() é estrito |
UsageCounterStoreInterface::get | Tenant, recurso, limites de período, float $limit | Lê a linha de uso, criando-a com used = 0 quando ausente | UsageCounter | UsageStoreUnavailableException | Substrate. Nunca retorna um valor falsy em falha de backend |
UsageCounterStoreInterface::tryConsume | Tenant, recurso, limites de período, float $amount, float $limit | Reserva atômica de compare-and-set dentro do limite | ?UsageCounter (null quando a reserva violaria o limite) | UsageStoreUnavailableException | Substrate. Deve ser uma única operação atômica contra o armazenamento subjacente |
InMemoryUsageCounterStore | — | Implementação de referência em processo do contrato de armazenamento | Conforme a interface | Conforme a interface | Substrate. Apenas processo único; documenta o invariante de atomicidade |
QuotaEnforcementException (abstract) | — | Tipo base de toda negação do substrate | — | É a família de throwables | Substrate. Cada subtipo declara 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;Taxonomia de negação de QuotaEnforcementGuard::enforce
| Exceção | Status HTTP | Levantada quando |
|---|---|---|
MissingTenantContextException | 401 | Modo SaaS sem contexto de tenant autenticado |
NoPlanForTenantException | 402 | O resolvedor não encontra plano atribuído ao tenant |
UnknownFeatureException | 402 | O plano resolvido não define política para a chave de recurso |
UsageStoreUnavailableException | 503 | O armazenamento de uso não pode ser lido ou atualizado atomicamente; também levantada para um $amount não positivo |
QuotaExceededException | 402 (SaaS) / 403 (on-prem) | A cota de uma política de bloqueio é excedida, ou uma reserva concorrente consumiu a última folga |
Contrato de comportamento
Seção intitulada “Contrato de comportamento”- O registro padrão traz três níveis (Standard / Advanced / High Control) com cotas de CU e conjuntos de capacidades crescentes. Uma solicitação de plano não registrado falha com um
InvalidArgumentExceptionexplícito. QuotaManager::checkQuota()levanta apenas quando ambas as condições se sustentam: a política é de bloqueio e o uso atual está estritamente acima da cota incluída. Uma política de alerta de orçamento nunca levanta; o excedente é sinalizado por meio de alertas.remainingQuota()eusagePercentage()são leituras puras e nunca bloqueiam. A cota restante fica negativa em excedente; o percentual de uso ultrapassa1.0em excedente.- Os alertas são avaliados em ordem crescente de limiar: aviso de 80%, aviso de 100% (critical), depois orçamento-excedido (critical). O orçamento-excedido é condicionado a excedente estrito; um uso de exatamente 100% dispara o aviso de 100%, não o orçamento-excedido.
- Cada tipo de alerta dispara no máximo uma vez por tenant por período de billing. O estado de disparo é registrado por meio de
AlertStateRepositoryInterface, de modo que a deduplicação é tão durável quanto a implementação escolhida. - A chave de deduplicação embute o período UTC
YYYY-MM. Um novo mês civil, portanto, rearma automaticamente cada tipo de alerta; nenhuma chamada de limpeza é necessária para o rearmamento na virada.clearAlerts()limpa o período atual, o que rearma os alertas no meio do período, por exemplo após um upgrade de plano. - Um guarda de divergência de plano em
evaluate()rejeita uma chamada em que o plano fornecido e a definição de plano divergem, protegendo contra uma definição de um nível diferente do plano do tenant. - Toda a aritmética de período é ancorada em UTC. O instante de reset de cota excedida é o primeiro dia do mês civil seguinte à meia-noite UTC; uma resposta de soft-stop deve anunciá-lo como o horizonte de nova tentativa.
QuotaEnforcementGuardé fail-closed no modo SaaS. Tenant ausente, plano ausente, recurso desconhecido, indisponibilidade do armazenamento e violação de cota — todos negam; nada escapa para uma permissão implícita. Implantações não SaaS só fazem opt-out construindo o guarda com umDeploymentModenão SaaS.- Políticas de bloqueio reservam uso por meio de
UsageCounterStoreInterface::tryConsume, um compare-and-set atômico. Requisições concorrentes não podem, coletivamente, empurrar o uso além do limite; o perdedor da corrida recebeQuotaExceededExceptionmesmo que a pré-verificação tenha passado. - Sob uma política de alerta de orçamento, o guarda registra o consumo em best-effort e nunca nega; uma reserva além do teto flexível ainda registra a linha no limite.
QuotaExceededExceptioné ciente do modo de implantação: negações em SaaS mapeiam para HTTP 402 com o spec codeSPEC-BILLING-003e são marcadas como retryable; negações on-prem mapeiam para HTTP 403 comSPEC-LIC-001.- A biblioteca não emite respostas HTTP por conta própria. Os códigos de status declarados são o contrato para a camada de borda, que mapeia uma negação lançada para uma resposta e não deve invocar o handler faturável.
Casos extremos e modos de falha
Seção intitulada “Casos extremos e modos de falha”- Cota incluída não positiva.
usagePercentage(),evaluate()eOverageCalculator::calculate()produzem uma razão de uso de0.0em vez de dividir por zero. Os alertas de limiar então nunca disparam apenas com base na razão. - Alerta de orçamento mais excedente grande. O gerenciador e o guarda retornam resultados permitidos. Não trate a ausência de uma exceção como prova de estar dentro da cota; consulte
OverageResultou o fluxo de alertas. - Exatamente no limite.
checkQuota()comcurrentCu == includedCuQuotapassa.BudgetExceededrequer excedente estrito.UsageCounter::wouldExceed()também é estrito. MonthlyCapReached. O enum declara este quarto tipo de alerta, masBillingAlertService::evaluate()nunca o emite; sua lista de candidatos cobre apenas os três alertas de limiar. Ele é reservado para emissores de rastreamento de cap fora deste módulo.- Definições de nível duplicadas.
PlanRegistryindexa pelo valor do nível; a última definição de um nível substitui silenciosamente as anteriores. Construa registros a partir de uma lista deduplicada. - Permissão zero versus ilimitado. Um limite
0.0emQuotaPolicysignifica que todo consumo no período é excedente. Apenas o sentinela negativoUNLIMITEDdesativa a medição;isUnlimited()nunca bloqueia. - Valor de reserva não positivo.
enforce()nega um$amountnão positivo de forma fail-closed comUsageStoreUnavailableException(503). Isso é um defeito do chamador, não uma indisponibilidade do armazenamento. - Indisponibilidade do armazenamento. Qualquer falha de leitura ou de reserva aparece como
UsageStoreUnavailableExceptione nega. O guarda nunca permite trabalho sem medição enquanto o medidor está fora do ar. - Implementações em memória.
InMemoryAlertStateRepositoryeInMemoryUsageCounterStoresão corretas apenas dentro de um único processo PHP. Implantações com múltiplas réplicas devem fornecer implementações apoiadas por um datastore com atomicidade real; um armazenamento de ler-depois-escrever é um defeito que permite ultrapassar a cota sob carga. - Modo FIPS. O Billing não realiza operações criptográficas próprias e não tem comportamento específico de FIPS. A identidade de tenant que ele consome deve originar-se de um contexto autenticado cuja postura de FIPS é documentada com a superfície de SaaS.
Conformidade
Seção intitulada “Conformidade”| Afirmação | Padrão | Cláusula |
|---|---|---|
| O código de status 402 é reservado para uso futuro; ele não carrega semântica de requisição normativa própria. | RFC 9110 | §15.5.3 |
| 429 indica que o cliente enviou requisições demais em um dado período de tempo (“rate limiting”). | RFC 6585 | §4 |
| Retry-After indica quanto tempo o user agent deve esperar antes de fazer uma requisição de acompanhamento. | RFC 9110 | §10.2.3 |
Todas as cláusulas são parafraseadas; o NextPDF não reproduz texto normativo. O NextPDF não faz nenhuma afirmação de conformidade ou certificação de protocolo HTTP para esta superfície. O mapeamento 402 / 429 / 200 declarado por OveragePolicy::httpStatusCode() e os códigos de negação 401 / 402 / 503 do guarda são uma convenção de produto alinhada com as cláusulas acima: a RFC 9110 reserva o 402, portanto seu uso aqui como negação de pagamento é a convenção comum da indústria, não uma semântica definida pelo IETF. O horizonte de nova tentativa do soft-stop (resetsAt) é o valor que uma camada de borda deve expor como orientação de Retry-After. Emitir respostas HTTP reais, cabeçalhos e comportamento de cache é responsabilidade da aplicação hospedeira.
Notas de desenvolvimento
Seção intitulada “Notas de desenvolvimento”- Componha o modelo a partir de
PlanRegistry::defaultRegistry(), umaOveragePolicye umQuotaManager; adicioneBillingAlertServicecom uma implementação durável deAlertStateRepositoryInterfacepara os alertas. - Monte
QuotaEnforcementGuardno pipeline de requisição após a autenticação do tenant e antes do handler faturável. CaptureQuotaEnforcementExceptione oQuotaExceededExceptiondo billing na borda e mapeiehttpStatusCode()para a resposta. - As definições de plano neste módulo são a única fonte de verdade para o billing; não mantenha uma definição de billing paralela em outro lugar da sua implantação.
- As implementações em memória tornam toda a superfície testável em unidade sem I/O. Testes de fronteira recomendados: uso exatamente na cota, uma unidade acima, limiares de razão em 0.8 e 1.0, o guarda de divergência de plano, a corrida de CAS (duas reservas contra a última unidade de folga) e a negação por indisponibilidade do armazenamento.
- As classes do modelo core carregam
@since 2.2.0; o substrate carrega@since 2.3.0. A linha de pacote atual é 3.1.0. - O operador é dono das implementações do repositório de estado de alerta e do armazenamento de uso, da sua durabilidade entre réplicas e de qualquer rearmamento de alerta no meio do período por meio de
clearAlerts().
Limite de publicação
Seção intitulada “Limite de publicação”Esta página documenta apenas o comportamento observável externamente e a superfície pública de API suportada. Caminhos de namespace internos, classes auxiliares, tabelas de mecanismos, nomes de arquivos de runbook e prefixos de tickets estão fora do escopo.