Знакът на КАГАМИ КАГАМИ
kagami.bg/academy · lesson · machine-readable viewVERIFIED 2026-10-01 · UPDATED 2026-10-01
IDENTITY
module
GX10-04-138 · Contract deadlines: from a clause to a reminder (Art. 72 ZZD arithmetic, human confirmation)
series
GX10 (local AI server class: NVIDIA GB10, e.g. ASUS Ascent GX10 / DGX Spark)
level
Intermediate
duration
2–3 h
prerequisites
Python 3.10+; for the optional parts: Ollama with a local model (lesson 04-135), PostgreSQL, n8n 2.x with timezone Europe/Sofia (n8n lesson 7). Fictional contracts only
trust_label
VERIFIED 2026-10-01 (statute texts read from lex.bg; Easter dates cross-checked) · UPDATED 2026-10-01 · NOT TESTED on a GB10-class machine. The date arithmetic and its 9 unit tests WERE run in a Linux sandbox (Python 3.10.12): 9 of 9 passed. The Ollama extraction, the SQL schema and the n8n workflow were NOT run
legal_status
Preparation, not legal advice. Every deadline must be confirmed by a person; where rights or money depend on it, by a lawyer. Statute texts come from lex.bg (not the official gazette): verify against the official edition before relying on them
language
human view: bg · english edition: /en/academy/gx10/ (same file name)
previous / next
04-137 notary deed OCR extraction / 04-140 contract drafting aid (legal chain 04-135 → 04-136 → 04-137 → 04-138 → 04-140 → 04-142 → 04-193; related: 04-135 contract analysis, 04-136 legal document RAG)
PURPOSE

Turn dated obligations in contracts into reminders safely: a local model only locates clauses and returns quote, number, unit and anchor (never a date); plain code computes the end date under Art. 72 of the Obligations and Contracts Act (ZZD) with non-working days and Bulgarian official holidays for 2026 and 2027 (Art. 154 of the Labour Code, KT); a person confirms each deadline; only confirmed deadlines produce reminders (30, 14, 7 and 1 day before, moved back to the previous working day); n8n sends them each working morning at 08:00 Europe/Sofia. Output is a reading and planning aid for a lawyer, not legal advice.

KEY CONCEPTS
LEGAL TEXT (verbatim, Bulgarian original, lex.bg, read 2026-10-01)
COMMANDS / PATHS
CHECKLIST
NEXT MODULE

04-140 contract drafting aid (next in the legal chain) · related: 04-135 contract analysis, 04-136 legal document RAG with citations · GX10 series index · offer: Quick experiment (kagami.bg/stalbata/)

SOURCES
TAGS
gx10nvidia-gb10contract-deadlineszzd-art-72working-daysbulgarian-holidaysremindersn8nhuman-in-the-looplocal-ai
ПРОВЕРЕНО · 01.10.2026 ОБНОВЕНО · 01.10.2026

Срокове от договори: от клаузата до напомнянето

В договора пише „30 дни от подписването“, „един месец предизвестие“, „5 дни след получаване на фактурата“. Кога точно изтича срокът? Строим помощник, който намира такива клаузи, смята датата по правилата на чл. 72 ЗЗД (с празниците и неприсъствените дни за 2026 и 2027), пита човек да потвърди всеки срок и чак тогава праща напомняния. Датата я смята обикновен код, не моделът. Помощникът подготвя, не решава.

