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 validate a expand-only update pri start a deploy
  • reconcile samostatneho readonly loginu reply_pilot_backup pro 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.databasechangelog
  • public.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_user
  • public.app_user_jira_profile
  • public.app_user_google_calendar_profile
  • public.app_skill
  • public.app_user_skill
  • public.app_permission
  • public.app_role
  • public.app_role_permission
  • public.app_user_role
  • public.app_configuration

Tabulka app_user ma zatim:

  • id BIGINT GENERATED BY DEFAULT AS IDENTITY
  • login_key TEXT NOT NULL
  • auth_user_id TEXT NOT NULL DEFAULT ''
  • email TEXT NULL
  • display_name TEXT NOT NULL DEFAULT ''
  • gmal_box_link TEXT NOT NULL DEFAULT ''
  • chart_color VARCHAR(7) NOT NULL ve formatu #RRGGBB, s nahodnym vychozim odstinem pro nove i pri migraci existujici uzivatele
  • created_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), name
  • token_type omezeny na pippa nebo other
  • token_hash (unikatni SHA-256 hex) a token_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 CASCADE
  • jira_account_id TEXT NULL
  • jira_email TEXT NULL
  • jira_assignable BOOLEAN NOT NULL DEFAULT TRUE
  • jira_oauth_credentials_ciphertext TEXT NULL
  • created_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 CASCADE
  • google_subject TEXT NOT NULL
  • google_email TEXT NOT NULL
  • google_display_name TEXT NOT NULL DEFAULT ''
  • google_oauth_credentials_ciphertext TEXT NOT NULL
  • created_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 CASCADE
  • body_html TEXT NOT NULL
  • body_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 1
  • default_email_sales_user_id BIGINT NULL REFERENCES public.app_user (id) ON DELETE SET NULL
  • created_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_assigned
  • company.view_all
  • company.write
  • company.write_assigned
  • company.merge
  • security.manage_roles
  • security.impersonate_user
  • task.supplier_onboarding.ask_question
  • user.manage_skills

Seedovane system role jsou:

  • admin se vsemi opravnenimi krome security.impersonate_user
  • sales s company.view_assigned a company.write_assigned
  • sales_int s company.view_assigned, company.view_all, company.write, company.write_assigned a company.merge
  • loader s company.view_all, company.write a task.supplier_onboarding.ask_question
  • support_impersonator s security.impersonate_user a user.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_all muze videt vsechny firmy v povolenych read cestach
  • uzivatel bez company.view_all, ale s company.view_assigned, vidi jen firmy, kde party_organization.default_email_sales_user_id = app_user.id
  • firmy s party_organization.default_email_sales_user_id IS NULL jsou ve V1 viditelne jen pro company.view_all
  • party_organization.show_by_default zustava jen display filtr uvnitr autorizovaneho rozsahu, neni to autorizace
  • zapisove company/task endpointy pro existujici firmu prijimaji company.write nebo company.write_assigned a zaroven kontroluji stejny company visibility scope; role sales tedy 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_profile muze cist jen firmu napojenou na tento aktivni task a samotny aktivni task; vyjimka neplati pro zapis, nesouvisejici firmy, neprirazene tasky ani Done tasky; rozsirena firemni spoluprace zustava omezena na aktivni prirazene Reply Pilot Task
  • zalozeni nove firmy prijima company.write nebo company.write_assigned; backend nove firme vzdy nastavi party_organization.default_email_sales_user_id = app_user.id aktualniho uzivatele, takze scoped zapis vytvari jen self-assigned firmu

