Changelog
O que há de novo.
Cada melhoria para a plataforma, como ele envia.
· kamo-internal
Um grupo em que se pode colocar as pessoas e pagar
Criar um grupo produziu um nome e nada mais — nenhuma maneira de adicionar ninguém, nenhuma maneira para o seu gerente escolher um plano, sem lugar para pagar. Abrir um grupo agora mostra quem paga, quem está nele, que ainda está à espera de um lugar, e o plano controla que Comprem esses lugares. A criação de um pergunta quem paga, usando o seletor de membros canônicos. Costumava enviar a conta da organização uid, que é exatamente por isso que cada grupo foi a organização; o pagador está agora resolvido servidor- lado do gestor e A contaUid já não está na escuta. A lista distingue ter um assento de esperar por um. Um gerente que coloca oito pessoas em cinco lugares é dito que três colegas não têm acesso em vez de Descobrir por eles.
· BillingService
Fazer os grupos de faturamento trabalhar fim a fim, e parar de cobrar auto-pagamento por nada
Um grupo agora pode reunir pessoas, comprar um plano no cartão de seu próprio gerente, e mão Os seus lugares fora. Nada disso existia: as únicas operações foram criadas, aprovar e deletar, então um proprietário que fez um grupo tem uma etiqueta e nenhuma maneira de usá-lo. BillingGroupSeatService é onde o dinheiro se move e o direito é emitido, mantido Para além da manutenção de registos, não se pode adicionar nenhuma nova rota que salte os portões. subscribe() recusa antes de create() é chamado ao invés de depois, porque create() abre uma assinatura live Stripe — um grupo não aprovado, um legado, um gerente sem cartão ou uma quebra de tampa de assento param por aí. Dois defeitos fizeram do grupo a pessoa errada: - O pagador era qualquer contaUd o corpo pedido chamado, sem controlo, e o O console enviou o da organização. Um grupo facturado à organização não é um grupo de faturamento; é um segundo nome para a própria assinatura da organização. O pagador é agora derivado do gerente e os dois são afirmados para concordar Antes que algo seja acusado. Enviar contaUid é rejeitado. - garantir que a ContaForMember reutilize o link mais antigo do membro, que para Qualquer um que tenha sido adicionado à conta da organização é a conta da organização. Assegura que aContaPessoalForMember identifica pelo membro primário, que é O único campo que diz que uma conta pertence a uma pessoa. Onde um proprietário gerencia seu próprio grupo, "seu cartão" e "a organização cartão" são genuinamente a mesma linha de conta e o mesmo cliente Stripe, e não O código pode diferenciá-los. Isso é recusado, a menos que confirmado, em vez de papel Escuto. Separadamente, dois bugs vivos encontrados aqui: - /self/seat carregou o membro e nunca emitiu-lhes um assento. Chama-se create() e retornou; não havia nenhuma linha de assinaturaMembro e nenhuma ContaLicense, então o comprador ficou sem licença enquanto seu cartão era cobrado todos os meses. - @Transactional em AccountBillingService tinha se desviado para um método privado, então Spring ignorou-o e garantir que oAccountForMember funcionasse sem nenhuma transação. A falha entre os seus dois salva órfão uma Conta a próxima pesquisa não pôde E a chamada seguinte criou uma segunda. A auto-cobertura é nova: responde "quem paga o meu lugar", o que nada fez. É também o que impede alguém já coberto ser oferecido um segundo lugar em O seu próprio cartão.
· InitializerService
Esquema para grupos que podem realmente pagar
Três corredores: BillingGroupRosterMigration cria billing pay group members — a quem um grupo significa pagar, antes de ter comprado alguma coisa. org id está desnormalizado para ele para que o banco de dados possa impor "um membro está no máximo em um grupo de faturamento ativo por organização" como um único parcial. Dois grupos que pagam por uma pessoa são dois taxas por um assento, e nada mais no esquema iria pegá-lo: AssinaturaOs únicos membros são por assinatura e a AccountLicense não tem nenhum. ************* adiciona billing group uid a account subscriptions e substitui a única de três colunas por duas únicas parciais. Alargando quatro colunas não funcionariam — os NULLs são distintos dentro de um índice único aqui, então todas as fileiras não-grupo deixariam de ser restringidas. BillingPayGroupColumnsMigration adiciona market id, sed limit e status, e marcas grupos existentes LEGACY. Eles foram cobrados para qualquer conta uid o pedido o corpo transportado, e o console enviou o da organização, então seu account uid é Quase certamente errado. Não há maneira segura de adivinhar qual cartão foi significado, e adivinhando contas erradas a pessoa errada, por isso eles são recusados no ponto de carga em vez de reparar. A primeira execução mostrou por que o retry importa: ambos os índices de substituição falharam com 40001 não conseguiu serializar o acesso, o corredor registrou "skipped" e continuou, e o DROP da velha restrição foi então bem sucedido — deixando account subscriptions sem nenhuma singularidade, o que é estritamente pior do que a colisão que isso corrige. As instruções DDL agora repetem a falha de serialização, e a queda é ignorada A menos que ambas as substituições sejam confirmadas.
· kamo-shared-library
Deixe um grupo de faturamento ser seu próprio pagador
Um grupo poderia ser nomeado e nada mais. A sua assinatura nunca foi estabelecida — nada na plataforma chamada setSubscription — então subscription uid ficou null para sempre e o grupo não poderia comprar ou segurar um único assento. Três coisas tiveram que mudar no modelo: BillingGroupMember é novo. A verdade do assento fica onde estava, na assinaturaMembro e ContaLicença, mas AssinaturaMembro.assinatura não é NULL, então até que um grupo compra um plano não há nenhum lugar para registrar as pessoas que existe para cobrir — E reunir pessoas antes de comprar assentos para eles é o ponto. Isto é... intenção; isto é direito. ContaAssunto ganha faturamentoGroupUid, e o (conta, mercado, organização alvo) A restrição única sai da entidade. Um grupo cobra seu gerente, sobre o conta própria do gestor, visando a mesma org no mesmo mercado que qualquer banco esse gerente já comprou para si mesmos — a mesma tupla. Então um grupo é o primeiro compra não falhou, encontrou assinatura pessoal do gerente e reescreveu é: plano, assentos, ID de listras. Sem excepção, sem segunda fila. A regra agora vive como dois índices parciais únicos que a JPA não consegue expressar, criados pelo KamoInitializer; a declaração da entidade teve que ir ou ddl-auto iria recriar a restrição antiga à frente do corredor que a deixa cair. ******************* é eliminado em vez de Depreciada. Ele retorna opcional sobre a força de uma restrição que já não existe, então uma vez que uma conta detém tanto uma assinatura pessoal quanto uma do grupo corresponde a duas filas e lançamentos em todos os cinco locais de chamadas em três serviços. Deixar para ser encontrado por autocompletar foi a opção mais perigosa. Além disso: allowsGroups() agora inclui REQUIRES APPROVAL. Com exclusão da o caminho de aprovação inalcançável — a criação de um grupo foi configuração de código a bandeira aprovada pediu requiresAproval() e assim só poderia sempre resposta "já aprovada". Um dono que escolheu a supervisão não conseguiu grupos.
· kamo-internal
Editor de cursos de RH, grade de atribuições e jogador de cursos membro
O /hr/training substitui o seu substituto pelas duas sub-tabs solicitadas e pelo membro A grade de treinamento atribuída ganha a coluna de ação que estava segurando por falta de um jogador. Cursos. Uma grade de catálogo com publicar, atribuir, duplicar, arquivar e excluir em linha. Apagar é desabilitado uma vez que alguém tenha uma atribuição e os pontos de dica no Archive em vez disso — excluir um é claro que alguém foi atribuído órfãos suas provas, uma vez que a atribuição Os pais teriam desaparecido. "Nada mudou desde a versão N" é mostrado como INFO em vez de um erro: o autor pressionou Publicar em algo já publicado, que é um sucesso que eles interpretaram mal. Editor. Esboço · canvas · inspetor como uma rota aninhada de página inteira em vez de um diálogo — um slide precisa de uma tela ao lado de um esboço e de um inspetor, autoria é trabalho multi-sessão que tem que sobreviver a uma atualização, e hrTabMatches foi escrita para que uma rota aninhada mantenha a aba pai acesa e MANAGEM TRAINING-gated with no new registry. As poupanças são por ITEM e deduzidas; um PUT de curso inteiro com a semântica replace-set, dois administradores editam duas unidades diferentes em uma o seu trabalho. Depois de cada flush o curso é re-ler, porque saveItemContent recria fileiras de crianças e uma cópia local antiga enviaria ids fantasmas na próxima gravação. Reordenar são botões de seta conduzidos por moveInOrder, que CLAMPS em vez de embrulhar — embrulhando silenciosamente reordena um curso que o autor fez não significa reordenar, e a ordem é a ordem de leitura. MEDIA vem de /hr/resources através de uma nova BibliotecaPickerDialog, generalizada de MaterialPickerDialog elevando suas duas constantes codificadas em adereços. assocType é o ORDINAL (13), nunca o enum id (15): o enum skips id 6 e enviar um id pousa em um diferente regras da coleção. Alt texto é necessário em qualquer slide de mídia e blocos publicar — a ferramenta de criação é o único ponto de execução que realmente detém, porque uma revisão checklist é executado uma vez e alt texto nunca é adicionado. Jogador. Continua no primeiro GAP, nunca o ponto mais distante alcançado, e cada portão uniu o registro do servidor com esta sessão — ler apenas a linha do servidor torna o próximo inacessível o momento um tique-taque não foi circular, que é o erro exato que o assistente legal enviou. A conclusão do vídeo é medida como COBERTURA ASSISTIDA, não atualTime. /api/imaging/stream homenageia HTTP Intervalo de fim a fim, para que um aluno possa procurar o último segundo em uma solicitação e qualquer posição baseada gate é derrotado por um único arrastar; acumulation() trata um salto maior do que o intervalo de tick como uma procura e abre um novo intervalo em vez de creditar o span saltou sobre. videoCoverage.test.ts afirma "seek para o fim produz menos de 5%" como seu primeiro caso. Cobertura é tempo de mídia, então a reprodução de 2x ainda tem que Cobrir a linha do tempo — razão pela qual permitir que seja justa em vez de uma lacuna. Uma escotilha de escape visível aparece uma vez que o comprimento total do vídeo tenha decorrido em tempo de relógio de parede e registros skipReason no progress row, porque um portão sem fuga torna-se um bilhete de apoio cuja resposta é sempre "nós vamos marque-o completo para você" — o mesmo resultado, não rastreado. O corredor de teste não carrega dados de correção: a carga útil é construída a partir do PlayerQuestion Dto, que não tem tal campo, e o servidor decide. Respostas salvar automaticamente para que um laptop fechado não é um perdido Tentativa, e espaços em branco são permitidos, mas deve ser confirmado. teste npm: 3 guardas limpos, 102 arquivos, 1257 testes. compilação seguinte compila.
· DaemonService
O tique de lembrete de formação horária
HOURLY às :10, ao contrário da varredura legal do diário 13:00. O treinamento lembra cada org em seu próprio envio local hora, e o timezone da Organização abrange 21 horas (Pacífico/Honolulu para Austrália/Sydney) — um único diariamente UTC tick significa "hoje" é errado para a maioria da plataforma, que é exatamente por isso que a varredura legal diz a um inquilino de Sydney que "deve hoje" no dia errado. Cada carrapato visita apenas as orgs cuja hora local combina, tipicamente de um a três, assim que este escaneia MENOS do que um trabalho de cross-org diário em vez de 24x mais. O trabalho vive aqui em vez de como @Scheduled in DocsService porque esta implantação é réplicas: 1 com estratégia: Recriar, então o pod antigo se foi antes do novo começar e uma janela de implantação não pode Fogo duplo. DocsService é RollingUpdate com maxSurge: 1, para o comprimento de cada lançamento lá são dois pods e ambos acreditam que é 09:00 — e para um trabalho cuja saída é mail para cada atraso empregado na plataforma, "normalmente uma vez" não é uma propriedade. Tempos finitos explícitos, porque um RestTemplate padrão espera para sempre e, combinado com @DisallowConcurrentExecution, um encravado a jusante para o trabalho em execução novamente até o pod reinicia — silenciosamente, e indistintamente de "nada era devido". Nunca volta a lançar em Quartzo: A exceção de fuga pode voltar a disparar imediatamente, transformando uma hora passada numa tempestade de repetição.
· DocsService
A infraestrutura completa de treinamento de RH
Curso de autoria, publicação, atribuição, o jogador membro, classificação do lado do servidor, provas WORM e a varredura do lembrete — todo o módulo por trás do /api/docs/training. AUTOR. Os cursos contêm uma lista ordenada de itens em que um ENSAIO é uma PEER de uma UNIDADE, nunca uma criança: que uma decisão permite que um único modelo express exame final" sem uma segunda estrutura. As poupanças de conteúdo são definidas em substituição porque o editor possui o lista completa ordenada; uma API de patch por linha precisa de um verbo de exclusão mais uma reordenação, que é três rodadas viagens e três chances para o esboço que o autor vê para divergir do armazenado. PUBLICAR congela uma versão imutável que atribui pin. Cada regra de um membro é copiado para ele NA MESMA DECLARAÇÃO que escreve a linha — o módulo legal declarou essas colunas e deixou-os para uma chamada posterior que nunca veio, e é por isso que cada versão legal na produção tem um certificado nulo. TRFP1 é prefixado em vez de delimitador-juntado de modo que um valor trabalhado não pode imitar um limite de campo, e isCorrect é INSIDE o hash: o módulo legal enviado LFP1 sem o campo equivalente, publicar respondeu "nada mudou", e um layout corrigido poderia Nunca ser congelado. Publicar recusa com um 409 nomeando o item ofensivo — incluindo um slide de mídia com no alt text, porque a ferramenta de autoria é o único ponto de execução que realmente detém. GRADING É SERVER-SIDE, e esta é uma propriedade de segurança em vez de uma característica. O jogador é servido ************* que são classes diferentes das DTOs do autor e simplesmente não tem nenhum campo de correção para povoar — uma classe compartilhada com um campo anulado retorna o primeira vez que alguém reutiliza um mapper, e o vazamento é silencioso enquanto o registro de conformidade continua procurando válido. Shuffle e o sorteio de perguntas são computados aqui e gravados na tentativa, então uma retomada sessão vê as mesmas perguntas; um shuffle cliente-side sobre uma chave de resposta do cliente-held é o teatro. Respostas corretas só voltam quando as respostas permitem essa tentativa, então um membro não pode falhar deliberadamente para colher a chave. Crédito parcial é pisou em zero (sem ele, selecionando tudo pontua negativo e arrasta para baixo o resto do teste). Percentagem é arredondada meio-up exatamente uma vez que a tela de resultados, o e-mail, o certificado e CSV não pode discordar cerca de 79,5% contra uma barra de 80%. Um curso sem testes passa em conclusão — possível == 0 nunca deve dividir e nunca deve significar "falhado". EVIDÊNCIA. TRMF1 (o que viram) e TRQZ1 (o que responderam) são armazenados como digestões E verbatim cordas canônicas, porque um digerir ninguém pode recomputar de evidências armazenadas não é evidência. A atestado é escrito para cada resultado terminal, passe ou falhe: "eles o tomaram e não passaram" é exactamente como um facto de conformidade. Os Reminders varrem ORG-MAJOR e disparam cada org na sua hora local. Organização. intervalo de tempo 21 O ZoneId.systemDefault () diz a um inquilino de Sydney que "deve hoje" no dia errado. O balde permanece uma função pura de (dueDate, hoje), que é o que torna Extend Due Date livre varrer replayable; catch-up envia apenas o degrau não enviado mais recente após uma parada e registra o Os mais antigos, como substituídos. Idempotência é uma tabela real com um índice de reivindicação único em vez de um Redis SET NX, então "dissemos a Ada em 4 de março" sobrevive a um FLUSHDB e é questionável — a tabela legal própria O Javadoc diz que está certo e adiado. Uma corrida seca não reivindica nada; alegando que queimaria cada balde Inspeccionado e suprimir a corrida real atrás dele. DocsInternalAuthFilter ganha o caminho interno de treinamento. Sem ele, a varredura cruzada é world-alcançável, porque ResourceServerConfig é anyRequest().permitAll() e OTKPreAuthFilter continua a cadeia quando nenhum OTK está presente. 455 testes passam.
· kamo-shared-library
Os quatro modelos de email de treinamento
A mesma constante/chave pontilhada que a família legal do RH: o CONSTANT é o tronco do nome do arquivo, o templateKey a plataforma SENDS é o valor pontilhado dentro de cada .json. Fazer isso errado é viver QUOTE SENT/commerce.quote sent bug — cada envio de 404s para sempre contra um modelo que semeou perfeitamente — então HrTrainingTemplateTest pinos ambos metades em ambas as direções. UM modelo de lembrete cobre cada degrau da escada (T-14/7/3/1, devido hoje, e todos os passados período). Tone é transportado por uma sobrancelha pré-renderizada e duePhrase em vez de por arquivos separados, porque um par canônico meio-adicionado joga dentro loadAll() — que é o que seedForOrg itera — e quebraria a semeadura para cada org na plataforma em vez de apenas a sua própria. Cada modelo é um 600px inline-styled card com um VML à prova de balas + âncora CTA, porque Outlook O motor da palavra ignora a âncora completamente. O teste afirma que ctaUrl aparece exatamente três vezes (VML href, anchor href, copy-paste fallback), que nenhum modelo usa {{#if} (o renderizador é um regex plano substituir e renderizá- lo como texto visível), que nenhum modelo puxa um recurso externo, e que cada um carrega as três variáveis que o EmailTemplateService injeta para que a marca branca funcione. Sujeitos usam cursoTitlePlain e corpos usam cursoTítulo: os dois renderizam a partir de um mapa variável, então um valor escapado em um assunto mostra "Manual do empregado" no cabeçalho do cliente de e- mail enquanto um Uma em um body ships marca viva do próprio domínio assinado pela DKIM. Nenhum email de repetição é enviado em uma falha final. Dizer a alguém por e- mail que tem permanentemente falhou um curso obrigatório, sem caminho para a frente e nenhum humano ligado, é o meio errado — o RH O grid carrega-o em vez disso. 1186 testes passam.
· kamo-internal
Uma superfície de membro para ambas as obrigações, um crachá para qualquer um dos dois
A aba de conformidade do membro teve dois sub-tabs, Pendente e Concluído, para um tipo de obrigação. Esses são um ESTADO, não um tipo — então eles colapsam em um filtro de status (com "todos", que duas abas não tinha onde colocar, e que anteriormente significava verificar ambos para responder "foi este eu assinado?"), e a faixa de tabulação é liberada para separar as duas coisas que são genuinamente diferentes: Pacotes jurídicos — instrumentos para assinar, comportamento inalterado atrás de um filtro Formação designada — cursos a realizar Cada painel mantém seu próprio filtro em seu próprio param. Um filtro compartilhado levaria um balde legal no painel de treinamento, onde a mesma palavra significa um conjunto diferente de status, e abas de comutação Seria silenciosamente refiltrar a outra lista. Um ?sub= estrangeiro é normalizado no monte, porque SettingsView.writeTabToUrl sets ?tab= e delete ?configure but leaves ?sub= soly — the screen was já correto através do recuo do índice, mas o URL estava reivindicando um estado que este componente nunca teve, e o URL é o que é bookmarked e colado em um ticket de suporte. NavTop agora mostra UM crachá para ambos. Um membro não pensa em "pacotes para assinar" e "cursos para tomar "como dois sistemas, eles pensam em coisas que eles devem; duas marcas quase idênticas lado a lado em cromado fixo iria fazê-los trabalhar para fora qual um é iluminado. A contagem é a soma, e o ícone ganha um estado real de atenção — dois anéis sobre ::antes/::depois de um invólucro, para que o próprio glifo nunca se mova e nunca combate a âncora do distintivo. Moção é um sinal adicional, não o único (a contagem e A cor do erro ainda o carrega), razão pela qual prefere o movimento reduzido o deixa cair sem nada perdido. MembroAtribuído Treinar deliberadamente não envia coluna de ação. FormaçãoAcção Para já decide start/resume/retake/certificate e é testado por unidade, mas o jogador do curso não é construído — e um visivelmente desactivado "Iniciar" é uma promessa que o produto não pode manter, que lê como quebrado em vez de inacabado. A coluna chega com o jogador. FormaçãoEstatutos Para enviar todos os status EXPLICAMENTE para "todos" em vez de omitir o param: o O recurso sem filtro do servidor existe para salvar um marcador velho, não para ser uma convenção de chamadas. Os estados viajam como parâmetros repetidos, nunca CSV — uma vírgula é codificada pelo gateway APIService. check-i18n-keys pegou as chaves faltando exatamente como projetado; hr.training.member.* foi empurrado para kamo-traducation-dictionary primeiro e verificado em sua origem/main antes deste commit. teste npm: 3 guardas limpos, 98 arquivos, 1221 testes passando.
· DocsService
A API de treinamento voltada para membros
Registra com.kamo.z.shared.hr.training em @EnableJpaRepositórios — @EntityScan é amplo para o entidades são encontradas de qualquer forma, mas repositórios não são. Dois funis de autorização, deliberadamente separados, espelhando o módulo legal: FormaçãoPortas de acesso /api/docs/training/** em MANAGEM TRAINING (RoleRightType 185) lidas e escritas, que deve permanecer exatamente o aplicativo / hr / hrTabs.ts direito fecha a aba. Um servidor mais estreito que o aba deixa um membro capaz de abrir a aba e coletar um 403 sobre uma grade vazia; mais amplo, e é um Endpoint acessível por alguém que a UI nunca ofereceu. Os direitos são lidos de member rights applicated, nunca do token, porque o mapa de sessão OTK não tem direitos. TrainingAssignmentOwnership autoriza a superfície própria do membro por IDENTIDADE. TrainingMemberController Por conseguinte, não possui nenhum domínio de acesso à formação — um membro que faça um curso que lhes foi atribuído não tem HR direito, e roteá-los através do funil HR significaria ou bloquear todos os funcionários de o seu próprio treino ou alargamento do direito de RH definido até que o catálogo e a lista de audiência aberta ao Org inteiro. Propriedade responde 404, nunca 403: um 403 confirma que o UID é real e gira o ponto final em um oráculo para quem foi designado o quê, e um registro de avaliação não deve ser enumerável por um par. TrainingMemberSurfaceAuthTest pins todas as três propriedades por reflexão, porque o modo de falha é alguém adicionando o campo mais tarde "assim que o ponto final é garantido também". GET /atribuições e GET /atribuições / contagem de despesas. Nenhum mapeamento leva um membro ou org id — ambos vêm da sessão. status é um param REPEATÓVEL, nunca CSV: APIService entrega sua URL upstream para RestTemplate como um String, que recodifica% 2C para% 252C, então uma vírgula chega como um token literal não corresponde a nenhuma constante de enum. Um filtro não reconhecido significa "sem filtro", nunca "match nothing", então um marcador obsoleto mostra tudo em vez de uma grade vazia que lê como perda de dados. O paged lido é um repositório DocsService-local sobre a entidade compartilhada, ordenada (entrega no DESC, uid DESC) — pageando em uma chave não-única duplica e pula linhas, e uma atribuição de 200 membros escreve Entregue dentro do mesmo milissegundo. a contagem pendente retorna a mesma forma JSON que o quadro empurrado do crachá para que um analisador cliente sirva As duas coisas. As linhas são mapeadas dentro da transação; curso e versão são LAZY. 413 testes passam.
· InitializerService
Criar o esquema de treinamento de RH
Registers com.kamo.z.shared.hr.training na lista explícita @EnableJpaRepositórios — @EntityScan é amplo então as entidades são encontradas de qualquer forma, mas repositórios não são, e sem isso as tabelas nunca são criadas. TrainingSchemaMigration (@Order(43); 42 é o legal) acrescenta o que o mapeamento da entidade não pode Expresso. Cinco índices UNIQUE são tratados como fatais porque cada um é uma garantia de correção nada mais fornece: - (curse uid, version number): duas publicações simultâneas teriam o mesmo número e cada atribuição atribuída à "versão 3" torna-se ambígua. - um atestado por atribuição: o guarda WORM bloqueia a atualização e o DELETE, não uma duplicata Inserto. - o índice de pedido de notificação é a indemnidade de todo o gasoduto de chamada de atenção — o pedido É um INSERT contra ele, por isso, sem ele, uma varredura re-enviar todos os empregados atrasados de Todos os inquilinos. - uma recertificação por (membro, curso, data de vencimento); uma linha de progresso por slide. O item do curso CHECK é escrito como um predicado de duas colunas de propósito: SchemaCleanupRunner @Order(1) deixa cair cada uma das colunas em cada execução, porque essa é a forma Hibernate emite para @Enumerado(STRING). BarataDB/Yugabyte não têm ADD CONSTRAINT SE NÃO EXISTE para CHECKs, então "já existe" é capturado e tratado como sucesso. O teste é apenas de reflexão. Teste mvn nesta produção de refo MIGRATES — Testes de demonstração é um @SpringBoot Teste sem perfil de teste e sem src/test/resources — assim que afirma o SQL constantes contra o nome @Table de cada entidade sem iniciar um contexto, que é a falha que realmente acontece: uma tabela renomear deixando o SQL escrito à mão para trás.
· kamo-shared-library
Transmita uploads de documentos e adicione o esquema de treinamento de RH
ImageService ganha uploadDocumentStreaming, a contraparte de streaming do uploadDocument. O formulário buffered não pode expressar um upload grande em tudo: readInputStream aloca novo byte[size], um array Java é superior no Integer. MAX VALUE e MinIOStorageService.upload Em seguida, aloca uma cópia completa do segundo segundo para a repetição do nó cruzado. Contra um recipiente 2Gi que fez um algumas centenas de megabytes o teto prático — é por isso que a tampa multi-parte de imagem 500MB enquanto o caminho do anexo de bate-papo, que já é transmitido, correu para 3 GiB. O novo método hashes uma vez (ambos digere um tampão), cheira o tipo MIME ANTES do hash passe então um arquivo 3 GiB é rejeitado sem gastar uma leitura completa sobre ele, e Delega armazenamento para uma nova lojaConteúdoEndereçado sobrecarga que aceita digestões pré-computadas então a fonte é lida duas vezes em vez de três vezes. Deliberadamente não @Transactional: storage gigabytes leva minutos e uma transação realizada através dele fixa uma conexão agrupada. Além disso, acrescenta com.kamo.z.shared.hr.training — 20 entidades, 11 enums e 20 repositórios para o Módulo de formação de RH, espelhando com.kamo.z.shared.hr.legal's Conventions: BaseUuidEntity, an explicit last modified with touch() porque BaseUuidEntity.dateAtualizado está morto, fixado colunaDefinição em cada opcional @JoinColumn, e @OnDelete(CASCADE) no público FKs porque CascadeType.* não emite nada na DDL. Duas partidas do esquema legal, ambas deliberadas: - Estádio de Formação divide-se em FALHO RETAKE ALLOWED (aberto) e FAILED FINAL (fechado), e REMINDABLE STATES é estritamente mais estreito do que OPEN STATES assim que um membro aguarda As notas nunca são perseguidas. Pined by TrainingAssignmentStatusTest. - hr training notificação é uma tabela real com um índice de reivindicação UNIQUE em vez de um Redis SET NX, então "dissemos a eles nesta data" sobrevive a um FLUSHDB e é questionável. Resposta e atestado repositórios estender o marcador Repositório nua, não JpaRepositório, assim os métodos de exclusão em lote que bypass @PreRemove não existem na interface. Requer um KamoInitializerService executado antes que qualquer serviço que leia estas tabelas seja implantado.
· InitializerService
Criar grupos billing pay explicitamente, e desfazer a colisão legada
Criando-o em uma migração em vez de deixá-lo para ddl-auto, que produziu um tabela semi-construída quando a entidade foi apontada para os grupos de faturamento pré-existentes.
· EmailService
Tamanho de cada caixa de correio contra o armazenamento da organização
Dovecot agora impõe um limite por emailbox, e nada estava definindo a coluna Lê. Isto escreve-o todas as noites, após o snapshot de armazenamento para que a capacidade de ser dividido é a figura de hoje em vez da de ontem. Cada caixa de correio recebe uma parte igual de toda a piscina. A propriedade que importa é que as ações somam à capacidade, de modo que o correio nunca pode organização além do seu tecto. É também a versão que um proprietário pode prever — "sua parte do armazenamento da organização" — e não se move quando um domínio não relacionado cresce, que dando a cada caixa de correio o restante da sala de estar. Planos ilimitados e organizações sem assinatura ao vivo escrever 0, que o user query se transforma em um NULL quota rule: sem limite. Essa direcção é deliberada, porque a alternativa é recusar correio para uma organização cujas linhas de faturamento estão desaparecidos ou a meio da migração.
· KlusterServices
Forçar a quota de armazenamento na entrega de entrada, e parar de enviar a senha DB
Armazenamento de objetos já recusa conteúdo que levaria uma organização para além de sua teto, que cobre todos os domínios passando pela loja de objetos. Correio de entrada foi o gap: LMTP escreve diretamente no maildir sem Java no caminho, então O plugin de cota do Dovecot é a única coisa que pode pará-lo. O plugin carrega globalmente para que a entrega LMTP seja verificada — esse é o caminho que importa — com imap quota adicionado para clientes que perguntam. Há deliberadamente não cota rule padrão: uma regra global se aplica a cada caixa de correio que o userdb faz não responder, o que é o oposto de falhar seguro. A regra por usuário de user query é a única fonte de um limite, e é NULL até cota bytes é Todas as 18 caixas de correio existentes são ilimitadas exactamente como antes. virtual users.quota bytes foi adicionado antes disso, predefinido em 0, porque o novo user query referencia-o: o envio da configuração primeiro teria falhado a cada pesquisa do usuáriodb e e- mail retirado. O dovecot.conf gerado foi validado com doveconf contra o vivo 2.3.20 antes de empurrar; ele resolve para "sievequota" para LMTP e "imap quotaquotaquota" para IMAP. Separadamente, o ConfigMap não carrega mais a senha MySQL. Foi enviado em plaintext, comprometido aqui e legível de `kubectl get cm -o yaml`. A ConfigMap agora possui um placeholder e a credencial vem do existente mysql- secret através do ambiente; o recipiente recusa- se a iniciar se for vazio em vez de correr com um userdb inutilizável. Note que isso edita o par -simples, que é o que o CI aplica e o que é realmente Correndo. dovecot-configmap.yaml e dovecot- implantação. yaml são aplicados por nada e descrever uma imagem completamente diferente.
· KlusterServices
Forçar a quota de armazenamento na entrega de entrada, e parar de enviar a senha DB
Armazenamento de objetos já recusa conteúdo que levaria uma organização para além de sua teto, que cobre todos os domínios passando pela loja de objetos. Correio de entrada foi o gap: LMTP escreve diretamente no maildir sem Java no caminho, então O plugin de cota do Dovecot é a única coisa que pode pará-lo. O plugin carrega globalmente para que a entrega LMTP seja verificada — esse é o caminho que importa — com imap quota adicionado para clientes que perguntam. Há deliberadamente não cota rule padrão: uma regra global se aplica a cada caixa de correio que o userdb faz não responder, o que é o oposto de falhar seguro. A regra por usuário de user query é a única fonte de um limite, e é NULL até cota bytes é Todas as 18 caixas de correio existentes são ilimitadas exactamente como antes. virtual users.quota bytes foi adicionado antes disso, predefinido em 0, porque o novo user query referencia-o: o envio da configuração primeiro teria falhado a cada pesquisa do usuáriodb e e- mail retirado. O dovecot.conf gerado foi validado com doveconf contra o vivo 2.3.20 antes de empurrar; ele resolve para "sievequota" para LMTP e "imap quotaquotaquota" para IMAP. Separadamente, o ConfigMap não carrega mais a senha MySQL. Foi enviado em plaintext, comprometido aqui e legível de `kubectl get cm -o yaml`. A ConfigMap agora possui um placeholder e a credencial vem do existente mysql- secret através do ambiente; o recipiente recusa- se a iniciar se for vazio em vez de correr com um userdb inutilizável. Note que isso edita o par -simples, que é o que o CI aplica e o que é realmente Correndo. dovecot-configmap.yaml e dovecot- implantação. yaml são aplicados por nada e descrever uma imagem completamente diferente.
· kamo-internal
Deixe um membro licenciar seu próprio assento do console
O painel que fecha o ciclo de auto-pagamento. Só aparece onde o a política da organização permite, e apenas para as pessoas que já não estão comprando em nome da organização — o servidor verifica a mesma política, por isso esconder é uma cortesia em vez do controlo. Diz quem é cobrado antes de pedir um cartão, e sob o modo de aprovação diz Claramente que nada é cobrado até que o proprietário concorde. Falha ao ler elegibilidade esconde a opção em vez de mostrar uma que seria recusada. Quando a política não permite que os auto-pagadores escolham um plano, eles tomam o a organização está ligada e o catador não é de todo mostrado — um proprietário que delega custos normalmente não delega a decisão do produto.
· BillingService
Diga às pessoas que o seu pagamento falhou, e manter a assinatura para baixo
O dunning que não existia. failure.payment failed escreveu uma linha e retornou: nenhum email, nenhuma bandeira, nenhuma alteração de estado, e billing.email-service-url foi configurado e referenciado por nenhum código Java. O webhook começa agora o relógio de delinquência na primeira falha e o limpa o momento em que uma fatura é paga, por isso uma assinatura recuperada começa do que carregar um relógio velho para o próximo fracasso. Apenas a primeira falha começa — uma repetição uma semana mais tarde não deve reiniciar a graça e entregar um não-paga assinatura mais uma semana de serviço em cada tentativa. A varredura deliberadamente não decide quando os direitos terminam. Isso acontece em o relógio, em isLive, então não há janela onde uma assinatura caducada ainda funciona porque um trabalho não funcionou. O que a varredura possui é o que um relógio não pode fazer: Dizendo isso uma vez, e cancelando depois de autoCancelAfterDays - trinta dias por padrão, muito tempo depois de o serviço parar. Essa lacuna é recuperável de propósito, porque cliente que atualiza seu cartão no dia vinte recebe tudo de volta pagando, e uma assinatura cancelada é muito mais difícil de desfazer do que uma suspensa. Notificação é o melhor esforço por design: Stripe repete um webhook que responde non-2xx, e um serviço de correio não acessível não é uma razão para reprocessar um pagamento evento.
· InitializerService
Adicionar as colunas do relógio dunning
past due desde e suspension notificated at, ambos nuláveis. Null significa pagar e não-ainda-told, respectivamente.
· kamo-shared-library
Fazer um pagamento falhado significa alguma coisa
PAST DUE contado como totalmente vivo em ambos os resolvedores de direitos, sem timestamp e sem tampa, de modo que um cliente cujo cartão falhou manteve todos os recursos por A Stripe deixou a subscrição nesse estado — na prática, indefinidamente — e ninguém, cliente ou operador, foi informado. Uma assinatura agora carrega quando a coleção falhou pela primeira vez, e PAST DUE está ao vivo apenas dentro do seu período de graça. Computado em leitura, exactamente como expiração do julgamento, assim o serviço pára no relógio em vez de esperar que um trabalho para notar: a decisão programada deixaria uma janela onde uma assinatura caducada ainda funcionava Porque a varredura ainda não tinha corrido. Duas escolhas deliberadas sobre estar errado. O valor por omissão é uma semana em vez de zero, porque um pagamento falhado é geralmente um cartão expirado e não uma decisão de Pára de pagar. E uma longa fileira sem selo lê como uma graça interior, porque cada subscrição anterior à coluna tem uma — tratamento nulo como caducado seria cortar clientes no momento que este enviado, para uma delinqüência ninguém tinha medido.
· BillingService
Deixe um membro comprar seu próprio assento, e fechar um buraco corpo-parâmetro
O auto-pagamento não tinha rota dentro da consola. Tudo na superfície de faturamento é em forma de conta — você já deve ter uma linha ContaMembro antes de Planos e A cobrança mostra-lhe qualquer coisa, e a única maneira de conseguir um era uma ação do proprietário — assim um membro que queria pagar por si mesmo foi dito para ir e perguntar. O verdadeiro caminho de auto-pagamento que existia, o check-out do consumidor utilizado pelos produtos de marca branca, é inacessível a partir do console porque o proxy de faturamento reconstrói cabeçalhos de arranhar e nunca encaminhar o cookie que seu resolvedor de sessão precisa. O comprador é quem quer que seja a sessão. X- Member- ID é definido pelo proxy de *** e nunca pelo chamador, por isso ao contrário das rotas da conta não existe ID no caminho para a troca — que é toda a razão por que esta é a sua própria rota, em vez do que outro responsável em /contas/{contaUid}. A política da organização é verificado servidor-side, então esconder a opção em outro lugar é mais do que decoração, e A tampa do assento por pagador é forçada aqui em vez de confiar no cliente. Também fecha um buraco de garantia cruzada no POST /contas. Essa rota não tem ID de conta em seu PATH, assim AccountOrgScopeInterceptor — que chaves nesse segmento — nunca e qualquer membro de qualquer organização poderia nomear conta no CORPO e reescrever seu contato de faturamento, redirecionando suas faturas e recibos. Nomear um membro arbitrárioId fez o mesmo a partir da outra direção. Ambos são agora obrigados a pertencer à organização do chamador.
· kamo-internal
Deixe o proprietário decidir quem pode pagar, e gerenciar os grupos que fazem
O proprietário tinha exatamente uma alavanca sobre o auto-pagamento e era a forma errada: um irreversível promover gesto, sem forma de dizer antecipadamente se era permitido e nenhuma maneira de desfazer — enquanto um caminho de checkout do consumidor membro auto-assinalar independentemente. Este é o controlo preventivo, e é o que O resto da consola esconde-se. Quatro modos, rotulados pelo que acontece com outras pessoas e não pelo enum: proprietário escolher uma política é decidir o que sua equipe pode ver, de modo que cada A opção descreve. Os grupos só aparecem quando o modo os permite, porque um grupo são várias pessoas fazendo o que uma pessoa já pode fazer. Os não-proprietários vêem a configuração apenas leitura, em vez de nada. "A organização paga centralmente" é a resposta para o porquê eles não têm opções de compra, e escondendo Isso só se transforma numa questão de apoio. Uma falha ao carregar esta seção deixa o coletor de planos funcionando — uma política console não pode ler não deve levar a superfície de compra para baixo com ele.
· BillingService
Política de org e APIs de grupos de faturamento
Quem dentro de uma organização pode pagar Kamo é agora responsável e settable. Ambos as rotas são org-scoped e, portanto, deliberadamente fora /api/billing/accounts/** e seu interceptor: "que meu pessoal compre seus próprios assentos" tem que ser responsável antes de existir uma segunda conta, que é exatamente quando uma configuração de conta Não posso ajudar. A leitura da política está aberta a qualquer membro, porque as superfícies que se escondem Os controles de auto-pagamento precisam da resposta e não revelam nada acionável. Tudo que altera quem paga pelo que é apenas proprietário, verificado contra a base de dados em vez do cabeçalho fornecido pelo chamador. Criação de grupo recusa quando a organização não tem permitido grupos — um grupo é várias pessoas fazendo o que uma pessoa já pode fazer, então uma organização que Também não permitiu o auto-pagamento. REQUISITOS APROVAÇÃO um grupo inicia não aprovado; todos os outros modos já deram permissão de manta, por isso começa aprovado. Excluir um grupo dissolve o nome e nunca o faturamento: cancelar o que ele paga é um ato separado, deliberado. O roster vem das linhas de assinatura do próprio membro, em vez de sendo duplicado no grupo, então um grupo não pode discordar com os assentos que é A pagar.
· kamo-internal
Um console de planos e faturamento que um proprietário pode realmente encontrar
Não havia como comprar uma primeira assinatura em qualquer lugar do produto. Listas de planos foram acessíveis apenas através de uma assinatura que você já realizou, então uma organização sem ninguém viu um painel vazio e uma dica apontando para uma org diferente. O único. tela de faturamento real viveu na página de perfil de membro, arquivado nas configurações aterrissagem sob um título intitulado "Personal", e foi escondido inteiramente para qualquer org sem um pai. A barra de administração da casa não tinha nenhuma entrada de cobrança. O console responde à pergunta com a qual um proprietário chega — o que estamos, o que custa, como faço para alterá-lo - e preços cada controle como eles movem, assim o total é visível antes de tudo ser cometido. Assentos, complementos e armazenamento bloqueia cada atualização de um número em execução, e um plano anual indica o que realmente deixa a conta uma vez por ano em vez de deixar um número por mês para ser multiplicado na cabeça do leitor. A aritmética é planQuote, testada contra o caminho de carga, porque ambos os lugares a plataforma anteriormente citado um preço discordou com o que Stripe cobrado. Acessível a partir da barra de administração home e do menu de configurações, fechado sobre o financiamento direitos em vez de CONFIGURE SYSTEM: decidir o que a organização paga é uma decisão financeira.
· BillingService
Por omissão, uma nova organização de destino da subscrição para o chamador
Uma primeira compra não tem assinatura existente para copiar alvoOrganizaçãoId de, e requerê-lo incondicionalmente é o que fez criar um a partir do console impossível. Uma organização que compra lugares para si mesma é o caso comum; O acordo sub-org ainda o passa explicitamente.
· kamo-internal
Mostrar o teto, não apenas os bytes
O painel reportado foi armazenado, adicionado, removido e os bytes da vida útil contra n referência em tudo — de modo que um proprietário olhando para 400 GB não tinha nenhuma maneira de dizer se isso estava confortável ou prestes a recusar seu próximo upload, e seu primeiro conhecimento de um problema teria sido um fracasso. O subsídio do plano e os blocos comprados são o que transforma esses números em uma posição, então a barra de capacidade vai primeiro e tudo abaixo lê contra ele. O bar permanece verde até que importe: âmbar a 90%, vermelho no máximo. Um bar que Torna-se âmbar a 60% ensina as pessoas a ignorá-lo. Também diz claramente que as leituras não são afetadas. "O armazenamento está cheio" diz como "Eu perdeu o acesso aos meus arquivos", a menos que você diga o contrário, e a única coisa encher o seu armazenamento nunca faz é tirar nada. Tamanho do bloco e preço vêm do catálogo em vez de ser codificado, então o "Add 10 GB por US $ 2,50 por mês" linha não pode derivar do que é realmente cobrado.
· SecurityService
Promoção do portal na política do proprietário, torná-lo idempotente, adicionar o seu inverso
Promover um membro a pagar por si próprio era incondicional, unilateral e inseguro repetir. Agora se recusa quando a política da organização diz que a organização paga todos — anteriormente o único controle sobre o auto-pagamento era a irreversibilidade do próprio gesto. É idempotente. Promover o mesmo membro duas vezes inserido uma segunda conta e uma segunda linha do membro da conta, tropeçando no único no membro id e respondendo 500; um duplo-clique, uma repetição após um blip de rede, ou um proprietário a repetir o fluxo para Movam mais licenciados. Reutiliza agora a conta e a subscrição da membro já tem e continua para a transferência. DemoteBillingOwner é o inverso que nunca existiu. Um proprietário que promoveu alguém não poderia desfazê-lo, e se esse membro deixou, seu pessoal A conta manteve-se detendo assentos mirando a organização sem nenhuma maneira de recuperar Eles. Os licenciados voltam à subscrição da organização e ao A conta e os métodos de pagamento são deixados em paz, porque reivindicar assentos da organização depende do negócio do proprietário e Apagar o histórico de alguém não é. A correspondência está no Usuário compartilhado em vez do que o ID do membro, uma vez que a promoção espelha o membro na org de topo. Também corrige a catraca do endpoint, que já estava a falhar no principal antes deste trabalho — assim a construção do SecurityService era vermelha e nada estava a implantar-se. A verificação não foi possível ver dois guardas reais: extractUserId lê userID fora do *** sessão, e requireTopLevelOwner passa por CallerSessionResolver e mais requer que o chamador possua a org de topo. Ensinando-o ambos deixar 25 ocioso as entradas de base devem ser suprimidas, que é exactamente o que o ficheiro pede — a fixação de um Endpoint significa remover sua linha, e é isso que faz o número cair. runSweep é linha de base com sua razão real: interna-auth guardado através de um parâmetro de cabeçalho que o corpo-scan não consegue alcançar.
· InitializerService
Largue a restrição que limita um membro a uma conta de faturamento
Encontra a restrição pela forma em vez de pelo nome — Hibernate a gerou e o nome não é estável em ambientes, então qualquer restrição única em account members cuja lista de colunas é exatamente member id é a única a soltar. A composite on (account uid, member id) tem duas colunas e nunca corresponde.
· kamo-shared-library
Que os membros e grupos paguem por si mesmos, segundo as regras do proprietário
Faltavam três coisas e uma era ativamente prejudicial. account members carregava um único elemento global no member id — um membro poderia administrar exatamente uma conta de faturamento em toda a plataforma. Esse é o teto que fez cada arranjo multi-pagador impossível, e já estava causando danos em vez de meramente limitando: AccountMemberService.add() trabalhou em torno dela silenciosamente DELETAR a linha do membro em sua conta anterior, assim concedendo acesso de faturamento A um revogou-o de outro sem aviso; e promoverToBillingOwner lançou uma violação de restrição em sua segunda execução, transformando um duplo-clique em um 500. A compósito único em (account uid, member id) já existia e expressa o Uma verdadeira regra. Deixar cair faz uma descoberta derivadaByMemberId retornando Jogando opcional o momento Qualquer um tem duas filas, e nove pessoas dependem disso. É agora uma consulta limitada Ordenados pela criação, para que aqueles que ligarem, mantenham a resposta que tiveram em vez de dependendo do que o planner retornar primeiro; findAllByMemberId está lá para código que quer todos eles. OrgBillingPolítica é o controle preventivo que o proprietário nunca teve. O 2026-04-16 projeto especificado billing delegation mode e implementação deixou cair, deixando um irreversível promover gesto e nenhuma maneira de dizer antecipadamente se O auto-pagamento foi de todo permitido — enquanto um caminho de check-out do consumidor membros auto-assinalar de qualquer maneira. Onde um modo proíbe algo que as superfícies escondem em vez de o oferecer e falhar. BillingGroup nomeia um arranjo o modelo já permitido: AccountSubscription é único em (conta, mercado, org-alvo), por isso várias contas atingir uma organização. Não tem uma lista própria — quem detém um lugar é AssinaturaMembro mais ContaLicença, a mesma resposta que para qualquer outra pessoa, e um A segunda lista seria uma segunda verdade para manter o passo.
· SecurityService
Expor o impacto da falha aberta ao operador
A página de catálogo de aplicativos já responde à pergunta independente de orgs — faz qualquer Plano mencionar este aplicativo — e levanta um aviso quando ninguém faz. Isto responde Inquilinos por inquilinos atrás dele, que é o que os portões realmente abrem a válvula.

Seu universo está esperando.
Comece gratuitamente em minutos.Adicione produtos sempre que estiver pronto — sem migração, sem novos logins.
Gratuito para sempre — não é necessário cartão de crédito