⏱ 2–3 ч Средно GX10 Python · PostgreSQL · n8n Само измислени договори
Python (дати, правилата)🔒 локално Ollama · Llama 3.1 (намира клаузите)🔒 локално PostgreSQL · n8n (напомнянията)🔒 локално Имейл през твоя SMTP доставчик🌐 външна услуга
⚖️
ВАЖНО: това не е правен съвет
Урокът показва как се смята срок по чл. 72 ЗЗД и как се строи напомняне. Не е правна консултация и не замества юриста. Колко време имаш, кога тръгва срокът и кой ден е „последният“ зависи от точните думи на договора и от закона. Всеки срок се потвърждава от човек, а когато от него зависят права или пари — и от юрист. Правните текстове са цитирани пълно по lex.bg към 01.10.2026 и затова трябва да се сверят с официалното издание, преди да се разчита на тях.
🔄
ОБНОВЕНО · 01.10.2026 — какво
Урокът е преработен по текущите документи. Махнахме: ескалация по SMS през външна услуга (телефони на хора и външен доставчик — излишно за урока), примерите с пароли във връзката към базата, „.ics“ файловете и FastAPI таблото (за да остане фокусът върху изчислението и потвърждението), твърдението, че „преклузивните срокове не могат да се възстановяват“ (не е сверено с текст на закон) и таблицата „Видове срокове“ с давностни периоди. В нея пишеше „1 г. за наем“, а в чл. 111, б. „в“ ЗЗД наемът е с тригодишна давност: „С изтичане на тригодишна давност се погасяват: а) вземанията за възнаграждение за труд, за които не е предвидена друга давност; б) (изм. - ДВ, бр. 12 от 1993 г.) вземанията за обезщетения и неустойки от неизпълнен договор; в) вземанията за наем, за лихви и за други периодични плащания. г) (зал. - ДВ, бр. 12 от 1993 г.)“ (lex.bg, 01.10.2026). Давностите не са тема на този урок — тях ги преценява юристът. Добавихме: пълния текст на чл. 72 ЗЗД и на чл. 154 КТ; код, който смята срокове във дни, седмици и месеци със скачане над неприсъствени дни; празниците за 2026 и 2027 (Великден и преместените дни); тестове (9 проверки, пуснати в пясъчник); стъпка, в която човек потвърждава всеки срок — без „да“ няма дата и няма напомняне; изчисляването е извън модела, а цитатът му се сверява с договора; графикът на n8n е със зона Европа/София, като в Обучение 7. Спорният „пример за изход“ (срок за 12 месеца „считано от 01.07.2026“, показан като 01.07.2027 без пояснение) е заменен с обяснение защо тук има два прочита.
⚠️
Какво не сме пускали сами
Пуснато е: изчисляването на дати и тестовете му — в пясъчник (Linux, Python 3.10.12), не на машина от класа GB10; 9 от 9 проверки минаха. Не е пускано: извличането с Ollama (няма измерена точност на български договори), SQL схемата в Postgres и графикът в n8n — затова няма етикет „ТЕСТВАНО“. Версиите на Ollama и модела са като в урока за договори. Празниците за 2027 са изчислени по правилото на чл. 154, а не сверени с публикуван официален календар за 2027 ⚠️ — виж „Празниците“ по-долу. Допълнителни неприсъствени дни и размени на работни дни, които Министерският съвет обявява поотделно, кодът не знае.

01Какво ще научиш

02Преди да започнеш

Правилото: чл. 72 ЗЗД, дословно

📜
Закон за задълженията и договорите, чл. 72 (по lex.bg, 01.10.2026; последната отразена промяна в заглавието на акта: ДВ, бр. 35 от 27.04.2021 г.)
„Срокът, който се брои по месеци, изтича в съответното число на последния месец; ако този месец няма съответно число, срокът изтича в последния му ден. Срокът, който се брои по седмици, изтича в съответния ден на последната седмица. Когато срокът се брои по дни, не се брои денят на събитието или на момента, от който започва да тече срокът. Срокът изтича в края на последния ден.
Когато последният ден от срока е неприсъствен, срокът свършва в първия следващ присъствен ден.
Когато срокът изтича определено число дни преди известен ден, този ден, както и денят на изтичането на срока не се броят.
Писмените изявления и съобщения, от каквото и да било естество, се считат за извършени в срока, ако са предадени на пощата, по телеграфа или с радиограма до изтичането на двадесет и четвъртия час от последния ден на срока.
Под начало на месеца се разбира първото число, под среда на месеца - петнадесетото число и под край на месеца - последният ден на месеца.“
Това са петте алинеи на члена, по реда им в lex.bg. Кодът в урока покрива първите три; четвъртата и петата не са в него (виж таблицата по-долу).

Кои дни са „неприсъствени“: чл. 154 КТ, дословно

Чл. 72 казва „неприсъствен“, но не изброява кои са тези дни. Използваме общоприетото четене: събота, неделя и официалните празници по Кодекса на труда. Това четене е практика, не буква на чл. 72 ⚠️ — юристът го потвърждава за конкретния договор.