Dalsi migrace zavedly:

  • public.party
  • public.party_person
  • public.party_person_name_legacy
  • public.party_organization
  • public.vat_jurisdiction
  • public.party_vat_registration
  • public.party_outreach_policy
  • public.contact_mech
  • public.contact_mech_email
  • public.contact_mech_phone
  • public.party_contact_mech
  • public.party_relationship
  • public.activity_type
  • public.activity
  • public.activity_email
  • public.activity_email_attachment
  • public.activity_email_link
  • public.activity_call
  • public.activity_meeting
  • public.activity_note
  • public.activity_participant
  • public.task_type
  • public.task
  • public.task_jira
  • public.task_jira_email_thread
  • public.task_jira_work_type
  • public.task_jira_supplier_onboarding
  • public.task_jira_email_thread_reply
  • public.task_jira_reply_pilot_task
  • public.task_jira_organize_meeting
  • public.work_wizard_task_snooze
  • public.jira_sync_state
  • public.ai_prompt_type
  • public.ai_prompt
  • public.lead_import_batch
  • public.lead_import_item
  • public.requirement_type
  • public.requirement_state_type
  • public.requirement_evidence_source_type
  • public.requirement_evidence_verdict_type
  • public.party_requirement_state
  • public.party_requirement_evidence
  • public.party_requirement_eval_queue
  • public.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 EL mapuje zemi GR, XI mapuje GB a cesky lokalni nazev je DIČ
  • party_vat_registration uklada raw i normalizovanou hodnotu, odkazuje na party pres ON DELETE CASCADE a na katalog pres ON DELETE RESTRICT
  • UNIQUE (party_id, vat_jurisdiction_code) povoluje nejvyse jednu registraci dane jurisdikce na party; value_normalized ma 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_type
  • public.task
  • public.task_jira
  • public.task_jira_email_thread
  • public.task_jira_work_type
  • public.task_jira_supplier_onboarding
  • public.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_key
  • status
  • party_id
  • user_id
  • resolution
  • jira_work_type_code
  • jira_summary
  • jira_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_at
  • jira_synced_at
  • jira_sync_error
  • jira_assignee_account_id
  • jira_assignee_display_name
  • jira_assignee_email
  • jira_summary
  • jira_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_name pro osoby je odvozene z public.party_person.first_name a public.party_person.last_name
  • manualni formulare osob edituji jen first_name a last_name
  • contract changeset 0060 odstranuje vzdy prazdny public.party_person.middle_name; dalsi casti jmena zustavaji v last_name
  • public.party_person_name_legacy uchovava hodnoty jmen pred normalizaci pro audit a rollback migrace

Trinacty changeset rozsiril public.task_jira o:

  • feed BOOLEAN NOT NULL DEFAULT FALSE
  • enlistment_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_state
  • public.task_jira.feed_state_code TEXT NOT NULL DEFAULT 'missing'

task_feed_state je lookup tabulka s internimi kody:

  • missing
  • verifying
  • available

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_type
  • public.requirement_state_type
  • public.requirement_evidence_source_type
  • public.requirement_evidence_verdict_type
  • public.party_requirement_state
  • public.party_requirement_evidence
  • public.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_attachment
  • public.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_code
  • public.party_requirement_state.resolved_value_type_code
  • public.party_requirement_state.resolved_value_raw
  • public.party_requirement_state.resolved_value_normalized
  • public.requirement_type novy kod SUPPLIER_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_policy
  • public.lead_import_batch
  • public.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_type
  • public.ai_prompt

ai_prompt_type je lookup tabulka pro workflow-specific seznamy promptu. Aktualne se seeduji tyto typy:

  • EMAIL_REPLAY
  • IMPORT_WIZARD
  • SUPPLIER_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_id
  • ai_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_at
  • public.task_jira.jira_synced_at
  • public.task_jira.jira_sync_error
  • public.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_id
  • public.task_jira.jira_assignee_display_name
  • public.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_type
  • public.task_jira.jira_work_type_code TEXT NOT NULL DEFAULT 'supplier_onboarding'
  • public.task_jira_supplier_onboarding
  • public.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_type novy kod reply_pilot_task
  • public.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 CASCADE
  • task_type_value TEXT NOT NULL DEFAULT 'General'
  • requester_app_user_id BIGINT NULL REFERENCES public.app_user (id) ON DELETE SET NULL
  • created_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_type novy kod organize_meeting
  • public.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 CASCADE
  • task_id BIGINT NOT NULL REFERENCES public.task (id) ON DELETE CASCADE
  • snoozed_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_link
  • public.email_thread_company_exclusion
  • public.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 v party_relationship, redundantni party_contact_mech.from_date a surrogate party_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 podle id;
  • timestampy task_jira_organize_meeting, ktere jsou shodne s task.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_ENABLED
  • DB_PREVIEW_USER_NAME
  • DB_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_BIND urcuje bind adresu na hostu; default je 127.0.0.1
  • HOST_DB_PORT urcuje host port; default je 5433
  • 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:

  1. pridej Liquibase changeset do reply-pilot-db/migrations/
  2. dopln novy soubor do reply-pilot-db/migrations/db.changelog.xml
  3. zajisti explicitni --rollback
  4. pokud je zmena destruktivni, oznac include contextFilter="contract" a naplanuj ji do pozdejsiho releasu po nasazeni kompatibilnich konzumentu
  5. 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.