Reply Pilot Database
reply-pilot-db je samostatny PostgreSQL modul pro Reply Pilot. Nevystavuje
REST/JSON API; vystavuje samotny PostgreSQL server na sdilene Docker siti
reply-pilot-internal a na host bind ${HOST_DB_BIND}:${HOST_DB_PORT}.
Co modul resi
- persistentni ulozeni databazovych souboru na host path mimo kontejner
- PostgreSQL konfiguraci pres
conf/postgresql.conf - logovani vyhradne na
stderr, sbirane container runtime - verzovane Liquibase changelogy pres
scripts/db-migrate.sh - automaticke spusteni Liquibase
validatea expand-onlyupdatepristartadeploy - reconcile samostatneho readonly loginu
reply_pilot_backuppro logical backup service
Kompletni backup/restore runtime kontrakt je v Reply Pilot Database Backup.
Backend inicializuje kazde fyzicke spojeni sveho Hikari poolu prikazem
SET jit = off. Kratke interaktivni dotazy jinak mohou stravit sekundy JIT
kompilaci. Nastaveni plati jen pro backendove session, ne pro cely PostgreSQL
server. Task/company visibility dotazy pouzivaji mnoziny ID misto korelovanych
email-resolution poddotazu, aby planner mohl filtrovat tasky pred detailnimi
joiny. Pravidla pristupu a kanonicke email/company vazby zustavaji stejna.
Persistencni host path
Nejdulesitejsi parametr je HOST_DATA_DIR v .env.local nebo .env.server.
Tahle cesta urcuje, kam na hostu fyzicky spadnou PostgreSQL data. Proto data
preziji restart kontejneru i serveru.
Uvnitř kontejneru se PostgreSQL cluster inicializuje do PGDATA=/app/data/postgresql.
To je schvalne podadresar bind mountu, protoze oficialni PostgreSQL image
nepodporuje initdb primo do rootu mountpointu.
Migrace
Liquibase pouziva vlastni tabulky:
public.databasechangelogpublic.databasechangeloglock
Pri prvnim spusteni nad starsi databazi se puvodni public.schema_migrations
automaticky prevede do Liquibase historie, aby se uz aplikovane zmeny
nepoustely znovu. Nasledny changeset 0004 stary tracking table smaze.
Aktualni user a aplikacni konfiguracni tabulky jsou:
public.app_userpublic.app_user_jira_profilepublic.app_user_google_calendar_profilepublic.app_skillpublic.app_user_skillpublic.app_permissionpublic.app_rolepublic.app_role_permissionpublic.app_user_rolepublic.app_configuration
Tabulka app_user ma zatim:
id BIGINT GENERATED BY DEFAULT AS IDENTITYlogin_key TEXT NOT NULLauth_user_id TEXT NOT NULL DEFAULT ''email TEXT NULLdisplay_name TEXT NOT NULL DEFAULT ''gmal_box_link TEXT NOT NULL DEFAULT ''chart_color VARCHAR(7) NOT NULLve formatu#RRGGBB, s nahodnym vychozim odstinem pro nove i pri migraci existujici uzivatelecreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
Migrace 0092 pridava app_user_mcp_token pro osobni MCP pristup:
id,app_user_id(FK s cascade pri smazani uzivatele),nametoken_typeomezeny napippaneboothertoken_hash(unikatni SHA-256 hex) atoken_ciphertext(verzovany AES-256-GCM envelope)expires_on(nullable DATE),created_at,last_used_at,revoked_at
Original tokenu se v DB neuklada jako plaintext. Envelope je svazany s vlastnikem
a hashem; klic MCP_TOKEN_ENCRYPTION_KEY vlastni BE SOPS env mimo databazi.
Hash slouzi stejne autentizaci obou typu. Pippa desifruje original nejnovejsiho
aktivniho tokenu typu pippa a posila jej na /mcp. Datum expirace plati vcetne
zvoleneho dne v APP_TIMEZONE; NULL nema expiraci. Revokace zachova metadata.
Migrace take pridava mcp.access pro role admin, sales, sales_int a loader.
Rollback 0092 odstrani tabulku vcetne tokenu a toto opravneni; overuje se na
izolovane DB, ne nad uzivatelskymi credentialy. Viz MCP.
Migrace 0094 pridava mcp_email_send pro trvale blokovani opakovaneho odeslani
pres MCP. Primarni klic (app_user_id, operation_key) serializuje soubezne pozadavky;
company_id a SHA-256 request_hash zabrani pouziti stejneho klice pro jiny obsah.
result (JSONB) uklada prubezny/potvrzeny vysledek vcetne znamych Gmail/Jira ID,
prijemcu a predmetu, nikoli kopii tela nebo prilozenych souboru. Casove sloupce jsou
created_at a updated_at. Claim se commitne pred volanim Gmail; nejasny stav nesmi
automaticky vyprset ani uvolnit opakovane odeslani. FK na uzivatele a spolecnost maji
ON DELETE RESTRICT; tabulka nema vazbu na mazatelnou historii Pippy. Rollback maze
evidenci deduplikace, proto se overuje jen na izolovane DB a v produkci nesmi probehnout
za aktivniho emailoveho nastroje ani ztratit audit jiz provedenych operaci.
Activity/contact schema a kanonicke vazby emailovych vlaken tento zurnal nemeni.
Tabulka app_user_jira_profile drzi lokalni Jira assignment nastaveni pro
aplikacniho uzivatele:
app_user_id BIGINT PRIMARY KEY REFERENCES public.app_user (id) ON DELETE CASCADEjira_account_id TEXT NULLjira_email TEXT NULLjira_assignable BOOLEAN NOT NULL DEFAULT TRUEjira_oauth_credentials_ciphertext TEXT NULLcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
jira_account_id je stabilni Atlassian identifikator uzivatele a ma prednost
pri mapovani assignee z Jiry. Kdyz jira_email neni vyplneny, aplikace pro Jira
lookup pouzije app_user.email. Migrace zalozi vychozi profil pro existujici
uzivatele a aplikacni Jira assignee service pri nacitani seznamu zalozi chybejici
profily pro nove uzivatele s jira_assignable = TRUE.
jira_oauth_credentials_ciphertext je volitelny verzovany credential envelope
pro delegovane Jira komentare. Backend ho uklada sifrovany pres AES-256-GCM;
obsahuje access token, aktualni rotating refresh token, expiraci, scopes, Jira
cloud ID a overenou Atlassian identitu. Plaintext token se do DB neuklada.
Sifrovaci klic je runtime secret backendu a neni soucasti databaze.
Tabulka app_user_google_calendar_profile drzi jeden delegovany Google
Calendar grant pro konkretniho aplikacniho uzivatele:
app_user_id BIGINT PRIMARY KEY REFERENCES public.app_user (id) ON DELETE CASCADEgoogle_subject TEXT NOT NULLgoogle_email TEXT NOT NULLgoogle_display_name TEXT NOT NULL DEFAULT ''google_oauth_credentials_ciphertext TEXT NOT NULLcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
google_subject a google_email jsou overene pres Google OpenID Connect.
Credential envelope obsahuje access token, refresh token, expiraci a scopes.
reply-pilot-google jej sifruje samostatnym AES-256-GCM klicem a backend
persistuje pouze opaque vysledek. Plaintext token ani sifrovaci klic se do
databaze ani backend runtime nedostanou. Akce „Odpojit Google Calendar“ nejprve
pres Google modul odvola grant u Google a potom backend cely profilovy radek
smaze.
Tabulka app_user_email_signature drzi volitelny HTML podpis pro odchozi
emailove odpovedi konkretniho aplikacniho uzivatele:
app_user_id BIGINT PRIMARY KEY REFERENCES public.app_user (id) ON DELETE CASCADEbody_html TEXT NOT NULLbody_plain_text TEXT NOT NULL DEFAULT ''created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
Podpis je volitelny; chybejici radek znamena, ze uzivatel nema nastaveny podpis. Aplikace uklada HTML podpis oddelene od markdown textu odpovedi a pri vytvareni Gmail draftu nebo primem odeslani jej pripoji az po renderovani odpovedi do HTML.
Tabulka app_configuration drzi singleton konfiguraci aplikace:
id SMALLINT PRIMARY KEY DEFAULT 1default_email_sales_user_id BIGINT NULL REFERENCES public.app_user (id) ON DELETE SET NULLcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
default_email_sales_user_id je globalni fallback obchodnik pro emailovou
komunikaci, kdyz konkretni firma nema nastaveny vlastni
party_organization.default_email_sales_user_id.
Pracovni dovednosti uzivatelu jsou oddelene od RBAC opravneni:
app_skill(code TEXT PRIMARY KEY, label TEXT, description TEXT, active BOOLEAN, sort_order INTEGER)app_user_skill(app_user_id BIGINT REFERENCES app_user(id) ON DELETE CASCADE, skill_code TEXT REFERENCES app_skill(code) ON DELETE RESTRICT)
Primarni klic app_user_skill (app_user_id, skill_code) dovoluje kazdemu
uzivateli libovolnou kombinaci dovednosti a brani jen duplicitnimu prirazeni
stejne dovednosti. Kod organize_meeting_caller oznacuje uzivatele, kterym lze
nově priradit pozadavek Organize a Meeting; kod supplier_meeting_host
oznacuje uzivatele, jejichz pripojeny Google Calendar lze nabidnout pro schuzku
s dodavatelem. Kod sync_inbox povoluje synchronizaci pripojene osobni Gmail
schranky a nema zadne vychozi prirazeni; pro stavajici i nove uzivatele je tedy
ve vychozim stavu vypnuty. Odebrani dovednosti nerusi existujici tasky ani
udalosti, ale blokuje nove prirazeni. Changeset 0051 pri nasazeni priradi obe
puvodni dovednosti vsem existujicim uzivatelum, aby zachoval dosavadni chovani;
novy uzivatel nema dovednost, dokud mu ji spravce explicitne nepriradi.
RBAC tabulky drzi aplikacni role a opravneni nad stabilni identitou
app_user.id:
app_permission(code TEXT PRIMARY KEY, label TEXT, description TEXT)app_role(id BIGINT GENERATED BY DEFAULT AS IDENTITY, code TEXT UNIQUE, label TEXT, description TEXT, system_role BOOLEAN, active BOOLEAN, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ)app_role_permission(role_id BIGINT REFERENCES app_role(id) ON DELETE CASCADE, permission_code TEXT REFERENCES app_permission(code) ON DELETE CASCADE)app_user_role(app_user_id BIGINT REFERENCES app_user(id) ON DELETE CASCADE, role_id BIGINT REFERENCES app_role(id) ON DELETE CASCADE)app_user_impersonation_audit(id BIGINT GENERATED BY DEFAULT AS IDENTITY, real_app_user_id BIGINT REFERENCES app_user(id) ON DELETE CASCADE, target_app_user_id BIGINT REFERENCES app_user(id) ON DELETE CASCADE, started_at TIMESTAMPTZ, ip_address TEXT, user_agent TEXT)
Kanonicky katalog opravneni a seedovanych roli je v Permission Catalog.
RBAC migrace seeduji opravneni:
company.view_assignedcompany.view_allcompany.writecompany.write_assignedcompany.mergesecurity.manage_rolessecurity.impersonate_usertask.supplier_onboarding.ask_questionuser.manage_skills
Seedovane system role jsou:
adminse vsemi opravnenimi kromesecurity.impersonate_usersalesscompany.view_assignedacompany.write_assignedsales_intscompany.view_assigned,company.view_all,company.write,company.write_assignedacompany.mergeloaderscompany.view_all,company.writeatask.supplier_onboarding.ask_questionsupport_impersonatorssecurity.impersonate_userauser.manage_skills
Prirazeni konkretniho admina se nedela automaticky migraci. Musi probehnout
explicitnim SQL/script krokem nad existujicim app_user.id.
security.impersonate_user se nepridava automaticky do role admin. Ma se
prirazovat explicitne pres roli support_impersonator, protoze umoznuje
docasne pracovat jako jiny povoleny uzivatel. Start impersonace se zapisuje do
app_user_impersonation_audit. V1 nepovoluje impersonovat uzivatele s
security.manage_roles ani uzivatele se security.impersonate_user.
V1 autorizacni pravidlo pro firmy je:
- uzivatel s
company.view_allmuze videt vsechny firmy v povolenych read cestach - uzivatel bez
company.view_all, ale scompany.view_assigned, vidi jen firmy, kdeparty_organization.default_email_sales_user_id = app_user.id - firmy s
party_organization.default_email_sales_user_id IS NULLjsou ve V1 viditelne jen procompany.view_all party_organization.show_by_defaultzustava jen display filtr uvnitr autorizovaneho rozsahu, neni to autorizace- zapisove company/task endpointy pro existujici firmu prijimaji
company.writenebocompany.write_assigneda zaroven kontroluji stejny company visibility scope; rolesalestedy muze menit jen firmy a navazane tasky, kde je uzivatel vychozi osoba pro emailovou komunikaci - backend read-model company/task endpointy maji uzkou read-only vyjimku pro
libovolny aktivni Jira task: lokalni assignee nebo Jira assignee ulozeny v
task_jira/app_user_jira_profilemuze cist jen firmu napojenou na tento aktivni task a samotny aktivni task; vyjimka neplati pro zapis, nesouvisejici firmy, neprirazene tasky aniDonetasky; rozsirena firemni spoluprace zustava omezena na aktivni prirazeneReply Pilot Task - zalozeni nove firmy prijima
company.writenebocompany.write_assigned; backend nove firme vzdy nastaviparty_organization.default_email_sales_user_id = app_user.idaktualniho uzivatele, takze scoped zapis vytvari jen self-assigned firmu
Dalsi migrace zavedly:
public.partypublic.party_personpublic.party_person_name_legacypublic.party_organizationpublic.vat_jurisdictionpublic.party_vat_registrationpublic.party_outreach_policypublic.contact_mechpublic.contact_mech_emailpublic.contact_mech_phonepublic.party_contact_mechpublic.party_relationshippublic.activity_typepublic.activitypublic.activity_emailpublic.activity_email_attachmentpublic.activity_email_linkpublic.activity_callpublic.activity_meetingpublic.activity_notepublic.activity_participantpublic.task_typepublic.taskpublic.task_jirapublic.task_jira_email_threadpublic.task_jira_work_typepublic.task_jira_supplier_onboardingpublic.task_jira_email_thread_replypublic.task_jira_reply_pilot_taskpublic.task_jira_organize_meetingpublic.work_wizard_task_snoozepublic.jira_sync_statepublic.ai_prompt_typepublic.ai_promptpublic.lead_import_batchpublic.lead_import_itempublic.requirement_typepublic.requirement_state_typepublic.requirement_evidence_source_typepublic.requirement_evidence_verdict_typepublic.party_requirement_statepublic.party_requirement_evidencepublic.party_requirement_eval_queuepublic.party_supplier_identifier_fact
Legacy tabulky public.company, public.company_contact a
public.communication_history byly po migraci na party/activity model
odstranene changesetem 0010.
IČO identifier kontrakt
Company create/edit, lead import a CME synchronizace pouzivaji stejny backendovy
trust boundary pro normalizaci a vlastnictvi ceskeho ICO. Normalizace odstrani
vsechny whitespace znaky; nova nebo zmenena hodnota musi obsahovat presne osm
ASCII cislic. Rucni create/edit vyzaduje ICO pouze pro zemi registrace CZ a
CME synchronizace ho vyzaduje vzdy; lead import muze mit ICO prazdne.
Zahranicni firemni identifikatory pouzivaji party_identifier.
Aktualni target radek je
party_identifier(identifier_type_code = 'COMPANY_REGISTRATION_NUMBER',
value_normalized = <normalizovane ICO>). Stavajici unikatni omezeni
(identifier_type_code, value_normalized) zajistuje jednoho vlastnika hodnoty.
Formatovy check povoluje pro tento typ jen osm ASCII cislic a partial unique
index dovoluje nejvyse jeden tento identifikator na party.
Kod COMPANY_REGISTRATION_NUMBER je vyhrazeny pro ceske ICO a nesmi obsahovat
zahranicni rejstrikove nebo danove identifikatory. Overeny zahranicni zapis v
obchodnim rejstriku pouziva identifier_type_code =
'COMMERCIAL_REGISTER_NUMBER'; value_raw zachovava puvodni lidsky zapis a
value_normalized obsahuje zemi, vydavajici rejstrik, oddil a cislo ve formatu
<ISO zeme>:<REJSTRIK>:<ODDIL>:<CISLO>. Tohle issuer scope je nutne, protoze
samotne zahranicni cislo nemusi byt globalne jedinecne.
Zeme registrace firmy je ulozena samostatne v
party_organization.registration_country_code. Prazdny retezec znamena
historicky neovereny zaznam; aplikace zemi neodvozuje z osmimistneho cisla,
protoze ceske a slovenske ICO maji stejny tvar. Rucni create/edit podporuje
CZ, SK, PL a DE. Ceske ICO se uklada do kanonickeho identifieru,
zatimco slovenske ICO, polske KRS/REGON/NIP a nemecky Handelsregister se ukladaji do
party_identifier s country/scheme prefixem v value_normalized. Editace firmy
uklada zemi a tyto oficialni identifikatory v jedne transakci; zmena zeme
nahradi predchozi podporovane zahranicni registracni identifikatory, ale ponecha
obecne identifikatory jako domeny, weby, GLN a vendor kody.
Create a skutecna zmena ICO udrzuji v jedne transakci pouze kanonicky identifier.
Autoritativni narok nepouziva ON CONFLICT DO NOTHING; cizi vlastnik nebo
soubezny vitez zpusobi rollback cele mutace a field-level konflikt. Update pred
porovnanim zamkne organizacni radek. Neoverene historicke hodnoty jsou oddelene
pod LEGACY_UNRESOLVED_COMPANY_REGISTRATION_NUMBER a nevstupuji do matching,
ownership ani search indexu.
Lead import a CME mohou platnym ICO doplnit existujici firmu pouze tehdy, kdyz
jeji kanonicky identifier je prazdny nebo stejny. Cizi vlastnik nebo jine
autoritativni ICO zpusobi chybu bez prepisu. Lead ponecha puvodni JSON v
lead_import_item.source_record_json a existujici service flow zapise
last_error. CME ponecha puvodni hodnotu v party_cme_source, zapise
last_error a pres row savepoint pokracuje dalsim zaznamem. Lead matching
pouziva target party_identifier.
CME matching pouziva vyhradne povinne platne ICO z party_identifier. Nula
kandidatu vytvori novou firmu, jeden kandidat se propoji a dva nebo vice
kandidatu skonci chybou bez vytvoreni nebo propojeni. Prazdne nebo malformed ICO
je row error. DIC, nazev ani predchozi party_cme_source.party_id se jako
fallback nepouzivaji.
Company merge pred presunem vazeb kontroluje target identifikatory obou zamknutych firem. Nula nebo jedna ruzna autoritativni hodnota je deterministicka a pri explicitnim merge zustane na cilove firme; dve ruzna neprazdna autoritativni ICO zpusobi rollback celeho merge. Nesluceny zdrojovy organization radek ani importni auditni zaznamy se pri konfliktu nemeni.
Read modely, search, importy, CME matching a merge pouzivaji pouze
party_identifier. Expand changeset 0058 po archivaci unresolved hodnot
docasne udrzoval stary organizacni sloupec DB triggerem pro jedno rollback
okno. Contract changeset 0059 byl v produkci explicitne aplikovan
2026-08-20; trigger, jeho funkce i
party_organization.company_registration_number uz neexistuji. Standardni
!contract deploy destruktivni changesety nadale preskakuje.
Changeset 0055 presouva prvni rucne overeny zahranicni rejstrikovy zaznam
firmy Gustav Gerster z docasneho ceskeho ICO sloupce do party_identifier:
raw hodnota HRA 640607 ma kanonickou hodnotu
DE:AMTSGERICHT_ULM:HRA:640607. Legacy hodnotu vycisti jen tehdy, kdyz se
identifikator ve stejne transakci skutecne vlozi; unique konflikt zpusobi
rollback bez ztraty zdrojove hodnoty. Dalsi malformed radky se nesmi migrovat
jen podle delky nebo prefixu. Kazdy musi mit overeny typ, zemi a vydavajici
autoritu.
Kanonicky scheme code pro ceske ICO zustava
COMPANY_REGISTRATION_NUMBER; zahranicni obchodni rejstrik je oddeleny typ s
issuer scope v normalizovane hodnote.
VAT registration expand kontrakt
Changeset 0066 pridava novy VAT model bez backfillu:
vat_jurisdiction(code VARCHAR(2) PRIMARY KEY, country_code CHAR(2), local_name TEXT)je verzovany katalog 27 clenskych statu EU a Severniho Irska podle European Commission VIES format table a prehled lokalnich nazvu VAT ID- jurisdikcni prefix
ELmapuje zemiGR,XImapujeGBa cesky lokalni nazev jeDIČ party_vat_registrationuklada raw i normalizovanou hodnotu, odkazuje napartypresON DELETE CASCADEa na katalog presON DELETE RESTRICTUNIQUE (party_id, vat_jurisdiction_code)povoluje nejvyse jednu registraci dane jurisdikce na party;value_normalizedma jen neunikatni index, protoze shodna VAT hodnota nesmi predstavovat globalni vlastnictvi firmy- DB checky vyzaduji neprazdny raw zapis, normalizovany tvar
^[A-Z]{2}[A-Z0-9]+$a shodu prefixu s jurisdikci
Backendova policy nejprve prevadi vstup na uppercase, potom odstrani pouze
whitespace, -, ., a /, a nakonec kontroluje jurisdikcni format. Jine znaky
se neztrati a validace je odmitne. Jde pouze o syntaktickou validaci; v tomto
slici se nevola live VIES. Od itemu 34.7 je party_vat_registration
autoritativni runtime zdroj. Rucni create/edit nahrazuji kolekci, lead a CME
mohou pridat registraci sve jurisdikce a merge pracuje pouze s kolekci. Shodna
normalizovana VAT hodnota muze byt na vice party a nepouziva se pro automaticky
lead/CME match ani ownership. Obecne identifikatorove flow uz nezapisuje
party_identifier.TAX_IDENTIFIER. Jednorazovy vat-registration-backfill z
itemu 34.3 doplnil kolekci z historickeho organization scalaru; po dokonceni
cutoveru byl v itemu 34.10 odstranen vcetne CLI.
Contract changeset 0068 je samostatny data-cleanup pro legacy
party_identifier.TAX_IDENTIFIER. Pred smazanim ulozi vsechny puvodni sloupce
do JSONB snapshotu v party_tax_identifier_cleanup_audit a zaznamena pre/post
count. Explicitni rollback obnovi puvodni ID, party vazbu, raw/normalized
hodnotu i created_at a auditni tabulku odstrani. Standardni !contract update
changeset preskoci. Item 34.6 splnil produkcni podminky zero-consumer auditu,
overeni ocekavaneho poctu a recovery testu cerstve zalohy: zaloha
reply-pilot-20260821T105254Z.sql.gz byla obnovena do izolovane databaze,
cileny update aplikoval pouze 0068 a produkcni audit obsahuje snapshot vsech
318 radku s poctem 318 -> 0. Po rucnim company save i naplanovanych CME jobech
zustava legacy pocet nula. party_organization.tax_identifier nebyl odstranen;
zustava 1 138 neprazdnych scalaru a 1 067 VAT registraci. Starsi contract
changesety 0053 a 0054 zustaly neaplikovane.
Item 34.10 odstranil posledni scalar JSON/form/DTO adaptery a vsechny runtime
ctenare i writery organization scalaru. Samostatny contract changeset 0069
pred dropem odmita legacy party_identifier.TAX_IDENTIFIER, registrace na
merged parties a kazdy syntakticky platny scalar, ktery nema shodnou
normalizovanou registraci na efektivni party. Neplatne historicke scalars lze
zahodit. Rollback sloupec obnovi a doplni value_raw pouze party s prave jednou
registraci; vice registraci nelze bezeztratove znovu zredukovat na scalar.
Standardni !contract update changeset nadale preskakuje.
Item 34.11 aplikoval v produkci pouze 0069 po zero-consumer auditu a recovery
testu cerstve zalohy reply-pilot-20260821T130020Z.sql.gz o velikosti
18 069 571 bajtu a SHA-256
c270e205d0ad561c7302f9345c5bc2eb2297176f7be6a79d6ff93ddafedd1a42.
Obnovena databaze zachovala 4 929 parties, 3 895 organizations, 1 138
neprazdnych scalaru a 1 068 VAT registraci; cilene update/rollback/update proslo
a rollback z kolekce rekonstruoval 1 068 single-registration scalaru. Produkcni
apply zachoval pocty 4929 / 3895 / 1068, odstranil
party_organization.tax_identifier a ponechal nesouvisejici contracty 0053
a 0054 neaplikovane. Legacy TAX_IDENTIFIER i registrace na merged parties
zustavaji na nule. party_cme_source.tax_identifier je zdrojovy snapshot a
changeset ho nemeni.
Cleanup neplatnych sdilenych e-mailovych domen
Changeset 0070 pred zavedenim kanonickeho domenoveho matchingu odstrani
existujici party_identifier.DOMAIN hodnoty googlemail.com, volny.cz a
atlas.cz, ktere produkcni analyza RP-4121 potvrdila jako sdilene domeny
poskytovatelu, ne domeny vlastnene firmou. Nejde o runtime blacklist: aplikace
podle tohoto seznamu nefiltruje prichozi e-maily ani nove rucne zadane hodnoty.
Pred smazanim changeset ulozi presny JSONB snapshot vsech dotcenych radku a
pre/post pocty do party_domain_cleanup_audit. Rollback obnovi puvodni ID,
party vazbu, raw/normalizovanou hodnotu i created_at a auditni tabulku odstrani.
Nove DOMAIN hodnoty mohou vzniknout jen explicitni rucni mutaci identifikatoru;
email ingest ani vytvoreni tasku domenu neodvozuje a firmu z ni nezaklada.
Changeset 0071 pridava kanonicky view
public.email_thread_address_company_resolution. Pro kazdou normalizovanou
externi adresu a (provider, external_thread_id) vraci nula az vice firem podle
presneho aktivniho firemniho e-mailu, presneho aktivniho e-mailu osoby s
aktualnim CONTACT_FOR, nebo rucne zadane presne DOMAIN. Neprirazena adresa
ma ve view jeden radek s party_id IS NULL. View neuklada materializovanou
vazbu vlakno-firma a nepouziva text nazvu firmy ani participant party_id jako
samostatny matching signal.
Changeset 0072 pridava
public.email_thread_assignment_review(provider, external_thread_id,
reviewed_by_user_id, reviewed_at). Primarni klic je dvojice provider/thread;
radek pouze skryje konkretni stale neprirazene vlakno z manualni fronty. Stejna
adresa v jinem novem vlakne zustane viditelna. Changeset 0073 pridava
opravneni email.inbox.review a seeduje ho rolim admin a sales_int.
Changeset 0078 pridava opravneni
task.email_thread_reply.unlinked roli sales_int. Changeset 0079 pred
cleanupem ulozi pro kazdy Email Thread Reply task puvodni task_jira.party_id,
aktualni pole kanonicky odvozenych firem a priznak neshody do
email_thread_reply_task_company_audit. Souhrn nula/jedna/vice firem, pocet
neshod a detailni CSV se kontroluje prikazem:
./scripts/db-email-reply-task-company-audit.sh \
> /tmp/rp-3955-email-reply-task-company-audit.csv
Contract changeset 0080 nastavi task_jira.party_id na NULL pro vsechny
email_thread_reply tasky. Pred zmenou skonci chybou, pokud je audit pro
kterykoli mazany odkaz chybejici nebo zastaraly; rollback puvodni hodnoty obnovi
z auditni tabulky. Standardni !contract deploy cleanup neaplikuje.
Pred contract changesetem 0054 lze opakovatelny CSV dry-run spustit takto:
./reply-pilot-db/scripts/db-email-company-reconciliation.sh \
https://reply-pilot.example.test \
> /tmp/rp-4121-email-company-reconciliation.csv
Report porovna historicke linked eventy, aktualni legacy linky a kanonicky
resolver po vlakne, uvede zero/one/many kardinalitu, neprirazene adresy a odkazy
na vlakno i firmy. Sloupec comparison prednostne porovnava kanonicky vysledek
s aktualni legacy link tabulkou. Je read-only. Pokud uz databaze aplikovala
0054, report se stale vytvori, ale historical_event_table_available a
active_legacy_table_available jsou false a comparison ma hodnotu
baseline_unavailable; takovy vystup sam o sobe historickou paritu
neprokazuje.
Treti changeset pridava:
public.simple_auth_nonce
Tuto tabulku pouziva reply-pilot-app pro replay protection simple-auth
callbacku. Uklada pouzite rp_nonce a jejich expiraci.
Jedenacty changeset pridava task model:
public.task_typepublic.taskpublic.task_jirapublic.task_jira_email_threadpublic.task_jira_work_typepublic.task_jira_supplier_onboardingpublic.task_jira_email_thread_reply
task je obecny root zaznam podobny activity. Aktualne ma jediny typ jira,
ulozeny v task_type.
task_jira drzi Jira-specific pole a lokalni vazby:
jira_keystatusparty_iduser_idresolutionjira_work_type_codejira_summaryjira_description_text
jira_work_type_code rozlisuje lokalni cache pro Jira work item typu
supplier_onboarding nebo email_thread_reply. Jira zustava source of truth
pro workflow status, lokalni DB drzi work-type klasifikaci, vazby potrebne pro
aplikacni workflow a plain-text cache Jira summary/description pro read modely
a search. jira_description_text je textovy obsah, ne Jira ADF JSON.
task_jira_supplier_onboarding je subtype tabulka pro supplier onboarding
ticket. Vazba na firmu je pres task_jira.party_id; onboarding-only metadata
feed_state_code a enlistment_table jsou ulozena tady, ne v base
task_jira.
task_jira_email_thread_reply je subtype tabulka pro email-thread reply ticket.
Task nema vlastni vazbu na firmu: task_jira.party_id je pro tento work type
vzdy NULL. Aktualni mnozina nula, jedne nebo vice firem se odvozuje pres
external_thread_id z email_thread_address_company_resolution; zmena
kanonickeho rozliseni se proto okamzite projevi v zobrazeni i autorizaci.
Historicke odkazy na firmu v Jira description se neparsuji ani nepouzivaji pro
autorizaci.
task_jira_email_thread je historicka tabulka z puvodniho modelu. Novy kod pro
reply work items pouziva task_jira_email_thread_reply.
Pozdejsi migrace rozsiruje task_jira o metadata lokalni Jira cache:
jira_updated_atjira_synced_atjira_sync_errorjira_assignee_account_idjira_assignee_display_namejira_assignee_emailjira_summaryjira_description_text
Jira-owned pole jako status, assignee, summary a plain-text description se
periodicky synchronizuji z Jiry. Lokální DB je cache a Jira zustava primarni
zdroj pravdy pro stav a obsah ticketu. user_id zustava lokalni mapovani na
app_user, zatimco jira_assignee_* drzi raw hodnoty z Jiry i pro uzivatele,
ktere Reply Pilot lokalne nezna.
Dvanacty changeset pridava:
public.party_organization.show_by_default BOOLEAN NOT NULL DEFAULT TRUE
show_by_default urcuje, jestli se firma ma bezne zobrazovat ve standardnich
listingu, odvozenych company lookupu a company search dokumentech. Primy detail
firmy zustava pristupny i kdyz je hodnota nastavena na FALSE.
Pozdejsi migrace rozsiruje public.party_organization o:
default_email_sales_user_id BIGINT NULL REFERENCES public.app_user (id) ON DELETE SET NULL
Sloupec urcuje vychoziho obchodnika pro emailovou komunikaci konkretni firmy. Aplikace ho pouzije pri predvyplneni assignee email reply tasku, pokud je uzivatel porad dostupny v Jira assignable seznamu.
Changeset 0086 pridava party_organization.import_email_communication BOOLEAN
NOT NULL DEFAULT TRUE a tabulku email_import_skipped_thread. Flag ridi pouze
import celych emailovych vlaken do DB; existujici historie a prilohy zustavaji
pristupne. Fronta drzi jen provider/thread identitu, zdrojovou schranku, firemni
vazby a cas dalsiho pokusu. Pri opetovnem povoleni firmy worker automaticky doplni
cele preskocene vlakno z Google-owned cache. Podrobny kontrakt je v
Activity Model.
Pozdejsi migrace normalizuje jmena osob:
public.party.display_namepro osoby je odvozene zpublic.party_person.first_nameapublic.party_person.last_name- manualni formulare osob edituji jen
first_namealast_name - contract changeset
0060odstranuje vzdy prazdnypublic.party_person.middle_name; dalsi casti jmena zustavaji vlast_name public.party_person_name_legacyuchovava hodnoty jmen pred normalizaci pro audit a rollback migrace
Trinacty changeset rozsiril public.task_jira o:
feed BOOLEAN NOT NULL DEFAULT FALSEenlistment_table BOOLEAN NOT NULL DEFAULT FALSE
Oba atributy jsou lokalni metadata Jira tasku v Reply Pilotu. Pri vytvoreni
tasku maji vychozi hodnotu FALSE a meni se v editaci tasku.
Ctrnacty changeset historicky pridal CME matching. Changeset 43 bezpecne
zkopiruje legacy vazby do noveho CME source modelu a contract changeset 44
odstrani legacy tabulky az v pozdejsim releasu. Aktualni CME model je popsany
v sekci CME source firem nize.
Patnacty changeset nahrazuje puvodni task_jira.feed BOOLEAN stavovym kodem:
public.task_feed_statepublic.task_jira.feed_state_code TEXT NOT NULL DEFAULT 'missing'
task_feed_state je lookup tabulka s internimi kody:
missingverifyingavailable
Pri migraci se puvodni feed = FALSE prevadi na missing a feed = TRUE na
available. Zobrazene ceske popisky zustavaji zalezitosti aplikace, ne DB
schema, aby se daly menit v releasu bez prepisu ulozenych dat.
Sestnacty changeset pridava requirement tracking nad firmami:
public.requirement_typepublic.requirement_state_typepublic.requirement_evidence_source_typepublic.requirement_evidence_verdict_typepublic.party_requirement_statepublic.party_requirement_evidencepublic.party_requirement_eval_queue
requirement_type urcuje, jake dokumentacni pozadavky se u firmy sleduji,
napr. PRICE_LIST, TOP10, BUSINESS_TERMS nebo SUPPLIER_IDENTIFIER.
Kody ENLISTMENT_TABLE a FEED byly odstranené changesetem 0085; jejich
source of truth je nejnovější Supplier Onboarding task podle task.created_at
(pri shode rozhoduje vyssi task.id).
party_requirement_state drzi aktualni agregovany stav ostatnich pozadavku pro
jednu firmu. Umoznuje odlisit UNKNOWN, REQUESTED, CANDIDATE_RECEIVED,
CONFIRMED_RECEIVED, REJECTED a NEEDS_REVIEW, plus volitelny manualni
override.
party_requirement_evidence uklada jednotlive dukazy odvozene z emailu,
threadu, prilohy nebo manualniho vstupu. Evidence muze odkazovat na konkretni
activity_email zpravu pres activity_email_id, a protoze model zatim nema
samostatnou DB entitu pro email thread ani attachment, uklada i
external_thread_id, metadata prilohy a AI vysvetleni/confidence.
party_requirement_eval_queue je worker fronta pro prubezny prepocet
agregovaneho stavu po novych emailech, novych prompt verzi nebo manualnich
zasazich bez nutnosti full scanu celeho activity modelu. Emailovy import, AI a
agregace tuto frontu pro ZT ani feed nepouzivaji.
Sedmnacty changeset pridal extracted facts z emailu. Po contract changesetu
0085 z nich zustava jen:
public.party_supplier_identifier_fact
party_supplier_identifier_fact uklada konkretni kandidátní identifikatory
dodavatele nalezene v jednom emailu nebo jeho priloze. Drzi
identifier_type_code, raw/normalized hodnotu, confidence a vazbu na
activity_email; volitelne muze ukazovat i na party_requirement_evidence.
Tato fact tabulka predstavuje vrstvu mezi raw emailem a finalnim agregovanym
stavem SUPPLIER_IDENTIFIER v party_requirement_state.
Osmnacty changeset pridava email artifact model navazany na activity_email:
public.activity_email_attachmentpublic.activity_email_link
activity_email_attachment uklada metadata priloh k jednomu importovanemu
emailu. Je to DB source of truth pro attachment metadata, ale ne pro samotny
binární obsah. Binarky zustavaji v existujici file cache modulu
reply-pilot-google (oddelene podle Gmail account id) a relative_path slouzi
jako bridge mezi DB a Gmail-owned storage.
activity_email_link uklada explicitni http(s) URL nalezene v body_text
jednoho importovaneho emailu. Drzi raw i normalizovanou podobu, host, path a
kratky kontext. Jde o obecna metadata emailu; ZT ani feed z nich nejsou
odvozovany.
activity_email.body_html uklada pouze dekodovane text/html MIME body casti,
ne raw RFC822 email. body_text zustava source pro summary, search, AI kontext
a extrakci odkazu.
Changeset 0083 je pouze expand migrace: pridava do activity_email nullable
source_app_user_id a source_mailbox_email s prazdnym defaultem. Existujici
radky ani unikatni klic nemeni. Sdilena schranka zustava pod providerem gmail;
osobni schranky pouzivaji account-scoped provider gmail:app-user:<id>, takze
shodne Gmail message ID z ruznych schranek nemuze aktualizovat cizi aktivitu.
Osobni import nadale nespousti supplier-requirement automatiku. Nove osobni
zpravy spousti Jira reply-task automatiku s povinnym prirazenim vlastnikovi
schranky. Expand migrace 0087 pridava do task_jira_email_thread_reply
provider TEXT NOT NULL DEFAULT 'gmail' a index (provider, external_thread_id).
Stavajici tasky zustavaji ve sdilene schrance; osobni tasky pouzivaji provider
gmail:app-user:<id>. Lookup, odvozene firmy, autorizace a search rozlisuji
provider spolu s Gmail thread ID.
Samotny importni repository navic odmitne osobni vlakno, ktere neodpovida
existujici firme pres presny firemni email, aktivni CONTACT_FOR email nebo
rucne ulozenou domenu; ochrana tedy nezavisi na spravnem filtrovani workerem.
Nezname adresy se ulozi jen jako raw activity participant a osobni import z nich
nevytvari contact_mech_email, osobu, firmu ani domenu. Vlastni adresa osobni
schranky je take raw-only a nevytvari party vazbu ani kdyz uz v kontaktnim
modelu existuje.
activity_email_attachment.relative_path dostane u osobniho importu prefix
accounts/app-user-<id>/, aby globalni unikatnost metadata nemohla presunout
prilohu mezi aktivitami dvou Gmail uctu se stejnym thread ID a nazvem souboru.
Devatenacty changeset rozsiril finalni requirement projection vrstvu:
public.party_requirement_state.resolved_delivery_mode_codepublic.party_requirement_state.resolved_value_type_codepublic.party_requirement_state.resolved_value_rawpublic.party_requirement_state.resolved_value_normalizedpublic.requirement_typenovy kodSUPPLIER_IDENTIFIER
party_requirement_state tak umi pro SUPPLIER_IDENTIFIER drzet nejen finalni
state_code, ale i finalni identifier_type a hodnotu. ZT a feed se v teto
projekci od changesetu 0085 nepouzivaji.
Changeset 0085 je contract migrace pro RP-5162. Pred smazanim zkopiruje
legacy ZT/feed requirement types, states, evidence, queue a feed facts do
tabulek s prefixem rp5162_, pote odstrani radky pro ENLISTMENT_TABLE a
FEED a tabulku party_feed_fact. Rollback obnovi schema i archivovana data.
Migrace nevytvari zadne Supplier Onboarding tasky a neprenasi do nich legacy
hodnoty. Po nasazeni aplikace a contract migrace je nutny plny Solr reindex,
aby company dokumenty obsahovaly hodnoty z nejnovějšího onboarding tasku.
Dvacity changeset pridava lead import a outreach policy vrstvu:
public.party_outreach_policypublic.lead_import_batchpublic.lead_import_item
party_outreach_policy drzi explicitni obchodni rozhodnuti nad firmou, hlavne
stav DO_NOT_CONTACT s lidskou poznamkou a vazbou na operatora.
lead_import_batch drzi auditni zaznam o jednom serverovem JSONL souboru,
typicky pripravenem mimo Reply Pilot batch modulem reply-pilot-wholesale-scout.
Uklada jmeno souboru, hash, cestu v backend storage a agregovany stav review.
lead_import_item drzi jednotlive kandidaty z daneho batch souboru. U kazde
polozky uklada puvodni JSON payload, prefill match na existujici firmu, AI draft
prvniho osloveni, operatorovu poznamku a vysledek rozhodnuti IMPORTED,
DO_NOT_CONTACT nebo SKIPPED. Pri importu muze navazat i lokalni task
zaznam a Jira key.
Dvacaty prvni changeset nahrazuje puvodni single-purpose prompt tabulku obecnym typed modelem:
public.ai_prompt_typepublic.ai_prompt
ai_prompt_type je lookup tabulka pro workflow-specific seznamy promptu.
Aktualne se seeduji tyto typy:
EMAIL_REPLAYIMPORT_WIZARDSUPPLIER_ONBOARDING
ai_prompt drzi konkretni predpripravene prompty. Kazdy radek ma povinnou vazbu
na ai_prompt_type, zobrazene name a prompt_text; poradi je stabilne podle
id.
Pri migraci se puvodni obsah public.email_replay_ai_prompt automaticky presune
do public.ai_prompt pod typ EMAIL_REPLAY a legacy tabulka se odstrani.
Stejny changeset rozsiri public.lead_import_item o:
ai_prompt_idai_prompt_snapshot
ai_prompt_id ukazuje na vybrany prompt pouzity pri generovani AI draftu v
lead import wizardu. ai_prompt_snapshot uklada presne instrukce, ktere byly
do AI requestu poslany, aby audit zustal stabilni i po pozdejsi uprave nebo
smazani promptu.
Dvacaty druhy changeset odstranuje z public.ai_prompt legacy sloupec code;
unikatnost promptu se dale neridi kodem, ale vazbou na typ a internim ID.
Dvacaty treti changeset pridava:
public.app_user_jira_profile
app_user_jira_profile oddeluje Simple Auth identitu v app_user od lokalniho
Jira assignment profilu. jira_account_id je stabilni Jira/Atlassian account id
pro mapovani assignee z Jiry. jira_email je volitelny override pro vyhledani
Jira uzivatele; pokud chybi, pouzije se app_user.email. jira_assignable
urcuje, jestli se uzivatel nabizi v dropdown seznamech pro prirazeni Jira tasku.
Dvacaty ctvrty changeset doplni vychozi app_user_jira_profile radky pro
existujici uzivatele s jira_assignable = TRUE. Stejny default pro nove
uzivatele pri nacitani assignee seznamu prubezne zajistuje aplikacni sluzba.
Dvacaty paty changeset pridava Jira task sync podporu:
public.task_jira.jira_updated_atpublic.task_jira.jira_synced_atpublic.task_jira.jira_sync_errorpublic.jira_sync_state
jira_sync_state drzi globalni stav periodicke synchronizace Jira tasku.
Hlavni watermark je last_seen_jira_updated_at, tedy Jira updated cas,
nikoli cas serveru Reply Pilotu. Incremental sync pouziva tento watermark minus
konfigurovatelny safety overlap; pokud watermark chybi, job provede full sync
projektovych Jira ticketu.
Dvacaty sesty changeset rozsiruje Jira task cache o raw assignee hodnoty z Jiry:
public.task_jira.jira_assignee_account_idpublic.task_jira.jira_assignee_display_namepublic.task_jira.jira_assignee_email
Tyto sloupce oddeluji skutecneho Jira assignee od lokalni vazby
task_jira.user_id. Pokud Jira API vrati email, jira_assignee_email lze
porovnat s app_user_jira_profile.jira_email; kdyz email kvuli Jira privacy
chybi, UI muze porad zobrazit Jira display name nebo account id.
Dvacaty sedmy changeset pridava do public.app_user_jira_profile:
jira_account_id
Mapovani tasku na lokalniho uzivatele pri Jira syncu a filtr Moje ukoly
porovnavaji nejdriv task_jira.jira_assignee_account_id proti
app_user_jira_profile.jira_account_id; email zustava fallback pro pripady, kdy
Jira email vraci.
Dvacaty osmy changeset rozdeluje lokalni Jira task cache podle work typu:
public.task_jira_work_typepublic.task_jira.jira_work_type_code TEXT NOT NULL DEFAULT 'supplier_onboarding'public.task_jira_supplier_onboardingpublic.task_jira_email_thread_reply
Lookup task_jira_work_type puvodne rozdelil hodnoty supplier_onboarding a
email_thread_reply. Pozdejsi migrace doplnila hodnotu reply_pilot_task pro
obecne delegovane Jira tasky typu Reply Pilot Task. Migrace backfilluje
vsechny existujici task_jira radky na supplier_onboarding a zalozi
odpovidajici subtype radky v task_jira_supplier_onboarding. Jira-side zmena
existujicich work items z puvodniho Task na Supplier Onboarding se dela
rucne v Jira Cloud pres bulk move.
Dvacaty devaty changeset presouva onboarding-only pole feed_state_code a
enlistment_table z base tabulky task_jira do subtype tabulky
task_jira_supplier_onboarding. Email Thread Reply tasky tato pole ve schematu
nemaji; jejich workflow stav zustava Jira-owned task_jira.status cache.
Tricaty changeset pridava novy public.ai_prompt_type kod
SUPPLIER_ONBOARDING pro predpripravene prompty pouzite pri generovani
editovatelneho Jira summary a description pred zalozenim Supplier Onboarding
ticketu z detailu firmy.
Tricaty druhy changeset pridava globalni public.app_configuration singleton
a firemni party_organization.default_email_sales_user_id pro vychoziho
obchodnika emailove komunikace.
Ctyricaty changeset vraci do public.task_jira plain-text cache pro Jira texty:
jira_summary TEXT NOT NULL DEFAULT ''jira_description_text TEXT NOT NULL DEFAULT ''
Cache se plni z Jira task syncu a z backend task mutaci, ktere vytvareji nebo edituji Supplier Onboarding a Email Thread Reply tasky. Uklada se jen plain text; Jira ADF JSON se do DB neuklada.
Ctyricaty prvni changeset pridava lokalni subtype pro obecne delegovane Jira tasky:
public.task_jira_work_typenovy kodreply_pilot_taskpublic.task_jira_reply_pilot_task
reply_pilot_task reprezentuje Jira issue type Reply Pilot Task.
customfield_10269 Jira pole taskType zustava Jira-owned hodnota a v DB se
uklada jako raw text v task_jira_reply_pilot_task.task_type_value. Aktualni
potvrzene hodnoty jsou General, Meeting organization a
Registration (B2B), ale DB nepouziva check constraint, aby Jira-owned dropdown
mohl byt pozdeji rozsiren bez destruktivni migrace.
task_jira_reply_pilot_task drzi workflow-specific data, ktera nepatri do base
task_jira:
task_id BIGINT PRIMARY KEY REFERENCES public.task_jira (task_id) ON DELETE CASCADEtask_type_value TEXT NOT NULL DEFAULT 'General'requester_app_user_id BIGINT NULL REFERENCES public.app_user (id) ON DELETE SET NULLcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
Vazba na spolecnost zustava v base task_jira.party_id; tahle hodnota je
source of truth pro Reply Pilot workflow a autorizaci. Jira description ma
obsahovat lidsky odkaz na spolecnost, ale backend se pri rozhodovani nema ridit
parsovanim description.
Pri resolve akci backend bere requester_app_user_id jako source of truth pro
zadavatele, prida zadany text do Jira/plain-text description, priradi Jira task
zpet na Jira ucet tohoto app usera a ponecha Jira status In Progress. Pri
close akci muze task uzavrit jen ulozeny zadavatel; backend presune Jira task
do Done a assignee nemeni.
Ctyricaty sesty changeset pridava do public.app_user_jira_profile:
jira_oauth_credentials_ciphertext TEXT NULL
Sloupec drzi sifrovany delegovany OAuth 2.0 credential envelope. Je nullable, protoze uzivatel muze Reply Pilot pouzivat bez delegovaneho Jira uctu; v tom pripade zapis komentare vrati vyzvu k autorizaci. Rollback sloupec odstrani. Technicky Jira API token backendu zustava mimo DB v runtime secrets.
Ctyricaty sedmy changeset pridava lokalni subtype pro Jira issue type
Organize a Meeting:
public.task_jira_work_typenovy kodorganize_meetingpublic.task_jira_organize_meeting
task_jira_organize_meeting.contact_party_id je nullable vazba na
public.party. Hodnota NULL znamena, ze pri zalozeni pozadavku nebyla
kontaktni osoba vybrana. Backend dovoli ulozit nenulovou osobu jen tehdy, kdyz
ma ke spolecnosti z task_jira.party_id aktivni vztah CONTACT_FOR.
Spolecnost, Jira assignee, summary a plain-text Description zustavaji v base
task_jira cache; contact_party_id je workflow-specific metadata subtype.
Ctyricaty osmy changeset pridava public.work_wizard_task_snooze pro docasne
odlozeni polozky v Průvodci úkoly konkretnim uzivatelem:
user_id BIGINT NOT NULL REFERENCES public.app_user (id) ON DELETE CASCADEtask_id BIGINT NOT NULL REFERENCES public.task (id) ON DELETE CASCADEsnoozed_until TIMESTAMPTZ NOT NULL- primarni klic
(user_id, task_id)
Odklad je pouze per-user pohledova vlastnost Reply Pilotu; do Jiry se
nezapisuje. Kliknuti na „Teď neřešit“ provede upsert s expiraci za tri hodiny.
Průvodce filtruje jen radky s snoozed_until v budoucnosti, proto se ukol po
expiraci znovu zobrazi i pred fyzickym uklidem radku. Worker expirovane radky
periodicky maze.
Ctyricaty devaty changeset pridava
public.app_user_google_calendar_profile. Jeden radek predstavuje jeden
uzivatelsky Google Calendar grant a po smazani app_user se smaze pres
ON DELETE CASCADE. Povinne check constrainty zakazuji prazdny Google subject,
email a sifrovany credential envelope. Rollback tabulku odstrani.
Padesaty changeset pridava do Google Calendar profilu
meeting_mode_preference s vychozi hodnotou both a check constraintem pro
hodnoty both, online a in_person. Rollback odstrani constraint i sloupec.
Padesaty prvni changeset pridava katalog public.app_skill, vazebni tabulku
public.app_user_skill, dovednosti organize_meeting_caller a
supplier_meeting_host a opravneni user.manage_skills. Opravneni je seedovane
do systemove role admin; pracovni dovednosti nejsou soucasti RBAC roli.
Padesaty druhy changeset prirazuje existujici opravneni user.manage_skills
take systemove roli support_impersonator (v UI „Impersonace uživatelů“).
Stranka spravy dovednosti je tak dostupna pouze rolim admin a
support_impersonator; ostatni seedovane role toto opravneni nemaji.
Padesaty treti changeset je contract migrace, ktera po nasazeni vsech
kompatibilnich konzumentu odstrani constraint a sloupec
app_user_google_calendar_profile.meeting_mode_preference. Profil nadale drzi
Google identitu a opaque sifrovany Calendar credential envelope. Standardni
expand-only start ani deploy tuto migraci neaplikuje.
Padesaty ctvrty changeset je contract migrace, ktera odstrani zbytky zruseneho manualniho propojovani emailovych vlaken s firmami:
public.email_thread_company_linkpublic.email_thread_company_exclusionpublic.activity_email_thread_company_event
Aktualni model odvozuje firmy z activity_email, activity_participant a
vztahu party_relationship; tyto legacy tabulky uz zadny konzument v tomto
repozitari nepouziva. Standardni expand-only start ani deploy changeset
0054 neaplikuje.
Padesaty paty changeset je reverzibilni datova migrace prvniho overeneho
zahranicniho firemniho identifikatoru. Pro aktivni firmu 4972 presune nemecky
zapis HRA 640607 do party_identifier jako COMMERCIAL_REGISTER_NUMBER se
scope vydavajiciho rejstriku a teprve potom vycisti docasny cesky ICO sloupec.
Nejde o obecny prevod vsech malformed hodnot.
Padesaty sesty changeset pridava
party_organization.registration_country_code s prazdnou vychozi hodnotou pro
historicke neoverene firmy. Zname zaznamy 4972 a 6787 oznaci jako DE a
PL; ostatni zeme se musi doplnit rucne, protoze je nelze bezpecne odvodit z
existujicich identifikatoru.
Padesaty sedmy changeset prejmenovava systemovou roli sales_manager na
sales_int s popiskem Obchodník int bez zmeny jejich opravneni nebo
existujicich uzivatelskych prirazeni. Zaroven pridava systemovou roli loader
s popiskem Zavaděč a pouze opravnenimi company.view_all a company.write.
Padesaty osmy expand changeset presune neplatne kanonicke i unresolved legacy
registracni hodnoty do typu
LEGACY_UNRESOLVED_COMPANY_REGISTRATION_NUMBER. value_raw zachova puvodni
text a value_normalized dostane source/party scope, aby auditni hodnoty
nemohly kolidovat ani slouzit k matchingu. Platne jednoznacne legacy ICO
backfilluje do kanonickeho typu, vynuti jeho osmimistny format a nejvyse jedno
ICO na party. Docasny trigger udrzoval legacy sloupec pro rollback stare verze
aplikace do nasledneho contract kroku.
Padesaty devaty contract changeset nejprve overi nulovy drift, odstrani docasny
trigger a zahodi party_organization.company_registration_number. Standardni
start/deploy jej pres !contract preskoci. V produkci byl po prijeti
identifier-only aplikace explicitne aplikovan 2026-08-20.
Contract changesety 0060 az 0064 odstranuji pouze redundantni sloupce, pro
ktere produkcni audit i repository audit prokazaly ekvivalentni zdroj nebo
konstantni/prazdnou hodnotu:
party_person.middle_name, prazdne role vparty_relationship, redundantniparty_contact_mech.from_datea surrogateparty_contact_mech_purpose.id;- vzdy prazdne
party_requirement_state.requested_at; - konstantni
activity_email_attachment.storage_backend_code; ai_prompt.sort_order, jehoz poradi se shoduje s runtime poradi podleid;- timestampy
task_jira_organize_meeting, ktere jsou shodne stask.created_at.
Kazdy changeset obsahuje datovou stop podminku. Pokud se schema pred explicitnim
contract releasem zacne pouzivat jinak, migrace skonci chybou misto ztraty dat.
Standardni !contract start/deploy tyto changesety neaplikuje.
Changeset 0065 pridava opravneni
task.supplier_onboarding.ask_question a prirazuje ho systemovym rolim
admin a loader. Opravneni chrani backend mutaci, ktera preda Supplier
Onboarding task zavaděce obchodnikovi ve stavu Waiting for Info.
CME source firem
party_cme_source je read model puvodu firmy v CME. Primarni klic
(source_type, source_id) rozlisuje DODAVATEL a OSLOVENI; vice CME zaznamu
muze odkazovat na stejne party_id. Tabulka uchovava CME obchodnika a datumy
rezervace/oslovení oddelene od party_organization.default_email_sales_user_id,
protoze lokalni vychozi obchodnik ovlivnuje opravneni a prirazovani tasku.
Sloupec company_contacts JSONB uchovava pole aktivnich CME kontaktu ve tvaru
contactId, name, email, phone, note. Jde o zdrojovy, neovereny snapshot
pro zobrazeni v CME casti detailu firmy.
Synchronizace je jednosmerna CME -> Reply Pilot a zpracovava vsechny
dodavatele ze zdroje DODAVATEL, bez dalsiho filtru obchodni aktivity,
a rezervace ze zdroje OSLOVENI (osloveni_dodavatele). Oba zdroje zakladaji
chybejici firmy a aktualizuji sve zaznamy v party_cme_source, vcetne
obchodnika a dat rezervace/oslovení. Rezervace nepridava roli SUPPLIER
ani nemeni vychoziho emailoveho obchodnika. Kazdy zdrojovy radek musi mit
platne ICO a existujici firmu hleda vyhradne v kanonickem party_identifier.
Vice aktivnich kandidatu se stejnym ICO je chyba a zadna firma se nevytvori ani
nepropoji. DIC, nazev ani predchozi source vazba nejsou fallback.
Drive pouzivany party_identifier.identifier_type_code = 'CME_DODAVATEL_ID'
se uz nevytvari ani nepouziva; jedinym source of truth pro CME identitu je
party_cme_source (source_type, source_id). Nejednoznacny zaznam se automaticky
neslouci; zustane v party_cme_source s prazdnym party_id a last_error.
Vychoziho emailoveho obchodnika synchronizace nastavi pouze pro DODAVATEL podle jednoznacne
shody cme_salesperson_email s app_user.email po orezani okolnich mezer a bez
rozliseni velikosti pismen; cilove pole
party_organization.default_email_sales_user_id dostane lokalni app_user.id.
Prazdna i odlisna rucni volba se prepise, shodna hodnota se znovu nezapisuje.
CME obchodnik_id a autentizacni auth_user_id jsou odlisne identity a nesmi se
porovnavat. Login ani jmeno nejsou matching kriterium. Chybejici obchodnik,
prazdny email, zadny nebo vice odpovidajicich lokalnich uzivatelu prirazeni
nemeni ani nemaze a nebrani importu jinak platne firmy. Vice emailovych shod
se loguje jako WARN. Existujici firmy se znovu vyhodnocuji pri kazdem syncu.
Pred zapisovanim se posoudi vsichni dodavatele se stejnym normalizovanym ICO.
Vice ruznych neprazdnych CME salesperson ID znamena INFO log a preskoceni
prirazeni pro celou firmu bez ohledu na poradi radku. Globalni vychozi
obchodnik a existujici Jira tasky se nemeni.
Ostatni lokalni vyplnene udaje synchronizace neprepisuje. Zdrojovy zaznam, ktery zmizi z CME,
dostane missing_since a nemaze se. CME synchronizace nevytvari ani neaktualizuje
contact_mech nebo party_contact_mech; drive importovane lokalni kontakty
zustavaji beze zmeny. Pri dalsim behu se nahradi jen company_contacts snapshot.
Readonly preview user
Operacni skripty DB modulu umi po migracich sladit i volitelneho readonly
PostgreSQL uzivatele pro nahled do DB. Konfigurace je v .env.local nebo
.env.server:
DB_PREVIEW_USER_ENABLEDDB_PREVIEW_USER_NAMEDB_PREVIEW_USER_PASSWORD
Kdyz je DB_PREVIEW_USER_ENABLED=1, skripty vytvori nebo zapnou login a daji
mu CONNECT na databazi, USAGE na public a SELECT na vsechny aktualni i
budouci tabulky/sekvence v public. Kdyz je DB_PREVIEW_USER_ENABLED=0,
skripty roli prepnou na NOLOGIN, takze jde v produkci vypnout jen nastavenim.
Lokalni start
cd reply-pilot-db
cp .env.example .env.local
./scripts/db-start.sh
db-start.sh po readiness checku automaticky spusti Liquibase validate a
update s filtrem !contract. Stejne tak db-deploy.sh. Tohle je zamerna
ochrana proti tomu, aby destruktivni migrace odstranila schema, ktere jeste
pouziva starsi bezici backend. Rucni migracni skript zustava zachovany a prime
Liquibase prikazy lze volat pres ./scripts/db-liquibase.sh.
Od migrace 0043 musi byt destruktivni changeset v db.changelog.xml
oznaceny contextFilter="contract". Validace deploy zablokuje, pokud najde
drop, truncate, rename nebo nekompatibilni zmenu typu bez tohoto kontextu.
ALTER COLUMN ... DROP NOT NULL pouze uvolnuje nullability a validace jej
klasifikuje jako expand; soubezny DROP COLUMN nebo jiny destruktivni prikaz
nadale vyzaduje contract kontext.
Contract migrace se spousti az v pozdejsim samostatnem deployi, kdy jsou vsechny
konzumenty prokazatelne na novem schematu:
./scripts/db-liquibase.sh update --context-filter='@contract'
Prefix @contract znamena, ze se v tomto explicitnim kroku vyberou jen
changesety skutecne oznacene contract kontextem. Standardni deploy tenhle krok
nikdy nespousti.
Pro host publikaci DB plati:
HOST_DB_BINDurcuje bind adresu na hostu; default je127.0.0.1HOST_DB_PORTurcuje host port; default je5433- kdyz je
HOST_DB_BIND=0.0.0.0, PostgreSQL je dostupna i mimo serverovy loopback
Ověření
./scripts/db-psql.sh -c '\dt public.*'
./scripts/db-psql.sh -c 'select id, author, filename, orderexecuted from public.databasechangelog order by orderexecuted;'
./scripts/db-liquibase.sh history
./scripts/db-liquibase.sh rollback-count 1
Opakovane spusteni ./scripts/db-migrate.sh musi nechat schema beze
zmen. Kazdy migrations/*.sql soubor ma presne jeden Liquibase changeset a
musi obsahovat explicitni --rollback.
Workflow při změně schema
Kdyz menis databazove schema:
- pridej Liquibase changeset do
reply-pilot-db/migrations/ - dopln novy soubor do
reply-pilot-db/migrations/db.changelog.xml - zajisti explicitni
--rollback - pokud je zmena destruktivni, oznac include
contextFilter="contract"a naplanuj ji do pozdejsiho releasu po nasazeni kompatibilnich konzumentu - over expand migraci pres:
cd reply-pilot-db
./scripts/db-liquibase.sh validate
./scripts/db-liquibase.sh update
./scripts/db-liquibase.sh rollback-count 1
./scripts/db-liquibase.sh update
Contract migraci over pres expand-only update (musi ji preskocit), potom
explicitni @contract update, rollback a znovu explicitni update:
./scripts/db-liquibase.sh validate
./scripts/db-liquibase.sh update
./scripts/db-liquibase.sh update --context-filter='@contract'
./scripts/db-liquibase.sh rollback-count 1
./scripts/db-liquibase.sh update --context-filter='@contract'
Pokud se meni activity/contact data model, aktualizuj i docs/activity-model.md
a přegeneruj souvisejici diagramy pres rp.
Role osoby ve společnosti (0093)
Migrace 0093 rozšiřuje party_relationship o nepovinné texty job_title a
role_description s prázdnou výchozí hodnotou. Částečný unikátní index dovoluje
nejvýše jednu aktivní vazbu CONTACT_FOR pro dvojici osoba–společnost.
U existujících duplicit ponechá nejstarší aktivní vazbu a ostatní ukončí.
Žádné vazby nemaže; jejich ID a čas ukončení zachová v
party_relationship_role_migration_audit, aby rollback obnovil původní stav.
Migrace musí předcházet nasazení BE a APP. Rollback odstraní index i obě pole,
proto se jeho ověření provádí na izolované databázi. Viz Activity model.
Pippa conversations (0089)
ai_company_conversation is owned by a stable company party_id; it stores
the creator, title, summary and timestamps. ai_company_turn stores ordered
question/answer pairs, authors, processing state, context JSONB and result JSONB.
The context is a text/source snapshot for that answer; result metadata includes
citations, findings, source-use records and provider request identifiers.
The first question and its conversation are inserted in one transaction. Opening
a draft performs no insert. Question UUIDs prevent duplicate submissions. A conversation row lock plus a
partial unique index allows only one queued or processing turn per conversation.
Retry changes the failed last turn without appending duplicate messages.
The same migration seeds company.ai_assist for sales and sales_int.
Rollback removes both new tables and this permission/grants. See
Pippa for recovery, authorization and native attachment limits.
Pippa company onboarding (0090)
ai_company_conversation.onboarding JSONB stores the explicit company-creation
workflow: phase, proposal, approved fields, email draft and confirmed result IDs.
party_id may be null only when onboarding is present. The company is attached
after creation; existing company-filtered queries retain their scope. An index
on author and update time supports the owner's standalone conversation list.
Phase claims lock the conversation and require no active AI turn before commit.
This is an expand migration. Deploy it before the backend/frontend release. Rollback refuses while any standalone conversation exists; it never deletes those conversations to satisfy the old NOT NULL constraint. Resolve their ownership/history explicitly before attempting a production rollback. Once all rows have a company, rollback removes workflow metadata but retains ordinary conversation/turn history. No party/contact/activity schema is changed.
Pippa scouting (0091)
Scouting reuses ai_company_conversation and ai_company_turn with
onboarding.phase = scouting, an explicit pre-company workflow phase. Dedicated
queries distinguish it from company onboarding; no existing constraint is removed.
The regular queue, immutable source snapshots and write-attempt replay guard apply.
pippa_scout_candidate stores canonical public research, current verification,
known domain/verified registration identity keys, the global rejection reason,
actor/time and append-only decision history, plus nullable onboarding/company links.
pippa_scout_result associates a candidate with each scouting conversation and
stores search country, that search's evidence and its optional skip reason.
Deleting a conversation cascades only membership; canonical candidates and decisions
survive. Deleting a child onboarding conversation clears its link but retains the
company association if creation completed.
Candidate upserts serialize a short local DB transaction with an advisory lock, matching normalized exact website hosts or verified country/registry-scoped IDs. Known aliases are retained. Conflicting identities are rejected rather than merged; name similarity alone never merges candidates. Undiscovered aliases cannot be matched until evidence supplies a shared identifier. A candidate row lock ensures concurrent onboarding starts create only one child conversation. Candidate rejection also locks its linked onboarding conversation; approval rechecks rejection under the conversation lock before claiming company creation.
This additive migration must precede BE/APP deployment. Validate, update, rollback-count 1, update are covered on a disposable PostgreSQL database. Its rollback removes scouting candidates/membership and their decisions, so production rollback requires backing up that data; conversation/turn history is retained.