📜
Кодекс на труда, чл. 154 (действаща редакция по lex.bg, 01.10.2026; последната отразена промяна в заглавието на акта: ДВ, бр. 89 от 29.09.2026 г.)
„(1) Официални празници са:
1 януари - Нова година;
3 март - Ден на Освобождението на България от османско иго - национален празник;
1 май - Ден на труда и на международната работническа солидарност;
6 май - Гергьовден, Ден на храбростта и Българската армия;
24 май - Ден на светите братя Кирил и Методий, на българската азбука, просвета и култура и на славянската книжовност;
6 септември - Ден на Съединението;
22 септември - Ден на Независимостта на България;
1 ноември - Ден на народните будители - неприсъствен за всички учебни заведения;
24 декември - Бъдни вечер, 25 и 26 декември - Рождество Христово;
Велики петък, Велика събота и Великден - неделя и понеделник, които в съответната година са определени за празнуването му.
(2) (Нова - ДВ, бр. 105 от 2016 г., в сила от 01.01.2017 г.) Когато официалните празници по ал. 1, с изключение на Великденските празници, съвпадат със събота и/или неделя, първият или първите два работни дни след тях са неприсъствени.
(3) (Доп. - ДВ, бр. 52 от 2004 г., в сила от 01.08.2004 г., изм. - ДВ, бр. 15 от 2010 г., предишна ал. 2, изм. - ДВ, бр. 105 от 2016 г., в сила от 01.01.2017 г.) Министерският съвет може да обявява еднократно и други дни за неприсъствени за оказване на обществена почит към важни исторически, политически, културни или други особено значими събития, както и дни за честване на определени професии и за оказване на признателност.“
Редакционните бележки за минали изменения в началото на чл. 154 и на ал. 1 са пропуснати; текстът на нормата и на трите алинеи е цялостен.
1️⃣
1 ноември
В ал. 1 той е „неприсъствен за всички учебни заведения“. Затова за общия случай не го броим за почивен ден (както и в Обучение 7) ⚠️. Ако договорът е със заведение за обучение, питай юриста.

