Changelog
Cosa c'è di nuovo.
Ogni miglioramento alla piattaforma, come si spedisce.
· kamo-internal
Un gruppo su cui puoi mettere le persone e pagare
Creare un gruppo ha prodotto un nome e nient'altro — nessun modo per aggiungere nessuno, nessun modo per il suo manager di scegliere un piano, da nessuna parte pagare. Aprire un gruppo ora mostra chi paga, chi ci sta, chi sta ancora aspettando un posto, e il piano controlla che Compra quei posti. Creare si chiede chi paga, utilizzando il raccoglitore canonica. Una volta mandava il conto dell'organizzazione uid, che è esattamente il motivo per cui ogni gruppo è stato fatturato l'organizzazione; il pagamento è ora risolto lato server dal gestore e accountUid non è più sul cavo affatto. Il gallo distingue avere un posto da aspettare uno. Un manager che mette 8 persone su cinque posti è detto che tre colleghi non hanno accesso piuttosto che scoprirlo da loro.
· BillingService
Rendere i gruppi di fatturazione lavorare fine a fine, e smettere di pagare per nulla
Un gruppo può ora riunire le persone, comprare un piano sulla carta del suo manager, e la mano i suoi posti fuori. Nessuna di queste esisteva: le uniche operazioni sono state create, approvate e cancellare, quindi un proprietario che ha fatto un gruppo ha un'etichetta e non c'è modo di usarlo. BillingGroupSeatService è dove i soldi si muovono e il diritto è emesso, tenuto a parte la registrazione in modo che nessun nuovo percorso può essere aggiunto che salta le porte. abbonarsi() rifiuta prima di creare() viene chiamato piuttosto che dopo, perché creare() apre un abbonamento Live Stripe — un gruppo non approvato, un legacy, un manager senza carta o un tappo di sicurezza si fermano tutti lì. Due difetti hanno reso la fattura del gruppo la persona sbagliata: - Il pagatore era qualsiasi conto. console ha mandato l'organizzazione. Un gruppo fatturato all'organizzazione non è un gruppo di fatturazione; è un secondo nome per l'abbonamento dell'organizzazione. Il pagatore è ora derivato dal direttore e i due sono asseriti di concordare prima che tutto venga accusato. L'invio di accountUid è respinto. - garantireAccountForMember riutilizza il più antico link AccountMember del membro, che per chiunque abbia mai aggiunto al conto dell'organizzazione è il conto dell'organizzazione. garantirePersonalAccountForMember identifica invece da primarioMember, che è l'unico campo che dice che un account appartiene a una persona. Dove un proprietario gestisce il proprio gruppo, "la loro scheda" e "l'organizzazione scheda" sono veramente la stessa riga Account e lo stesso cliente Stripe, e non il codice puo' distinguerli. Ciò è rifiutato se non confermato, piuttosto che cartaceo. Passo. Separatamente, due bug dal vivo trovati mentre qui: - /self/seat ha accusato il membro e non ha mai emesso un posto. Ha chiamato creare() e restituito; non c'era alcuna riga SubscriptionMember e nessun AccountLicense, quindi l'acquirente rimase incriminato mentre la loro carta è stata fatturata ogni mese. - @Transactional in AccountBillingService si era spostato su un metodo privato, quindi La primavera l'ha ignorato e garantireAccountForMember ha funzionato senza alcuna transazione affatto. A fallimento tra i suoi due risparmi orfano di un Account la prossima ricerca non poteva vedi, e la seguente chiamata ha creato un secondo. /self/coverage è nuovo: risponde "chi paga per il mio posto", che nulla ha fatto. È anche quello che ferma qualcuno già coperto essendo offerto un secondo posto su la loro carta.
· InitializerService
Schema per gruppi che possono effettivamente pagare
Tre corridori: BillingGroupRosterMigration crea billing pay group members — che un gruppo significa pagare, prima che abbia comprato qualcosa. org id è denormalizzato su di esso così il database può far valere "un membro è sulla maggior parte di un gruppo di fatturazione attivo per organizzazione" come un unico parziale. Due gruppi che pagano per una persona sono due oneri per un posto, e niente altro nello schema lo cattura: AbbonamentoGli unici di Member sono per abbonamento e AccountLicense non ha nessuno. ****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************** e sostituisce il trecolo unico con due unici parziali. Lo spalanca quattro colonne non funzionerebbero — NULL sono distinte all'interno di un indice unico qui, quindi ogni fila non di gruppo smetterebbe di essere vincolato. BillingPayGroupColumnsMigration aggiunge market id, Sit limit e stato, e segni gruppi esistenti LEGACY. Questi sono stati fatturati a qualsiasi account uid della richiesta corpo trasportato, e la console ha inviato l'organizzazione, quindi il loro account uid è quasi certamente sbagliato. Non c'è modo sicuro di indovinare quale carta era destinata, e indovinando le fatture sbagliate la persona sbagliata, quindi sono rifiutati al punto di carica invece di riparare. La prima esecuzione ha mostrato perché le questioni di riprova: entrambi gli indici di sostituzione non sono riusciti con 40001 non poteva serializzare l'accesso, il corridore ha registrato "skipped" e ha continuato, e il DROP del vecchio vincolo poi riuscito — lasciando account subscriptions con nessuna unicità, che è rigorosamente peggio della collisione questo risolve. Dichiarazioni DDL ora riprovare sul fallimento di serializzazione, e la caduta è saltata completamente a meno che entrambi i sostituti non siano confermati.
· kamo-shared-library
Lasciate che un gruppo di fatturazione sia il proprio payer
Un gruppo potrebbe essere chiamato e nient'altro. Il suo abbonamento non è mai stato stabilito — nulla nella piattaforma chiamata setSubscription — così sottoscrizione uid rimasto null per sempre e il gruppo non poteva comprare o tenere un singolo posto. Tre cose hanno dovuto cambiare nel modello: BillingGroupMember è nuovo. Sedile verità rimane dove era, su AbbonamentoMember e AccountLicense, ma AbbonamentoMember.subscription NON è NULL, quindi fino a un gruppo compra un piano non c'è posto per registrare le persone che esiste per coprire — e raccogliere le persone prima di acquistare posti per loro è tutto il punto. Questo è intento; questo è diritto. AccountSubscription guadagna fatturazioneGroupUid, e il (account, mercato, target org) unico vincolo si allontana dall'entità. Un gruppo legge il suo manager, sul conto del gestore, mirando allo stesso org nello stesso mercato di qualsiasi sede che il direttore già comprato per se stessi — lo stesso tuple. Quindi un gruppo e' il primo l'acquisto non ha fallito, ha trovato l'abbonamento personale del gestore e riscritto esso: piano, sedili, Stripe id. Nessuna eccezione, nessuna seconda fila. La regola ora vive come due indici unici parziali che JPA non può esprimere, creati da KamoInitializer; la dichiarazione dell'entità doveva andare o ddl-auto avrebbe ricreato il vecchio vincolo davanti al corridore che lo cade. E' stata cancellata piuttosto che... deprecato. restituisce Facoltativo sulla forza di un vincolo che non più esiste, quindi una volta che un account contiene sia un abbonamento personale che un gruppo corrisponde a due file e lancia in tutti e cinque i siti di chiamata attraverso tre servizi. Lasciarlo trovare da autocompleto era l'opzione più pericolosa. Inoltre: consenteGruppi() ora include REQUIRES APPROVAL. Escluso ha fatto percorso di approvazione irraggiungibile — la creazione di un gruppo è stata rifiutata in modo definitivo, mentre codice che imposta la bandiera approvata chiesto richiede Approval() e così potrebbe solo mai rispondere "già approvato". Un proprietario che ha scelto la supervisione non ha nessun gruppo.
· kamo-internal
L'editor del corso HR, assegnazioni griglia e giocatore del corso membro
/hr/training sostituisce il suo segnaposto con i due sottotitoli il brief richiesto, e il membro La griglia di formazione assegnata guadagna la colonna d'azione che stava tenendo indietro per volere di un giocatore. Signori. Una griglia di catalogo con pubblicare, assegnare, duplicare, archiviare ed eliminare in linea. Cancellare è disabilitato una volta che qualcuno detiene un incarico e i punti di punta su Archivio invece — eliminando un corso qualcuno è stato assegnato agli orfani le loro prove, dal momento che l'assegnazione pins una versione la cui I genitori se ne sarebbero andati. "Niente è cambiato dalla versione N" è mostrato come INFO piuttosto che un errore: l'autore ha premuto Pubblicare su qualcosa già pubblicato, che è un successo che hanno letto male. EDITOR. Outline · tela · ispettore come un percorso a pagina intera nidificato piuttosto che una finestra di dialogo — una diapositiva ha bisogno di una tela accanto a un profilo e un ispettore, autore è lavoro multisessione che deve sopravvivere a un rinfresco, e hrTabMatches è stato scritto in modo da un percorso nidificato mantiene la scheda madre illuminata e MANAGE TRAINING-gated senza nuova registrazione. I risparmi sono per-ITEM e debounced; un PUT a tutto campo con sostituto semantics trasforma due amministratori modificando due unità diverse in una silenziosamente perdendo il loro lavoro. Dopo ogni filo il corso è riletto, perché saveItemContent ricrea le righe dei bambini e una copia locale stante invierebbe id fantasma sul salvataggio successivo. Reordering è pulsanti freccia guidato da moveInOrder, che CLAMPS piuttosto che avvolgere — avvolgere silenziosamente riordina un corso che l'autore ha fatto non significa riordinare, e l'ordine è l'ordine di lettura. MEDIA proviene da /hr/risorse attraverso una nuova LibraryPickerDialog, generalizzata da MaterialPickerDialog sollevando le sue due costanti codificate in oggetti. assocType è l'ORDINAL (13), mai l'enum id (15): l'enum skips id 6, e l'invio di un id atterra su un diverso Le regole della collezione. Il testo di Alt è richiesto su qualsiasi diapositiva di media e blocchi pubblicare — lo strumento di autore è l'unico punto di forza che effettivamente detiene, perché una lista di controllo di revisione è eseguito una volta e alt testo viene aggiunto mai. PLAYER. Riprende al primo GAP, mai il punto più lontano raggiunto, e ogni cancello si unirà il record del server con QUESTO sessione — la lettura solo la riga del server rende Next irraggiungibile il momento una zecca non è a tutto tondo, che è l'esatto bug il mago legale spedito. Il completamento video è misurato come WATCHED COVERAGE, non correnteTime. /api/imaging/stream honours HTTP Gamma fine alla fine, così un discente può cercare all'ultimo secondo in una richiesta e qualsiasi posizione basata cancello è sconfitto da un singolo trascinamento; accumulare() tratta un salto più grande del divario di zecca come una ricerca e apre un nuovo intervallo invece di accreditare l'arco saltato sopra. videoCoverage.test.ts afferma "seek alla fine i rendimenti inferiori al 5%" come primo caso. La copertura è media time, quindi la riproduzione 2x deve ancora coprire la linea temporale — che è il motivo per cui permetterlo è giusto piuttosto che una scappatoia. Un portello di fuga visibile appare una volta che la lunghezza completa del video è trascorso in tempo a parete e record skipReason sul progresso fila, perché un cancello senza fuga diventa un biglietto di supporto la cui risposta è sempre "noi segnalo completo per voi" — lo stesso risultato, non tratto. Il corridore di prova non contiene dati di correttezza affatto: il carico utile è costruito da PlayerQuestion Dto, che non ha un tale campo, e il server decide. Risposte autosave così un portatile chiuso non è un perso tentativo, e gli spazi vuoti sono consentiti ma devono essere confermati. npm test: 3 guardie pulite, 102 file, 1257 test. compilazione successiva.
· DaemonService
L'orario di formazione promemoria tick
ORE 10, a differenza delle 13:00. La formazione ricorda ogni org al suo invio locale OWN ora, e Organization.timezone è di 21 ore (Pacific/Honolulu in Australia/Sydney) — un singolo quotidiano UTC tick significa "oggi" è sbagliato per la maggior parte della piattaforma, che è esattamente il motivo per cui la spazzata legale dice a un inquilino di Sydney "dovuto oggi" il giorno sbagliato. Ogni zecca visita solo gli org la cui ora locale corrisponde, tipicamente da uno a tre, quindi questa scansione LESS di un lavoro cross-org giornaliero piuttosto che 24x di più. Il lavoro vive qui piuttosto che come @Scheduled in DocsService perché questa distribuzione è replica: 1 con strategia: Ricreare, così il vecchio baccello è andato prima che il nuovo inizia e una finestra di distribuzione non può doppio fuoco. DocsService è RollingUpdate con maxSurge: 1, così per la lunghezza di ogni rollout là sono due baccelli ed entrambi credono che sia 09:00 — e per un lavoro la cui uscita è posta ad ogni ritardo dipendente sulla piattaforma, "di solito una volta" non è una proprietà. Explicit timeouts finito, perché un RestTemplate predefinito aspetta per sempre e, combinato con @DisallowConcurrentExecution, una cuneo a valle ferma il lavoro in esecuzione di nuovo fino al pod riparte — silenziosamente, e indistinguibile da "niente era dovuto". Non ricresce mai al quarzo: un l'eccezione di fuga può sparare immediatamente, trasformando un'ora saltata in una tempesta di riprovazione.
· DocsService
Il backend completo HR Training
Autorizzazione del corso, pubblicazione, assegnazione, giocatore membro, grading lato server, prove WORM e il richiamo spazzare — l'intero modulo dietro /api/docs/training. Sto morendo. I corsi portano un elenco ordinato di oggetti in cui un TEST è un PEER di un UNIT, mai un bambino: che una decisione permette ad un singolo modello di esprimere sia "un test alla fine di ogni spettacolo di formazione" che "a esame finale" senza una seconda struttura. I salvare dei contenuti sono sostituiti perché l'editor possiede intero elenco ordinato; un per-row patch API ha bisogno di un verbo elimina più un riordine, che è tre round viaggi e tre possibilità per il profilo che l'autore vede divergere da quello memorizzato. PUBLISHING congela una versione immutabile che assegna pin. Ogni regola a cui un membro è tenuto è copiato su di esso NELLO SAME STATEMENT che scrive la riga — il modulo legale ha dichiarato quelle colonne e li ha lasciati a un chiamante successivo che non è mai venuto, ecco perché ogni versione legale in produzione ha una nullità di attestazione. TRFP1 è prefissato di lunghezza piuttosto che congiunto al delimitatore in modo da un valore artigianale non può imitare un confine di campo ed èCorrect è INSIDE l'hash: il modulo legale spedito LPFP1 senza il campo equivalente, pubblicare risposto "niente è cambiato", e un layout corretto potrebbe Non essere mai congelato. Pubblica rifiuta con un 409 che nomina l'oggetto offeso — compreso uno slide media con nessun testo alt, perché lo strumento di autore è l'unico punto di esecuzione che effettivamente detiene. GRADING IS SERVER-SIDE, e questa è una proprietà di sicurezza piuttosto che una caratteristica. Il giocatore viene servito ****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************** e semplicemente non hanno campo di correttezza per popolare — una classe condivisa con un campo nullo torna prima volta qualcuno riutilizza un mapper, e la perdita è silenziosa mentre il record di conformità continua a guardare valido. Shuffle e l'estrazione della domanda sono calcolati qui e registrati sul tentativo, quindi un ripreso la sessione vede le stesse domande; un client-side shuffle su una chiave di risposta client-held è il teatro. Le risposte corrette ritornano solo quando gli show-answer lo permettono per quel tentativo, quindi un membro non può fallire deliberatamente per raccogliere la chiave. Il credito parziale è a zero (senza di esso, selezionando tutti i punteggi negativi e trascina giù il resto della prova). Percentuale è arrotondato mezzo su esattamente una volta così la schermata dei risultati, l'e-mail, il il certificato e il CSV non possono essere in disaccordo circa il 79,5% contro una barra dell'80%. Un corso senza passaggi di test al completamento — possibile == 0 non deve mai dividersi e non deve mai significare "fallito". EVIDENCE. TRMF1 (quello che hanno visto) e TRQZ1 (quello che hanno risposto) sono memorizzati come digestivi e verbatim stringhe canoniche, perché un digerente nessuno può ricomputare da prove memorizzate non è prova. The l'attestazione è scritta per ogni esito terminale, passa o fallisce: "lo hanno preso e non hanno superato" è esattamente quanto un fatto di conformità come passaggio. I RIMINatori spazzano ORG-MAJOR e sparano ogni org alla sua ora locale OWN. Organizzazione. timezone camps 21 ore, così il legale spazzare ZoneId.systemDefault() dice un inquilino di Sydney "dovuto oggi" il giorno sbagliato. Il secchio rimane una pura funzione di (dueDate, oggi), che è ciò che rende Extend Due Data libero e il spazzare replayable; catch-up invia solo la più recente rung insent dopo un outage e registra il quelli più vecchi come soppiantati. Idempotency è un tavolo reale con un indice di rivendicazione unico piuttosto che un Redis SET NX, quindi "abbiamo detto ad Ada il 4 marzo" sopravvive ad un FLUSHDB ed è queryable — il legale della tabella javadoc dice che è giusto e differito. Una corsa a secco sostiene di NONHING; rivendicare brucerebbe ogni secchio esso ispezionato e soppresso la vera corsa dietro di esso. DocsInternalAuthFilter ottiene il percorso interno di formazione. Senza di esso la spazzata cross-org è accessibile al mondo, perché ResourceServerConfig è qualsiasiRichiesta().permitAll() e OTKPreAuthFilter continua la catena quando non è presente OTK. Passo 455 test.
· kamo-shared-library
I quattro modelli di posta elettronica di formazione
Stessa chiave costante/dotted divisa come famiglia legale HR: il CONSTANT è lo stelo del nome file, il templateKey la piattaforma SENDS è il valore punteggiato all'interno di ogni .json. Che male è la vita QUOTE SENT/commerce.quote sent bug — ogni inviare 404s per sempre contro un modello che seme perfettamente — così HrTrainingTemplateTest perni sia metà in entrambe le direzioni. Un modello di promemoria copre ogni rung della scala (T-14/7/3/1, dovuto oggi, e ogni periodo). Tono è portato da un sopracciglio pre-rendered e duePhrase piuttosto che da file separati, perché una coppia canonica semi-aggiunta getta dentro caricoAll() — che è ciò che semeForOrg itera — e romperebbe la semina per EVERY org sulla piattaforma piuttosto che solo la sua. Ogni modello è una scheda in linea 600px con VML + ancora CTA a prova di proiettile, perché Outlook's Il motore di Word ignora completamente l'ancora. Il test afferma che ctaUrl appare esattamente tre volte (VML href, href di ancoraggio, copy-paste fallback), che nessun modello utilizza {{#if}} (il rendering è un regex piatto sostituire e lo renderebbe come testo visibile), che nessun modello tira una risorsa esterna, e che ciascuno porta le tre variabili EmailTemplateService inietta così bianco-etichetta di marca funziona. I soggetti usano il corsoTitlePlain e i corpi usano il corsoTitle: i due rendono da una mappa variabile, quindi un valore sfuggito in un soggetto mostra "Employee's Handbook" nell'intestazione del client di posta mentre un unescaped uno in un corpo navi live markup dal dominio DKIM-signed dell'org. Nessuna email di retake viene inviata su un guasto FINALE. Dire a qualcuno via e-mail che hanno permanentemente fallito un corso obbligatorio, senza percorso in avanti e nessun umano attaccato, è il mezzo sbagliato — l'HR La griglia lo trasporta invece. Passo 1186 test.
· kamo-internal
Una superficie membro per entrambi gli obblighi, un distintivo per entrambi
La scheda di conformità del membro aveva due sotto-tabs, Pending e Completed, per un tipo di obbligo. Questi sono uno STATO, non un tipo — quindi crollano in un filtro di stato (con "tutti", che due schede non aveva alcun posto da mettere, e che in precedenza significava controllare entrambi per rispondere "era questo firmata?"), e la linguetta è liberata per separare le due cose che sono veramente diverse: Pacchetti legali — strumenti da firmare, comportamento immutato dietro un filtro Formazione assegnata — corsi di formazione Ogni pannello mantiene il proprio filtro nel proprio param. Un filtro condiviso = porterebbe un secchio legale nel pannello di formazione, dove la stessa parola significa un insieme diverso di stati, e schede di commutazione ri-filtrava silenziosamente l'altra lista. Un ?sub= straniero è normalizzato via sul monte, perché ImpostazioniView.writeTabToUrl imposta ?tab= ed elimina ?configure ma lascia ?sub= da solo — lo schermo era già corretto tramite l'indice fallback, ma l'URL stava rivendicando uno stato questo componente non aveva mai, e l'URL è ciò che viene segnalato e incollato in un biglietto di supporto. NavTop ora mostra un distintivo per BOTH. Un membro non pensa a "pacchetti da firmare" e "corsi a prendere" come due sistemi, pensano a cose che devono; due segni quasi identici fianco a fianco in il cromo fisso li renderebbe lavorare fuori che uno è acceso. Il conteggio è la somma, e l'icona guadagna un stato di vera attenzione — due anelli su:: prima/::: dopo un wrapper, quindi il glifo stesso non si muove mai e non combatte mai l'ancora di Badge. Il movimento è un segnale ADDITIONAL, non l'unico (il conteggio e il colore di errore lo trasporta ancora), motivo per cui preferisce-ridotto-mozione lo cade con nulla perso. Iscritto La formazione non spedisce deliberatamente colonna d'azione NO. formazione Azione Per già decide start/resume/retake/certificate e viene testato unitÃ, ma il giocatore del corso non à ̈ costruito - e un visibilmente disabilitato "Start" è una promessa che il prodotto non può mantenere, che legge come rotto piuttosto che incompiuto. La colonna arriva con il giocatore. formazione Per inviare ogni stato ESPLICITLY per "tutti" piuttosto che omettere il param: il Il non-filtro del server esiste per salvare un segnalibro stante, non per essere una convenzione di chiamata. Gli stati viaggiano come param ripetuti, mai CSV — una virgola è doppia codificata dal gateway APIService. check-i18n-keys catturato le chiavi mancanti esattamente come progettato; hr.training.member.* è stato spinto a kamo-translation-dictionary prima e verificato sulla sua origine / principale prima di questo commit. npm test: 3 guardie pulite, 98 file, 1221 test di passaggio.
· DocsService
L'API di Formazione
Iscrizioni com.kamo.z.shared.hr.training in @EnableJpaRepositories — @EntityScan è ampio in modo che il le entità si trovano in entrambi i modi, ma i repository non lo sono. Due imbuti di autorizzazione, deliberatamente separati, che rispecchiano il modulo legale: TrainingAccess gates /api/docs/training/** su MANAGE TRAINING (RoleRightType 185) leggi e scrivi, che deve rimanere esattamente l'app/hr/hrTabs.ts giusta porta la scheda su. Un server più stretto rispetto al scheda lascia un membro in grado di aprire la scheda e raccogliere un 403 su una griglia vuota; più ampio, ed è un endpoint raggiungibile da qualcuno che l'UI non l'ha mai offerto. I diritti sono letti da member rights applied, mai from the token, perché la mappa della sessione di OTK non ha diritti. TrainingAssignmentOwnership autorizza la superficie del membro da IDENTITY. FormazioneMemberController quindi non detiene campo TrainingAccess affatto — un membro che ha preso un corso che è stato assegnato non ha L'HR ha ragione, e il routing attraverso l'imbuto delle risorse umane significherebbe bloccare ogni dipendente da la propria formazione o ampliamento del set di destra HR fino al catalogo e il pubblico roster aperto al Tutto org. La proprietà risponde 404, mai 403: un 403 conferma che l'id è reale e gira il endpoint in un oracolo per chi è stato assegnato cosa, e un record di valutazione non deve essere enumerabile da un pari. TrainingMemberSurfaceAuthTest pins tutte e tre le proprietà per riflessione, perché la modalità di guasto è qualcuno che aggiunge il campo in seguito "così il punto finale è assicurato troppo". GET /assegnazioni e GET /assegnazioni/conto in sospeso. Nessuna mappatura prende un membro o un org id — entrambi vengono dalla sessione. stato è un param RIPEATABLE, mai CSV: APIService consegna la sua URL a monte RestTemplate come String, che ri-encodes %2C a %252C, quindi una virgola arriva come un token letterale non corrispondere costante enum. Un filtro non riconosciuto significa "nessun filtro", mai "match nothing", quindi un stale bookmark mostra tutto piuttosto che una griglia vuota che legge come perdita di dati. The paged read è un repository DocsService-local sull'entità condivisa, ordinato (deliveredAt DESC, uid DESC) — paging su una chiave non unitaria duplicati e salta righe, e un'assegnazione di 200 membri scrive consegnatoAll'interno dello stesso millisecondo. in attesa-count restituisce la stessa forma JSON come il telaio spinto del distintivo in modo che un cliente parser serve Entrambi. Le righe sono mappate all'interno della transazione; corso e versione sono LAZY. 413 test passano.
· InitializerService
Creare lo schema HR Training
Iscrizioni com.kamo.z.shared.hr.training nella lista esplicita @EnableJpaRepositories — @EntityScan è ampia quindi le entità si trovano in entrambi i modi, ma i repository non sono, e senza questo i tavoli non sono mai creati. TrainingSchemaMigration (@Order(43); 42 è quella legale) aggiunge ciò che la mappatura dell'entità non può express. Cinque indici UNIQUE sono trattati come fatali perché ciascuno è una garanzia di correttezza nient'altro fornisce: - (course uid, version number): due pubblicazioni concorrenti avrebbero altrimenti menta lo stesso numero e ogni assegnazione incentrata su "versione 3" diventa ambiguo. - una attestazione per incarico: la guardia WORM blocca UPDATE e DELETE, non un duplicato INSERT. - l'indice di richiesta di notifica è l'idempotency dell'intero promemoria pipeline — il reclamo è un INSERT contro di esso, quindi senza di esso un re-mail di spazzamento retried ogni dipendente in ritardo di ogni inquilino. - una rettifica per (membro, corso, data dovuta); una riga di progresso per diapositiva. Il corso-item CHECK è scritto come un predicato a due colonne di proposito: SchemaCleanupRunner @Order(1) cade ogni singolo-colonna IN-list CHECK su ogni corsa, perché questa è la forma Ibernato emette per @Enumerated (STRING). CockroachDB/Yugabyte non hanno alcun ADD CONSTRAINT IF NON ESISTI per CHECKs, quindi "già esiste" viene catturato e trattato come successo. Il test è solo riflesso. mvn test in questo repo MIGRATES PRODUZIONE — DemoApplicationTests è un @SpringBoot Test senza profilo di prova e senza src/test/resources — così afferma SQL costanti contro il nome @Table di ogni entità senza avviare un contesto, che è il fallimento che realmente accade: un tavolo rinomina lasciando la mano-scritta SQL dietro.
· kamo-shared-library
Carica i documenti in streaming e aggiungi lo schema HR Training
ImageService acquisisce uploadDocumentStreaming, la controparte in streaming di uploadDocument. Il modulo buffered non può esprimere un grande upload affatto: readInputStream assegna nuovo byte[size], un array Java si apre a Integer. MAX VALUE e MinIOStorageService.upload poi assegna una copia completa SECOND per retry cross-node. Contro un contenitore 2Gi che ha fatto poche centinaia di megabyte il controsoffitto pratico — per questo motivo il tappo multiparte dell'immagine sedeva a 500MB mentre il percorso di connessione di chat, che già streams, corse a 3 GiB. Il nuovo metodo hashes una volta (entrambi digerisce da un buffer), sniffa il tipo MIME PRIMA del hash pass così un file 3 GiB maleslabelled è respinto senza spendere una lettura completa su di esso, e Delega lo storage a un nuovo negozioContentAddressed sovraccarico che accetta digestori pre-computati quindi la fonte viene letta due volte piuttosto che tre volte. Deliberatamente non @Transactional: memorizzazione gigabyte richiede minuti e una transazione tenuta attraverso di esso pins una connessione in piscina. Aggiunge anche com.kamo.z.shared.hr.training — 20 entità, 11 enum e 20 depositi per Modulo di formazione HR, specchio com.kamo.z.shared.hr.legal's conventions: BaseUuidEntity, an esplicito last modified con tocco() perché BaseUuidEntity.dateUpdated è morto, pinned colonnaDefinizione su ogni facoltativo @JoinColumn, e @OnDelete (CASCADE) sul pubblico FKs perché CascadeType.* non emette nulla al DDL. Due partenze dal piano legale, entrambi deliberati: - TrainingAssignmentStatus divide FAILED in FAILED RETAKE ALLOWED (aperto) e FAILED FINAL (chiuso), e REMINDABLE STATES è strettamente più stretto di OPEN STATES in modo da un membro in attesa il grading non è mai inseguito. Indicato da TrainingAssignmentStatusTest. - hr training notification è una tabella reale con un indice di rivendicazione UNICA piuttosto che un Redis SET NX, così "abbiamo detto loro in questa data" sopravvive a FLUSHDB ed è queryable. I repository di risposta e attestazione estendono il marcatore di repository nudo, non JpaRepository, quindi i metodi di cancellazione batch che bypass @PreRemove non esistono sull'interfaccia. Richiede un KamoInitializerService eseguito prima di qualsiasi servizio che legge queste tabelle viene distribuito.
· InitializerService
Creare gruppi billing pay groups esplicitamente, e annullare la collisione legacy
Crearlo in una migrazione piuttosto che lasciarlo a ddl-auto, che ha prodotto una tabella semi-costruita quando l'entità è stato indicato ai gruppi di fatturazione preesistente.
· EmailService
Dimensione ogni casella di posta contro l'archiviazione dell'organizzazione
Dovecot ora fa rispettare un limite per-mailbox, e nulla stava impostando la colonna legge. Questo lo scrive di notte, dopo l'istantanea di archiviazione in modo che la capacità sia divisa è la figura di oggi piuttosto che quella di ieri. Ogni casella postale ottiene una quota uguale di tutta la piscina. La proprietà che conta è che le azioni sommano alla capacità, così la posta non può mai da solo prendere organizzazione oltre il suo soffitto. È anche la versione che un proprietario può prevedere — "la vostra parte del deposito dell'organizzazione" — e non si muove quando un il dominio non relativo cresce, che dando ogni casella di posta la restante headroom sarebbe. Piani e organizzazioni illimitate senza abbonamento vivo scrivono 0, che il user query si trasforma in un contingent rule NULL: nessun limite. Quella direzione è deliberata, perché l'alternativa è rifiutare la posta per un'organizzazione le cui file di fatturazione sono mancanti o la metà della migrazione.
· KlusterServices
Fornire la quota di archiviazione sulla consegna in entrata e interrompere la spedizione della password DB
Lo storage di oggetti già rifiuta i contenuti che avrebbero preso un'organizzazione oltre la sua soffitto, che copre ogni dominio che attraversa il negozio di oggetti. posta in entrata era il gap: LMTP scrive direttamente nel maildir senza Java nel percorso, quindi Dovecot's quota plugin è l'unica cosa che può fermarlo. Il plugin carica globalmente in modo che la consegna LMTP è controllata — questo è il percorso che questioni — con imap quota aggiunto per i clienti che chiedono. Non c'è volutamente Qu rule predefinito: una regola globale si applicherebbe ad ogni casella di posta che l'utentedb fa non rispondere per, che è l'opposto di fallire al sicuro. La regola per utente da user query è l'unica fonte di un limite, ed è NULL fino a quando quota bytes è set, così ogni una delle 18 caselle di posta esistenti è illimitata esattamente come prima. virtual users.quota bytes è stato aggiunto prima di questo, di default a 0, perché il nuovi riferimenti user query: spedire la configurazione prima avrebbe fallito ogni ricerca utentedb e preso la posta auth verso il basso. Il colombacot.conf generato è stato convalidato con colombconf contro il live 2.3.20 prima di spingere; si risolve a "quota sieve" per LMTP e "quota imap quota" per IMAP. Separatamente, ConfigMap non ha più la password MySQL. E' stato spedito. testo chiaro, commesso qui e leggibile da `kubectl ottenere cm -o yaml`. The ConfigMap ora detiene un segnaposto e le credenziali provengono dall'esistente mysql-secret attraverso l'ambiente; il contenitore si rifiuta di iniziare se è vuoto piuttosto che eseguire con un userdb inutilizzabile. Nota questo modifica la coppia -semplice, che è ciò che CI applica e che cosa è in realtà Correre. tortocot-configmap.yaml e colombcot-deployment. yaml sono applicati da nulla e descrivere completamente un'immagine diversa.
· KlusterServices
Fornire la quota di archiviazione sulla consegna in entrata e interrompere la spedizione della password DB
Lo storage di oggetti già rifiuta i contenuti che avrebbero preso un'organizzazione oltre la sua soffitto, che copre ogni dominio che attraversa il negozio di oggetti. posta in entrata era il gap: LMTP scrive direttamente nel maildir senza Java nel percorso, quindi Dovecot's quota plugin è l'unica cosa che può fermarlo. Il plugin carica globalmente in modo che la consegna LMTP è controllata — questo è il percorso che questioni — con imap quota aggiunto per i clienti che chiedono. Non c'è volutamente Qu rule predefinito: una regola globale si applicherebbe ad ogni casella di posta che l'utentedb fa non rispondere per, che è l'opposto di fallire al sicuro. La regola per utente da user query è l'unica fonte di un limite, ed è NULL fino a quando quota bytes è set, così ogni una delle 18 caselle di posta esistenti è illimitata esattamente come prima. virtual users.quota bytes è stato aggiunto prima di questo, di default a 0, perché il nuovi riferimenti user query: spedire la configurazione prima avrebbe fallito ogni ricerca utentedb e preso la posta auth verso il basso. Il colombacot.conf generato è stato convalidato con colombconf contro il live 2.3.20 prima di spingere; si risolve a "quota sieve" per LMTP e "quota imap quota" per IMAP. Separatamente, ConfigMap non ha più la password MySQL. E' stato spedito. testo chiaro, commesso qui e leggibile da `kubectl ottenere cm -o yaml`. The ConfigMap ora detiene un segnaposto e le credenziali provengono dall'esistente mysql-secret attraverso l'ambiente; il contenitore si rifiuta di iniziare se è vuoto piuttosto che eseguire con un userdb inutilizzabile. Nota questo modifica la coppia -semplice, che è ciò che CI applica e che cosa è in realtà Correre. tortocot-configmap.yaml e colombcot-deployment. yaml sono applicati da nulla e descrivere completamente un'immagine diversa.
· kamo-internal
Lasciare un membro autorizzare il proprio posto dalla console
Il pannello che chiude il loop su self-pay. Sembra solo dove la politica dell'organizzazione lo permette, e solo per le persone che non stanno già comprando per conto dell'organizzazione — il server controlla la stessa politica, quindi nasconderlo è una cortesia piuttosto che il controllo. Dice chi viene fatturato prima di chiedere una carta, e sotto la modalità di approvazione dice chiaramente che nulla viene addebitato fino a quando il proprietario non accetta. Non leggere l'ammissibilità nasconde l'opzione piuttosto che mostrare uno che sarebbe rifiutato. Quando la politica non permette ai self-payers di scegliere un piano, prendono quello che l'organizzazione è su e il picker non è mostrato affatto — un proprietario delegare il costo di solito non delegare la decisione del prodotto.
· BillingService
Dite alle persone che il loro pagamento è fallito e ritirate l'abbonamento
L'abbraccio che non esisteva. fattura.payment failed ha scritto una riga e ha restituito: nessuna e-mail, nessuna bandiera, nessun cambiamento di stato e fatturazione.email-service-url è stato configurato e citato da nessun codice Java affatto. Il webhook ora inizia l'orologio della delinquenza sul primo fallimento e lo cancella nel momento in cui viene pagata una fattura, quindi un abbonamento recuperato inizia fresco piuttosto che portare un orologio stante nel prossimo fallimento. Solo il primo fallimento inizia esso — una riprovazione una settimana dopo non deve riavviare la grazia e consegnare un non pagante sottoscrizione un'altra settimana di servizio su ogni tentativo. La spazzata deliberatamente non decide quando i diritti si fermano. Succede l'orologio, in isLive, quindi non c'è nessuna finestra in cui un abbonamento a giri ancora funziona perché un lavoro non ha funzionato. Quello che la spazzata possiede è ciò che un orologio non può fare: dicendo così una volta, e la cancellazione dopo autoCancelAfterDays — trenta giorni da default, molto dopo il servizio si ferma. Quel divario è recuperabile di proposito, perché un cliente che aggiorna la loro carta il giorno venti ottiene tutto indietro pagando, e un abbonamento annullato è molto più difficile da annullare di quello sospeso. La notificazione è di grande aiuto dal design: Stripe retries a webhook che risponde non-2xx, e un servizio di posta non raggiungibile non è un motivo per rielaborazione di un pagamento evento.
· InitializerService
Aggiungere le colonne dell'orologio
past due since e suspension notified at, entrambi nullable. Null significa pagare e rispettivamente.
· kamo-shared-library
Fare un pagamento fallito significa qualcosa
PAST DUE ha contato come vivere pienamente in entrambi i risolutori di diritto, senza timestamp e nessun tappo, quindi un cliente la cui carta non è riuscita ha mantenuto ogni caratteristica per tutto il tempo Stripe ha lasciato l'abbonamento in tale stato — indefinitamente in pratica — e nessuno, cliente o operatore, è stato detto. Un abbonamento ora porta quando la raccolta è fallita e PAST DUE è in diretta solo nel suo periodo di grazia. Computato in lettura, esattamente come la scadenza di prova, così il servizio si ferma sull'orologio piuttosto che aspettare un lavoro da notare: a la decisione programmata lascerebbe una finestra in cui un abbonamento non registrato ha ancora funzionato perché la spazzata non era ancora scappata. Due scelte deliberate sull'errore. I default di Grace a una settimana piuttosto che zero, perché un pagamento fallito è di solito una carta scaduta e non una decisione Smettila di pagare. E una fila passata senza timbro legge come grazia interiore, perché ogni abbonamento che predating colonna ha uno — trattando null come lapsed sarebbe hanno tagliato i clienti nel momento in cui questo spedito, per una delinquenza che nessuno aveva misurato.
· BillingService
Lascia che un membro acquisti il proprio posto, e chiuda un foro di parametri del corpo
Il self-pay non aveva traccia all'interno della console. Tutto sulla superficie di fatturazione è a forma di account — è già necessario tenere una riga AccountMember prima di Piani & Billing mostra tutto, e l'unico modo per ottenere uno era un'azione proprietario — quindi un membro che voleva pagare per se stesso è stato detto di andare e chiedere. L'unico vero percorso self-pay che esisteva, il checkout del consumatore utilizzato dai prodotti white-label, è irraggiungibile dalla console perché il proxy di fatturazione ricostruisce intestazioni da graffia e non inoltra mai il cookie di cui ha bisogno il risolutore di sessione. L'acquirente è chiunque la sessione dice di essere. X-Member-Id è impostato dal proxy da *** e mai dal chiamante, così a differenza delle rotte del conto non c'è nel percorso di swap — che è l'intero motivo per cui questo è il suo percorso piuttosto di un altro gestore sotto /conti/{accountUid}. La politica dell'organizzazione è controllato lato server, quindi nascondere l'opzione altrove è più che decorazione, e il tappo di sicurezza per-payer è applicato qui piuttosto che fidato dal cliente. Chiude anche un foro cross-tenant su POST /accounts. Questa via non ha nessun conto nel suo PATH, quindi AccountOrgScopeInterceptor — quali chiavi su quel segmento — mai visto, e qualsiasi membro di qualsiasi organizzazione potrebbe nominare un'altra organizzazione account in BODY e riscrivi il suo contatto di fatturazione, reindirizzando le sue fatture e ricevute. Denominazione di un membro arbitrarioId ha fatto lo stesso dall'altra direzione. Entrambi sono ora tenuti a appartenere all'organizzazione del chiamante.
· kamo-internal
Lascia che il proprietario decida chi può pagare e gestisca i gruppi che fanno
Il proprietario aveva esattamente una leva sul pagamento di sé ed era la forma sbagliata: una irreversibile promuovere il gesto, senza alcun modo di dire in anticipo se era permesso e nessun modo di annullarlo — mentre un percorso di controllo del consumatore separato lascia un membro auto-iscrizione indipendentemente. Questo è il controllo preventivo, ed è ciò il resto della console si nasconde. Quattro modalità, etichettate da ciò che accade ad altre persone piuttosto che dall'enum: un il proprietario che sceglie una politica è decidere che cosa il loro personale può vedere, in modo che è quello che ciascuno l'opzione descrive. I gruppi appaiono solo una volta che la modalità consente loro, perché un gruppo è diverse persone che fanno quello che una persona può già fare. I non proprietari vedono l'impostazione di sola lettura piuttosto che non affatto. "L'organizzazione paga centralmente" è la risposta al perché non hanno opzioni di acquisto, e nascondersi lo trasforma solo in una domanda di supporto. Un insuccesso di caricamento di questa sezione lascia il lavoro del raccoglitore di piano — una politica console non può leggere non deve prendere la superficie di acquisto giù con esso.
· BillingService
Org policy e fatturazione gruppo API
Chi all'interno di un'organizzazione può pagare Kamo ora è responsabile e impostabile. Entrambi i percorsi sono org-scoped e quindi deliberatamente fuori /api/billing/accounts/** e il suo intercettore: "il mio personale compra i propri posti" deve essere rispondebile prima di un secondo account esiste, che è esattamente quando un account-scoped impostazione non può aiutare. Leggere la politica è aperta a qualsiasi membro, perché le superfici che nascondono I controlli self-pay hanno bisogno della risposta e non rivela nulla di attuabile. Tutto che i cambiamenti che pagano per ciò che è solo proprietario, controllati contro il database piuttosto che l'intestazione fornita dal chiamante. La creazione di gruppi rifiuta quando l'organizzazione non ha consentito gruppi — un gruppo è diverse persone che fanno quello che una persona può già fare, quindi un'organizzazione che non ha permesso l'auto-pagamento non ha permesso nemmeno questo. Sotto REQUIRES APPROVAL un gruppo inizia non approvato; ogni altra modalità ha già dato il permesso di coperta, così inizia approvato. Cancellare un gruppo scioglie il nome e mai il fatturazione: annullare ciò che paga è un atto separato e deliberato. Il roster proviene dalle righe dell'abbonamento Membri piuttosto che essere duplicati sul gruppo, quindi un gruppo non può essere in disaccordo con i sedili è pagare per.
· kamo-internal
Un Plans & console di fatturazione un proprietario può effettivamente trovare
Non c'era modo di acquistare un primo abbonamento ovunque nel prodotto. Elenchi dei piani erano raggiungibili solo attraverso un abbonamento che hai già tenuto, quindi un'organizzazione con nessuno ha visto un pannello vuoto e un accenno che indica un org diverso. Quello reale schermo di fatturazione vissuto sulla pagina del profilo del membro, archiviato sulle impostazioni atterraggio sotto una voce dal titolo "Personal", ed è stato nascosto interamente per qualsiasi org senza un genitore. La barra degli amministratori di casa non aveva alcuna voce di fatturazione. La console risponde alla domanda con cui arriva un proprietario — cosa siamo, cosa il costo, come lo cambio — e prezzi ogni controllo come lo spostano, quindi il totale è visibile prima che tutto sia commesso. Sedili, componenti aggiuntivi e stoccaggio blocca ogni aggiornamento di una figura in esecuzione, e un piano annuale afferma cosa realmente lascia il conto una volta all'anno piuttosto che lasciare un numero al mese per essere moltiplicato nella testa del lettore. L'aritmetico è planQuote, testato contro il percorso di carica, perché entrambi i posti la piattaforma precedentemente citato un prezzo in disaccordo con ciò che Stripe ha fatturato. Raggiungibile dalla barra home admin e dal menu delle impostazioni, gated sulla finanza diritti piuttosto che CONFIGURE SYSTEM: decidere cosa paga l'organizzazione è una decisione finanziaria.
· BillingService
Predefinire l'obiettivo di un nuovo abbonamento
Un primo acquisto non ha abbonamento esistente per copiare targetOrganizationId da, e richiederlo incondizionatamente è quello che ha creato uno dalla console impossibile. Un'organizzazione che acquista posti per sé è il caso comune; il la disposizione sub-org lo passa ancora esplicitamente.
· kamo-internal
Mostra il soffitto, non solo i byte
Il pannello riportato memorizzato, aggiunto, rimosso e a vita byte contro no riferimento a tutti — quindi un proprietario che guarda 400 GB non aveva modo di dire se questo era comodo o circa rifiutare il loro prossimo caricamento, e la loro prima conoscenza di un problema sarebbe stato fallito. L'assegno del piano e i blocchi comprato sono ciò che trasformano quei numeri in una posizione, quindi la barra di capacità va prima e tutto ciò che sotto legge contro di esso. Il bar rimane verde fino a quando non conta: ambra al 90%, rosso al massimo. Un bar che gira ambra al 60% insegna alle persone a ignorarlo. Dice anche chiaramente che le letture non sono influenzate. "Storage is full" legge come "I hanno perso l'accesso ai miei file" a meno che tu non dica il contrario, e l'unica cosa riempire il vostro deposito non fa mai è prendere nulla via. Le dimensioni e il prezzo del blocco provengono dal catalogo piuttosto che essere codificati, quindi il "add 10 GB per $2.50 al mese" linea non può derivare da ciò che è effettivamente addebitato.
· SecurityService
Promozione cancello sulla politica del proprietario, renderlo idempotent, aggiungere il suo inverso
Promuovere un membro a pagare per se stesso era incondizionato, a senso unico e non sicuro per ripetere. Ora si rifiuta quando la politica dell'organizzazione dice che l'organizzazione paga per tutti — in precedenza l'unico controllo sul pagamento di sé era l'irreversibilità del gesto stesso. E' idemponte. Promuovere lo stesso membro due volte inserito un secondo account e una seconda riga AccountMember, tripping l'unico su member id e rispondere a 500; un doppio clic, una riprovazione dopo un blip di rete, o un proprietario che re-running il flusso per spostare più licenze tutti colpito. Ora riutilizza l'account e sottoscrive l'abbonamento il membro ha già e prosegue il trasferimento. demoteBillingOwner è l'inverso che non esisteva mai. Un proprietario che ha promosso qualcuno non potrebbe annullarlo, e se quel membro se ne fosse andato, loro personalmente Conto tenuto in possesso di posti di destinazione dell'organizzazione senza modo di reclamare loro. Le licenze tornano all'abbonamento dell'organizzazione e l'auto-payer è in piedi; il loro Account e i metodi di pagamento sono lasciati soli, perché il recupero dei posti a cui l'organizzazione dipende è l'attività del proprietario e cancellare la storia della fatturazione di qualcuno non lo e'. La corrispondenza è sull'Utente condiviso piuttosto che il membro id, dal momento che la promozione rispecchia il membro sul livello superiore org. Corregge anche il cricchetto endpoint, che era ALREADY FAILING sul principale prima di questo lavoro — così la costruzione di SecurityService era rossa e nulla stava dispiegando. La scansione non poteva vedere due guardie reali: ExtractUserId legge userID fuori dal *** sessione e richiedonoTopLevelOwner passa attraverso CallerSessionResolver e più richiede che il chiamante possieda l'org di livello superiore. Insegnare sia lasciare 25 stallo le voci di base vengono eliminate, che è esattamente ciò che il file chiede — fissando un endpoint significa rimuovere la sua linea, e questo è ciò che fa cadere il numero. runSweep è basato con la sua vera ragione: interno-terra custodito attraverso un parametro intestazione il corpo-scan non può raggiungere.
· InitializerService
Gettare il vincolo che ha limitato un membro a un account di fatturazione
Trova il vincolo per forma piuttosto che per nome — Hibernate generato e il nome non è stabile in ambienti, quindi qualsiasi vincolo unico su account members il cui elenco di colonne è esattamente member id è quello di cadere. The composito su (account uid, member id) ha due colonne e non corrisponde mai.
· kamo-shared-library
Lasciate che i membri e i gruppi paghino da soli, secondo le regole del proprietario
Mancavano tre cose e uno era attivamente dannoso. account members ha portato un unico globale su member id — un membro potrebbe amministrare esattamente UN conto di fatturazione piattaforma su tutto. Questo è il soffitto che ha fatto ogni accordo multi-payer impossibile, e già stava facendo danni piuttosto che limitando semplicemente: AccountMemberService.add() lavorato intorno ad esso in silenzio SCONTO della riga del membro sul loro conto precedente, in modo da concedere l'accesso di fatturazione a uno revocato da un altro senza preavviso; e promuovereToBillingOwner lanciato una violazione di vincolo al secondo giro, trasformando un doppio clic in un 500. The composito unico su (account uid, membro id) già esisteva ed esprime vera regola. Dropping fa un derivato trovareByMemberId di ritorno opzionale gettare il momento chiunque detiene due file, e nove chiamanti si affidano a esso. È ora una query legata ordinato dalla creazione, così quei chiamanti mantengono la risposta che avevano invece di a seconda di qualsiasi cosa il pianificatore ritorna prima; trovareAllByMemberId è lì per codice che li vuole tutti. OrgBillingPolicy è il controllo preventivo che il proprietario non ha mai avuto. Il 2026-04-16 design specificato billing delegation mode e l'implementazione ha abbandonato, lasciando un irreversibile promuovere il gesto e non dire in anticipo se l'autopagamento è stato permesso a tutti — mentre un percorso di controllo del consumatore separato I membri si sottoscrivono comunque. Dove una modalità vieta qualcosa che le superfici nascondono piuttosto che offrirlo e fallire. BillingGroup nomina un accordo il modello già permesso: AccountSubscription è unico su (account, mercato, target org), quindi diversi account potrebbero sempre bersaglio di un'organizzazione. Essa non ha alcun roster proprio — che detiene un posto è AbbonamentoMember plus AccountLicense, la stessa risposta di chiunque altro, e un il secondo roster sarebbe una seconda verità da tenere in passo.
· SecurityService
Esporre l'impatto non aperto all'operatore
La pagina del catalogo app risponde già alla domanda org-indipendente — fa qualsiasi il piano menziona questa applicazione — e solleva un avvertimento quando nessuno lo fa. Questo risponde inquilino-by-tenant uno dietro di esso, che è quello che effettivamente cancelli ribaltare la valvola.

Il tuo universo ti sta aspettando.
Aggiungi i prodotti quando sei pronto: nessuna migrazione, nessun nuovo login, nessuna richiesta di pagamento.
Gratis per sempre — nessuna carta di credito richiesta