Source code for pypix_api.utils.identificadores

"""
pypix_api.utils.identificadores
-------------------------------

Geração de identificadores da API Pix que possuem regra de formação semântica.

O ``idRec`` do Pix Automático não é um identificador livre: além de ter 29
caracteres alfanuméricos, ele codifica no próprio valor a política de
retentativa, o ISPB do participante e a data de criação. Um valor aleatório
passa na validação de formato do schema (``pattern: [a-zA-Z0-9]{29}``) e mesmo
assim é recusado pelo PSP — daí a necessidade de um gerador.

Exemplo de uso:
    from pypix_api.models.enums import PoliticaRetentativa
    from pypix_api.utils.identificadores import gerar_id_rec

    id_rec = gerar_id_rec('12345678', PoliticaRetentativa.PERMITE_3R_7D)
    # 'RR12345678' + data + sequencial
"""

import secrets
import string
from datetime import date

from pypix_api.models.enums import PoliticaRetentativa

# Alfabeto do sequencial conforme o schema RecId: [a-z|A-Z|0-9]
_ALFABETO_SEQUENCIAL = string.ascii_letters + string.digits

# Tamanhos definidos pela regra de formação RAxxxxxxxxyyyyMMddkkkkkkkkkkk
_TAMANHO_ISPB = 8
_TAMANHO_SEQUENCIAL = 11
_TAMANHO_ID_REC = 29

# Segundo caractere do idRec: indica se a recorrência aceita novas tentativas
# de pagamento pós-vencimento. Precisa ser coerente com politicaRetentativa.
_PREFIXO_POR_POLITICA = {
    PoliticaRetentativa.PERMITE_3R_7D: 'RR',
    PoliticaRetentativa.NAO_PERMITE: 'RN',
}


[docs] def gerar_id_rec( ispb: str, politica_retentativa: PoliticaRetentativa | str, data_criacao: date | None = None, sequencial: str | None = None, ) -> str: """Gera um ``idRec`` válido para criação de recorrência (Pix Automático). Monta o identificador conforme a regra de formação do schema ``RecId``:: RAxxxxxxxxyyyyMMddkkkkkkkkkkk (29 caracteres, case sensitive) R fixo, recorrência criada dentro do Pix A 'R' permite retentativa pós-vencimento, 'N' não permite xxxxxxxx ISPB do participante (8 dígitos) yyyyMMdd data de criação da recorrência kkkkkkkkkkk sequencial alfanumérico (11 caracteres) O segundo caractere é derivado de ``politica_retentativa``, garantindo que o identificador e o campo ``politicaRetentativa`` do corpo da recorrência não fiquem contraditórios — uma inconsistência que o schema não detecta. Args: ispb: ISPB do participante direto/indireto ou os 8 primeiros dígitos do CNPJ do prestador de serviço de iniciação. 8 dígitos em ``[0-9]``. politica_retentativa: Política de retentativa da recorrência. Aceita um membro de :class:`~pypix_api.models.enums.PoliticaRetentativa` ou a string equivalente. Deve ser o mesmo valor enviado no corpo. data_criacao: Data de criação da recorrência. Padrão: data corrente. sequencial: Sequencial de 11 caracteres em ``[a-zA-Z0-9]``, único dentro da data de criação. Padrão: gerado aleatoriamente. Returns: str: identificador de 29 caracteres. Raises: ValueError: Se o ISPB não tiver 8 dígitos em ``[0-9]``, se a política de retentativa for desconhecida ou se o sequencial informado não tiver 11 caracteres em ``[a-zA-Z0-9]``. Nota: O sequencial gerado automaticamente é aleatório (62^11 combinações), o que torna a colisão dentro de um mesmo dia improvável, mas não impossível. Quando houver controle transacional do lado da aplicação, informe ``sequencial`` explicitamente. """ # Não usar `str.isdigit()`: ele aceita dígitos não-ASCII (fullwidth, árabe-índicos # etc.), que passariam aqui e produziriam um idRec fora de [a-zA-Z0-9]{29}. if len(ispb) != _TAMANHO_ISPB or not set(ispb).issubset(string.digits): raise ValueError( f'ispb deve ter {_TAMANHO_ISPB} dígitos em [0-9], recebido: {ispb!r}' ) try: politica = PoliticaRetentativa(politica_retentativa) except ValueError as exc: validos = ', '.join(membro.value for membro in PoliticaRetentativa) raise ValueError( f'politica_retentativa inválida: {politica_retentativa!r}. ' f'Valores aceitos: {validos}' ) from exc if sequencial is None: sequencial = ''.join( secrets.choice(_ALFABETO_SEQUENCIAL) for _ in range(_TAMANHO_SEQUENCIAL) ) elif len(sequencial) != _TAMANHO_SEQUENCIAL or not set(sequencial).issubset( _ALFABETO_SEQUENCIAL ): raise ValueError( f'sequencial deve ter {_TAMANHO_SEQUENCIAL} caracteres em [a-zA-Z0-9], ' f'recebido: {sequencial!r}' ) data = data_criacao or date.today() return f'{_PREFIXO_POR_POLITICA[politica]}{ispb}{data:%Y%m%d}{sequencial}'