03Стъпки

  1. Какво строим

    Малки части, всяка се проверява отделно. Защо датата не я смята моделът? Езиков модел греши в аритметиката с дати, а грешката е тиха: изглежда убедително. Кодът със 9 теста е предвидим и може да се провери от юрист ред по ред.

    ЧастВършиПуснато
    deadline_calc.pyправилата на чл. 72 и празниците 2026–2027да, 9/9 теста
    confirm_flow.pyпредложен → потвърден / отхвърлен; напомняния само след „да“да
    deadline_extract.pyлокалният модел намира клаузи; цитатът се сверява с текстане (⚠️)
    SQL схемасрокове и напомнянияне (⚠️)
    n8n процесвсяка работна сутрин в 08:00 праща за днесне (⚠️)
  2. Как чл. 72 става код

    Всяко правило от члена е един ред в таблицата. Примерите са изчислени с кода по-долу (пясъчник, 01.10.2026).

    Какво казва чл. 72Какво прави кодътИзмислен пример → резултат
    дни: денят на събитието не се брои; изтича в края на последния денначало + N дни30 дни от подписване на 01.06.2026 (пн) → 01.07.2026 (ср)
    седмици: в съответния ден на последната седмицаначало + 7·N дни2 седмици от 20.05.2026 (ср) → 03.06.2026 (ср)
    месеци: съответното число; няма ли го — последния ден на месецаadd_months1 месец от 31.01.2027 → 28.02.2027 (нд)
    последният ден е неприсъствен → първият следващ присъственnext_working_day28.02.2027 (нд) → 01.03.2027 (пн)
    същото над празникпропуска и преместените дни5 дни от получена на 08.04.2026 фактура → 13.04 (Великденски понеделник) → 14.04.2026 (вт)
    последен ден в неделя, а понеделникът е преместен почивен денпропуска и двата2 дни от 22.05.2026 → 24.05 (нд), 25.05 (пн, ал. 2) → 26.05.2026 (вт)
    „N дни преди известен ден“: нито той, нито денят на изтичане се броятdays_before; при неприсъствен ден не мести сам, вдига знак3 дни преди 14.10.2026 → 10.10.2026 (сб) → за човека
    ал. 4: изявление, предадено на пощата до 24:00 на последния ден, е в срокане е в кода — това е правило за как се доказва спазването—
    ал. 5: начало / среда / край на месеца (1-во, 15-о, последния ден)не е в кода — ще се въведе като отделен вид начало—
    🧩
    Два прочита — решава човек
    Договор: „за срок от 12 месеца, считано от 01.07.2026“. Дали денят 01.07 е първият ден на срока или „денят на събитието“, който не се брои, зависи от думите и от тълкуването. Кодът със „start = 01.07.2026“ връща 01.07.2027 (чисто прилагане на чл. 72, ал. 1 за месеци); друг прочит дава 30.06.2027. Затова при всяка клауза с „считано от“ е нужен човек и, ако има пари или права, юрист ⚠️.
  3. Кодът за изчисление

    Празниците са за 2026 и 2027 — за друга година кодът спира с грешка, вместо да върне невярна дата. Неприсъствените дни се строят от фиксираните празници, Великден (изчислен, не въведен на ръка) и преместването по ал. 2. 1 ноември не е включен (виж кутията по-горе).

    python · deadline_calc.py
    """Изчисляване на срокове по чл. 72 ЗЗД. Празници 2026-2027 по чл. 154 КТ."""
    import calendar
    from datetime import date, timedelta
    
    SUPPORTED_YEARS = (2026, 2027)
    # Фиксирани официални празници по чл. 154, ал. 1 КТ (месец, ден).
    # 1 ноември е в списъка на закона, но е неприсъствен само за учебните
    # заведения - затова не е тук (за общия случай не е почивен ден).
    FIXED = [(1, 1), (3, 3), (5, 1), (5, 6), (5, 24), (9, 6), (9, 22),
             (12, 24), (12, 25), (12, 26)]
    
    
    def orthodox_easter(year: int) -> date:
        """Православен Великден (юлиански алгоритъм, превод към
        григориански; важи за 1900-2099)."""
        a, b, c = year % 4, year % 7, year % 19
        d = (19 * c + 15) % 30
        e = (2 * a + 4 * b - d + 34) % 7
        month, rest = divmod(d + e + 114, 31)
        return date(year, month, rest + 1) + timedelta(days=13)
    
    
    def non_working_days(year: int) -> set:
        """Великденските дни, фиксираните празници и преместването
        по чл. 154, ал. 2 КТ."""
        if year not in SUPPORTED_YEARS:
            raise ValueError(f"Празниците са заредени само за {SUPPORTED_YEARS}")
        holidays = {date(year, m, d) for m, d in FIXED}
        easter = orthodox_easter(year)
        off = holidays | {easter + timedelta(days=k) for k in (-2, -1, 0, 1)}
        # ал. 2: празник (без Великден) в събота/неделя -> първият работен
        # ден след него е неприсъствен; при два такива дни - първите два.
        for h in sorted(holidays):
            if h.weekday() >= 5:
                n = h + timedelta(days=1)
                while n.weekday() >= 5 or n in off:
                    n += timedelta(days=1)
                off.add(n)
        return off
    
    
    def is_non_working(d: date) -> bool:
        return d.weekday() >= 5 or d in non_working_days(d.year)
    
    
    def next_working_day(d: date) -> date:
        while is_non_working(d):
            d += timedelta(days=1)
        return d
    
    
    def previous_working_day(d: date) -> date:
        while is_non_working(d):
            d -= timedelta(days=1)
        return d
    
    
    def add_months(start: date, n: int) -> date:
        """Същото число на последния месец; ако няма такова - последният му ден."""
        y, m = divmod(start.month - 1 + n, 12)
        year, month = start.year + y, m + 1
        last = calendar.monthrange(year, month)[1]
        return date(year, month, min(start.day, last))
    
    
    def compute_deadline(start: date, n: int, unit: str) -> dict:
        """Срок, броен от събитие в ден `start` (денят на събитието не се брои)."""
        if unit == "days":
            raw = start + timedelta(days=n)
        elif unit == "weeks":
            raw = start + timedelta(weeks=n)
        elif unit == "months":
            raw = add_months(start, n)
        else:
            raise ValueError("unit: days | weeks | months")
        final = next_working_day(raw)
        return {"raw": raw, "final": final, "rolled": final != raw}
    
    
    def days_before(known_day: date, n: int) -> dict:
        """Срок 'n дни преди известен ден': нито този ден, нито денят на
        изтичане се броят. Ако падне в неприсъствен ден - не местим сами."""
        d = known_day - timedelta(days=n + 1)
        return {"raw": d, "final": d, "needs_human": is_non_working(d)}
    
    
    def reminder_dates(deadline: date, offsets=(30, 14, 7, 1), today=None) -> list:
        """Наши напомняния: броим назад, а в неприсъствен ден - към
        предишния работен ден (по-рано е по-безопасно). Минали - не."""
        today = today or date.today()
        out = []
        for k in offsets:
            r = previous_working_day(deadline - timedelta(days=k))
            if r >= today and r not in out:
                out.append(r)
        return sorted(out)
    🧮
    Как е проверен Великден
    Алгоритъмът дава 12.04.2026 и 02.05.2027 за православния Великден. Сверено с таблицата за датите на Великден в Уикипедия (колоната за православния Великден; не е официален източник) ⚠️. Чл. 154 само казва, че Великден е „определен за празнуването му“; че се ползва православният календар е общоприета практика.
  4. Тестовете

    Очакваните стойности са смятани на ръка по текста на закона. Ако промениш правилата, тестовете трябва да паднат.

    python · test_deadline_calc.py
    import unittest
    from datetime import date as D
    from deadline_calc import (orthodox_easter, non_working_days, compute_deadline,
                               days_before, reminder_dates, is_non_working)
    from confirm_flow import Proposed, review
    
    
    def weekday_offs(y):
        return sorted(d for d in non_working_days(y) if d.weekday() < 5)
    
    
    class T(unittest.TestCase):
        def test_easter(self):
            self.assertEqual(orthodox_easter(2026), D(2026, 4, 12))
            self.assertEqual(orthodox_easter(2027), D(2027, 5, 2))
    
        def test_2026(self):
            exp = [D(2026, *x) for x in [(1,1),(3,3),(4,10),(4,13),(5,1),(5,6),
                   (5,25),(9,7),(9,22),(12,24),(12,25),(12,28)]]
            self.assertEqual(weekday_offs(2026), exp)
    
        def test_2027(self):
            exp = [D(2027, *x) for x in [(1,1),(3,3),(4,30),(5,3),(5,4),(5,6),
                   (5,24),(9,6),(9,22),(12,24),(12,27),(12,28)]]
            self.assertEqual(weekday_offs(2027), exp)
    
        def test_days(self):
            r = compute_deadline(D(2026, 6, 1), 30, "days")
            self.assertEqual((r["final"], r["rolled"]), (D(2026, 7, 1), False))
            r = compute_deadline(D(2026, 4, 26), 14, "days")   # нд 10.05
            self.assertEqual((r["raw"], r["final"]), (D(2026, 5, 10), D(2026, 5, 11)))
            r = compute_deadline(D(2026, 4, 8), 5, "days")     # Великд. понеделник
            self.assertEqual((r["raw"], r["final"]), (D(2026, 4, 13), D(2026, 4, 14)))
            r = compute_deadline(D(2026, 5, 22), 2, "days")    # нд 24.05, пн 25.05
            self.assertEqual((r["raw"], r["final"]), (D(2026, 5, 24), D(2026, 5, 26)))
            r = compute_deadline(D(2027, 12, 23), 3, "days")   # нд 26, пропуска 27-28
            self.assertEqual((r["raw"], r["final"]), (D(2027, 12, 26), D(2027, 12, 29)))
    
        def test_months_weeks(self):
            r = compute_deadline(D(2027, 1, 31), 1, "months")
            self.assertEqual((r["raw"], r["final"]), (D(2027, 2, 28), D(2027, 3, 1)))
            self.assertEqual(compute_deadline(D(2026, 7, 1), 12, "months")["final"], D(2027, 7, 1))
            self.assertEqual(compute_deadline(D(2026, 5, 20), 2, "weeks")["final"], D(2026, 6, 3))
    
        def test_before(self):
            self.assertEqual(days_before(D(2026, 10, 10), 3)["raw"], D(2026, 10, 6))
            self.assertTrue(days_before(D(2026, 10, 14), 3)["needs_human"])   # събота 10.10
            self.assertFalse(days_before(D(2026, 10, 10), 3)["needs_human"])
    
        def test_reminders(self):
            self.assertEqual(reminder_dates(D(2026, 7, 1), today=D(2026, 5, 1)),
                [D(2026, 6, 1), D(2026, 6, 17), D(2026, 6, 24), D(2026, 6, 30)])
            self.assertEqual(reminder_dates(D(2026, 5, 26), (1,), today=D(2026, 5, 1)),
                [D(2026, 5, 22)])   # 25.05 е неприсъствен -> пт 22.05
            self.assertEqual(reminder_dates(D(2026, 7, 1), today=D(2026, 6, 20)),
                [D(2026, 6, 24), D(2026, 6, 30)])   # минали не се пращат
    
        def test_year_guard(self):
            with self.assertRaises(ValueError):
                is_non_working(D(2028, 1, 3))
    
        def test_human_confirms(self):
            p = Proposed("Договор А", "в срок от 30 дни", D(2026, 6, 1), 30, "days")
            self.assertEqual(p.reminders, [])
            review(p, "да", D(2026, 6, 1))
            self.assertEqual((p.status, p.final), ("confirmed", D(2026, 7, 1)))
            q = review(Proposed("Договор Б", "x", D(2026, 6, 1), 30, "days"), "не", D(2026, 6, 1))
            self.assertEqual((q.status, q.final, q.reminders), ("rejected", None, []))
    
    
    unittest.main(verbosity=2)

    Пускане: python3 test_deadline_calc.py. Резултатът в пясъчника (Linux, Python 3.10.12, 01.10.2026):

    изход · пясъчник
    test_2026 (__main__.T) ... ok
    test_2027 (__main__.T) ... ok
    test_before (__main__.T) ... ok
    test_days (__main__.T) ... ok
    test_easter (__main__.T) ... ok
    test_human_confirms (__main__.T) ... ok
    test_months_weeks (__main__.T) ... ok
    test_reminders (__main__.T) ... ok
    test_year_guard (__main__.T) ... ok
    
    Ran 9 tests in 0.001s
    OK
  5. Празниците, които кодът получава

    Работните дни, които са неприсъствени, изчислени от кода (уикендите не са в списъка):

    ГодинаДни в седмицата, които са неприсъствени
    202601.01 (чт) · 03.03 (вт) · 10.04 (пт, Велики петък) · 13.04 (пн, Великденски понеделник) · 01.05 (пт) · 06.05 (ср) · 25.05 (пн, за 24.05 в неделя) · 07.09 (пн, за 06.09 в неделя) · 22.09 (вт) · 24.12 (чт) · 25.12 (пт) · 28.12 (пн, за 26.12 в събота)
    202701.01 (пт) · 03.03 (ср) · 30.04 (пт, Велики петък) · 03.05 (пн, Великденски понеделник) · 04.05 (вт, за 01.05 в събота) · 06.05 (чт) · 24.05 (пн) · 06.09 (пн) · 22.09 (ср) · 24.12 (пт) · 27.12 (пн) и 28.12 (вт), за 25 и 26.12 в събота и неделя
    ⚠️
    Какво е сверено и какво — не
    Сверено: списъкът и правилото за преместване — с пълния текст на чл. 154 КТ; списъкът за 2026 съвпада с този в Обучение 7. Не е сверено: 04.05.2027 и 27–28.12.2027 са резултат от правилото, не от публикуван календар; 01.05.2027 съвпада с Велика събота, а ал. 2 изключва само „Великденските празници“ — как точно се прилага при такова припокриване, потвърждава юристът ⚠️. Министерският съвет може да обяви допълнителни неприсъствени дни (ал. 3) и размени на работни дни: провери решението му за годината, преди да разчиташ на датите.
  6. Моделът намира клаузата, не датата

    Моделът връща за всеки срок цитат, брой, единица и как започва срокът. Не му даваме да връща крайна дата. После кодът проверява, че цитатът го има в договора, а числото и единицата са допустими. Непотвърдените остават видими със знак verified: false — човекът ги вижда, не ги губи. Този код не е пускан срещу модел (⚠️); проверен е само синтаксисът.

    python · deadline_extract.py (не е пускан)
    import json
    import re
    
    import httpx
    
    OLLAMA_CHAT = "http://localhost:11434/api/chat"
    UNITS = {"days", "weeks", "months"}
    
    PROMPT = (
        "Намери в договора всички срокове, които се броят във дни, седмици или "
        "месеци. Върни САМО JSON във вида "
        '{"deadlines":[{"quote":"дословен цитат от текста","n":30,'
        '"unit":"days|weeks|months","anchor":"signing|delivery|invoice_received|'
        'fixed_date|other","wording":"точните думи за началото на срока"}]}. '
        "НЕ изчислявай крайна дата. Не добавяй нищо извън текста. "
        "Ако няма срокове: {\"deadlines\":[]}.\n\nДОГОВОР:\n"
    )
    
    
    def norm(s: str) -> str:
        return re.sub(r"\s+", " ", s).strip()
    
    
    def extract(text: str, model: str = "llama3.1:70b") -> list[dict]:
        r = httpx.post(OLLAMA_CHAT, timeout=600, json={
            "model": model,
            "messages": [{"role": "user", "content": PROMPT + text}],
            "format": "json", "stream": False,
            "options": {"temperature": 0.1, "num_ctx": 16384},
        })
        r.raise_for_status()
        found = json.loads(r.json()["message"]["content"]).get("deadlines", [])
        out = []
        for d in found:
            d["verified"] = (
                isinstance(d.get("n"), int) and d.get("unit") in UNITS
                and norm(str(d.get("quote", ""))) in norm(text)
            )
            out.append(d)   # непотвърдените остават видими - човекът ги вижда
        return out

    Llama 3.1 официално не поддържа български (картата на модела), затова очаквай пропуснати и сгрешени клаузи. Начална дата (кога е подписан договорът, кога е получена фактурата) идва от човека или от данните около договора, а не от модела.

  7. Човекът потвърждава всеки срок

    Предложеният срок няма дата. Човек вижда цитата, началото и изчислението (с отбелязан неприсъствен ден) и казва „да“ или „не“. Без „да“ не се създават напомняния. Състоянията са три: proposed, confirmed, rejected.

    python · confirm_flow.py
    from dataclasses import dataclass, field
    from datetime import date
    from deadline_calc import compute_deadline, reminder_dates
    
    
    @dataclass
    class Proposed:
        contract: str          # измислено име на договор
        quote: str             # дословен цитат от договора
        start: date
        n: int
        unit: str
        status: str = "proposed"   # proposed | confirmed | rejected
        final: date | None = None
        reminders: list = field(default_factory=list)
    
    
    def review(p: Proposed, answer: str, today: date) -> Proposed:
        """Човекът вижда цитата и изчислението и казва да/не.
        Без 'да' няма дата и няма напомняния."""
        r = compute_deadline(p.start, p.n, p.unit)
        if answer.strip().lower() in ("y", "yes", "да"):
            p.status, p.final = "confirmed", r["final"]
            p.reminders = reminder_dates(p.final, today=today)
        else:
            p.status = "rejected"
        return p

    Малък диалог върху три измислени срока, с въведени отговори „да“, „да“, „не“ (пясъчник, „днес“ е зададено 01.04.2026):

    изход · пясъчник
    Договор А (измислен)
      цитат: „в 30-дневен срок от подписването“
      начало: 01.06.2026 (пн) · 30 days
      по чл. 72: 01.07.2026 (ср)
      Потвърждаваш ли срока? (да/не):   статус: confirmed | 01.07.2026 (ср) | напомняния: 01.06, 17.06, 24.06, 30.06
    
    Договор А (измислен)
      цитат: „5 дни след получаване на фактурата“
      начало: 08.04.2026 (ср) · 5 days
      по чл. 72: 13.04.2026 (пн) -> 14.04.2026 (вт) (неприсъствен ден)
      Потвърждаваш ли срока? (да/не):   статус: confirmed | 14.04.2026 (вт) | напомняния: 07.04, 09.04
    
    Договор Б (измислен)
      цитат: „предизвестие от един месец“
      начало: 31.01.2027 (нд) · 1 months
      по чл. 72: 28.02.2027 (нд) -> 01.03.2027 (пн) (неприсъствен ден)
      Потвърждаваш ли срока? (да/не):   статус: rejected | няма срок | напомняния: няма

    Виж втория срок: напомнянията са 07.04 и 09.04, не 13.04 и 10.04, защото 10.04 (Велики петък) и 13.04 (Великденски понеделник) са неприсъствени — напомнянето се мести към предишния работен ден. По-рано е по-безопасно. Минали дати не се създават (30 и 14 дни преди 14.04 са преди „днес“).

    ✅
    Напомнянето не е срокът
    Правният срок се смята по чл. 72 и може да се мести напред. Напомнянето е наше вътрешно: то се мести назад, за да не падне в празник, и никога не удължава срока.
  8. Таблиците в PostgreSQL

    Ограничението CHECK не позволява „потвърден“ срок без дата и без име на този, който го е потвърдил — така правилото „няма потвърждение, няма срок“ важи и когато някой пише в базата директно. Схемата не е пускана (⚠️).

    sql · PostgreSQL (не е пускано)
    -- Предложените срокове и потвърдените. Дата има само след потвърждение.
    CREATE TABLE contract_deadlines (
        id            serial PRIMARY KEY,
        contract      text    NOT NULL,
        clause_quote  text    NOT NULL,
        start_date    date    NOT NULL,
        n             integer NOT NULL CHECK (n > 0),
        unit          text    NOT NULL CHECK (unit IN ('days', 'weeks', 'months')),
        deadline      date,
        status        text    NOT NULL DEFAULT 'proposed'
                      CHECK (status IN ('proposed', 'confirmed', 'rejected')),
        confirmed_by  text,
        confirmed_at  timestamptz,
        CHECK (status <> 'confirmed'
               OR (deadline IS NOT NULL AND confirmed_by IS NOT NULL))
    );
    
    -- По един ред за всяка дата на напомняне (след потвърждение).
    CREATE TABLE reminders (
        id          serial PRIMARY KEY,
        deadline_id integer NOT NULL REFERENCES contract_deadlines (id),
        remind_on   date    NOT NULL,
        sent_at     timestamptz,
        UNIQUE (deadline_id, remind_on)
    );
    
    -- Пример: потвърден срок 01.07.2026 и неговите напомняния
    INSERT INTO reminders (deadline_id, remind_on)
    VALUES (1, '2026-06-01'), (1, '2026-06-17'), (1, '2026-06-24'), (1, '2026-06-30');
    
    -- Заявка за n8n: какво да се изпрати днес (<= хваща и пропуснат ден)
    SELECT r.id, d.contract, d.clause_quote, d.deadline,
           (d.deadline - CURRENT_DATE) AS days_left
    FROM reminders r
    JOIN contract_deadlines d ON d.id = r.deadline_id
    WHERE d.status = 'confirmed'
      AND r.sent_at IS NULL
      AND r.remind_on <= CURRENT_DATE
      AND d.deadline >= CURRENT_DATE
    ORDER BY d.deadline;
    
    -- След изпращане (id идва от предишната стъпка)
    UPDATE reminders SET sent_at = now() WHERE id = $1;
  9. n8n: всяка работна сутрин в 08:00

    Процесът е верига от четири нода: Schedule Trigger (Custom Cron 0 0 8 * * 1-5, понеделник до петък в 08:00, зона Europe/Sofia — виж Обучение 7) → Postgres (заявката от предишната стъпка) → Send Email (до deadlines@example.com: договор, цитат, срок, оставащи дни) → Postgres (UPDATE … sent_at).

    • Защо Mon–Fri стига: датите на напомнянията са вече работни дни, а Schedule Trigger не знае празници (Обучение 7).
    • Защо remind_on <= CURRENT_DATE: по подразбиране n8n пропуска изпълнение, ако е бил изключен в часа му. Със „по-малко или равно“ пропуснато напомняне се изпраща следващата сутрин, а sent_at не позволява двойно изпращане.
    • Публикуване: графикът върви само след „Publish“; „Execute Workflow“ не доказва, че часът работи.
    • Пощата е външна услуга: в писмото влиза само цитатът и датата, не целият договор.

    Имената на полетата за параметри на Postgres нода (за $1) и на изпращача се различават по версия — виж документацията на твоята версия ⚠️; тази част не е пускана.

  10. Минимум, преди да му повярваш

    1. Пусни тестовете; промени една дата в тест и виж как падат.
    2. Сложи три измислени договора и сверѝ ръчно с календар всяка дата.
    3. Покажи на юриста таблицата от стъпка 2 и кутията „Два прочита“ — той решава как се четат „считано от“ и „преди известен ден“.
    4. Сверявай празниците всяка година с решението на Министерския съвет.

04Проверка

Тест

1. Срок „30 дни от подписването“; договорът е подписан на 01.06.2026. Кога изтича по чл. 72 ЗЗД?

2. Последният ден от срока е неделя, а понеделникът е почивен по чл. 154, ал. 2 КТ. Кога свършва срокът?

3. Защо датата не я смята езиковият модел?

4. Кога се създават напомняния за един предложен срок?

05Какво следва

06Източници

  1. Закон за задълженията и договорите (lex.bg) — чл. 72 и чл. 111, цитирани дословно; проверено 01.10.2026. Lex.bg не е официалното издание: сверка с Държавен вестник (dv.parliament.bg) ⚠️.
  2. Кодекс на труда (lex.bg) — чл. 154, ал. 1–3, дословно; проверено 01.10.2026.
  3. Дати на Великден (Уикипедия) — само за сверка на 12.04.2026 и 02.05.2027; не е официален източник.
  4. Обучение 7 · Schedules и Cron jobs · n8n: Schedule Trigger · n8n: Send Email 🌐 външна услуга — зона, пропуснати изпълнения, изпращане на писма (както са посочени в Обучение 7).
  5. llama3.1 в Ollama 🔒 локално · Ollama: API — размери, поддържани езици, format (както в урок 04-135).