#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Презентация: оплата мойки через Forte (кошелёк + карта)."""

from reportlab.lib.colors import Color, HexColor, white, black
from reportlab.lib.units import mm, inch
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas

pdfmetrics.registerFont(TTFont("Sans", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"))
pdfmetrics.registerFont(TTFont("Sans-Bold", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"))
pdfmetrics.registerFont(TTFont("Mono", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"))

W, H = 13.333 * inch, 7.5 * inch  # 16:9

NAVY = HexColor("#1B2A4A")
NAVY2 = HexColor("#243656")
ACCENT = HexColor("#C41E3A")
TEAL = HexColor("#1A7A6D")
GOLD = HexColor("#B8860B")
BG = HexColor("#F4F6F9")
TEXT = HexColor("#1C2434")
MUTED = HexColor("#5A6577")
LINE = HexColor("#D5DCE6")
ROW = HexColor("#EEF2F7")
OK = HexColor("#1B7A4E")
BAD = HexColor("#B42318")
SOFT = HexColor("#E8EEF6")
TEAL_SOFT = HexColor("#E5F3F0")
RED_SOFT = HexColor("#F8E8EB")
GOLD_SOFT = HexColor("#F7F0DE")
WHITE = white

OUT = "/var/www/html/mobridge/storage/app/Forte_moika_prezentaciya.pdf"
TOTAL = 14


def wrap(c, text, font, size, max_w):
    words = str(text).split()
    if not words:
        return [""]
    lines, cur = [], words[0]
    for w in words[1:]:
        trial = cur + " " + w
        if c.stringWidth(trial, font, size) <= max_w:
            cur = trial
        else:
            lines.append(cur)
            cur = w
    lines.append(cur)
    return lines


def wrap_hard(c, text, font, size, max_w):
    """Wrap also splitting long tokens (URLs, paths)."""
    raw = str(text)
    if c.stringWidth(raw, font, size) <= max_w:
        return [raw]
    lines, buf = [], ""
    for ch in raw:
        trial = buf + ch
        if c.stringWidth(trial, font, size) <= max_w:
            buf = trial
        else:
            if buf:
                lines.append(buf)
            buf = ch
    if buf:
        lines.append(buf)
    return lines or [""]


def chrome(c, n, title, subtitle=None):
    c.setFillColor(BG)
    c.rect(0, 0, W, H, fill=1, stroke=0)
    c.setFillColor(NAVY)
    c.rect(0, H - 18 * mm, W, 18 * mm, fill=1, stroke=0)
    c.setFillColor(ACCENT)
    c.rect(0, H - 18 * mm, 3.2 * mm, 18 * mm, fill=1, stroke=0)
    c.setFillColor(WHITE)
    c.setFont("Sans-Bold", 16)
    c.drawString(10 * mm, H - 11.5 * mm, title)
    if subtitle:
        c.setFont("Sans", 9)
        c.setFillColor(HexColor("#C5CDDB"))
        c.drawRightString(W - 10 * mm, H - 11.2 * mm, subtitle)
    c.setFillColor(NAVY)
    c.rect(0, 0, W, 8.5 * mm, fill=1, stroke=0)
    c.setFillColor(HexColor("#C5CDDB"))
    c.setFont("Sans", 8)
    c.drawString(10 * mm, 3 * mm, "Forte  ×  Мойка  ·  кошелёк, карта, шлюз оплаты")
    c.drawRightString(W - 10 * mm, 3 * mm, f"{n}  /  {TOTAL}")


def rounded(c, x, y, w, h, r=6, fill=None, stroke=None, sw=0.6):
    c.saveState()
    if fill:
        c.setFillColor(fill)
    if stroke:
        c.setStrokeColor(stroke)
        c.setLineWidth(sw)
    p = c.beginPath()
    p.moveTo(x + r, y)
    p.lineTo(x + w - r, y)
    p.arcTo(x + w - 2 * r, y, x + w, y + 2 * r, -90, 90)
    p.lineTo(x + w, y + h - r)
    p.arcTo(x + w - 2 * r, y + h - 2 * r, x + w, y + h, 0, 90)
    p.lineTo(x + r, y + h)
    p.arcTo(x, y + h - 2 * r, x + 2 * r, y + h, 90, 90)
    p.lineTo(x, y + r)
    p.arcTo(x, y, x + 2 * r, y + 2 * r, 180, 90)
    p.close()
    c.drawPath(p, fill=1 if fill else 0, stroke=1 if stroke else 0)
    c.restoreState()


def arrow_h(c, x1, x2, y, color=ACCENT):
    c.setStrokeColor(color)
    c.setFillColor(color)
    c.setLineWidth(1.4)
    c.line(x1, y, x2 - 3.2 * mm, y)
    p = c.beginPath()
    p.moveTo(x2, y)
    p.lineTo(x2 - 3.2 * mm, y + 1.4 * mm)
    p.lineTo(x2 - 3.2 * mm, y - 1.4 * mm)
    p.close()
    c.drawPath(p, fill=1, stroke=0)


def arrow_v(c, x, y1, y2, color=ACCENT):
    """y1 above y2 (downward)."""
    c.setStrokeColor(color)
    c.setFillColor(color)
    c.setLineWidth(1.4)
    c.line(x, y1, x, y2 + 3.2 * mm)
    p = c.beginPath()
    p.moveTo(x, y2)
    p.lineTo(x - 1.4 * mm, y2 + 3.2 * mm)
    p.lineTo(x + 1.4 * mm, y2 + 3.2 * mm)
    p.close()
    c.drawPath(p, fill=1, stroke=0)


def table(c, x, y_top, col_w, headers, rows, font=8, header_fill=NAVY, alt=ROW, max_h=None):
    """Draw table downward from y_top. Returns bottom y."""
    pad = 2.4 * mm
    header_h = 8.2 * mm
    # header
    tx = x
    c.setFillColor(header_fill)
    c.rect(x, y_top - header_h, sum(col_w), header_h, fill=1, stroke=0)
    c.setFillColor(WHITE)
    c.setFont("Sans-Bold", font)
    for i, h in enumerate(headers):
        c.drawString(tx + pad, y_top - header_h + 2.8 * mm, h)
        tx += col_w[i]
    y = y_top - header_h
    for ri, row in enumerate(rows):
        wrapped = []
        row_h = 0
        for i, cell in enumerate(row):
            lines = wrap(c, cell, "Sans", font, col_w[i] - 2 * pad)
            wrapped.append(lines)
            row_h = max(row_h, 3.2 * mm + len(lines) * (font + 3.2))
        row_h = max(row_h, 7.4 * mm)
        c.setFillColor(WHITE if ri % 2 else alt)
        c.rect(x, y - row_h, sum(col_w), row_h, fill=1, stroke=0)
        c.setStrokeColor(LINE)
        c.setLineWidth(0.3)
        c.line(x, y - row_h, x + sum(col_w), y - row_h)
        tx = x
        for i, lines in enumerate(wrapped):
            c.setFillColor(TEXT)
            c.setFont("Sans", font)
            ly = y - 4.6 * mm
            for ln in lines:
                c.drawString(tx + pad, ly, ln)
                ly -= font + 2.4
            tx += col_w[i]
        y -= row_h
    c.setStrokeColor(NAVY)
    c.setLineWidth(0.7)
    c.rect(x, y, sum(col_w), y_top - y, fill=0, stroke=1)
    return y


def bullet(c, x, y, text, max_w, size=10, color=TEXT, bullet_color=ACCENT):
    c.setFillColor(bullet_color)
    c.circle(x + 1.4 * mm, y + 1.2 * mm, 1.15 * mm, fill=1, stroke=0)
    c.setFillColor(color)
    c.setFont("Sans", size)
    lines = wrap(c, text, "Sans", size, max_w - 6 * mm)
    for i, ln in enumerate(lines):
        c.drawString(x + 5 * mm, y - i * (size + 3), ln)
    return y - len(lines) * (size + 3) - 2.6 * mm


def card(c, x, y, w, h, title, body_lines, accent=TEAL, title_size=11):
    rounded(c, x, y, w, h, r=7, fill=WHITE, stroke=LINE, sw=0.6)
    c.setFillColor(accent)
    c.rect(x, y + h - 1.8 * mm, w, 1.8 * mm, fill=1, stroke=0)
    c.setFillColor(NAVY)
    c.setFont("Sans-Bold", title_size)
    c.drawString(x + 4 * mm, y + h - 8.2 * mm, title)
    c.setFillColor(TEXT)
    c.setFont("Sans", 8.5)
    ty = y + h - 13.5 * mm
    for line in body_lines:
        for ln in wrap(c, line, "Sans", 8.5, w - 8 * mm):
            c.drawString(x + 4 * mm, ty, ln)
            ty -= 4.0 * mm
        ty -= 0.6 * mm


# ───────────────────────── slides ─────────────────────────


def s01_title(c):
    c.setFillColor(NAVY)
    c.rect(0, 0, W, H, fill=1, stroke=0)
    c.setFillColor(ACCENT)
    c.rect(0, 0, 8 * mm, H, fill=1, stroke=0)
    c.setFillColor(GOLD)
    c.rect(8 * mm, 0, 1.2 * mm, H, fill=1, stroke=0)

    c.setFillColor(HexColor("#C5CDDB"))
    c.setFont("Sans", 11)
    c.drawString(22 * mm, H - 28 * mm, "АРХИТЕКТУРА ОПЛАТ  ·  КОНФИДЕНЦИАЛЬНО  ·  ДЛЯ ПРЕЗЕНТАЦИИ")

    c.setFillColor(WHITE)
    c.setFont("Sans-Bold", 32)
    c.drawString(22 * mm, H - 48 * mm, "Оплата мойки через Forte")
    c.setFont("Sans", 16)
    c.setFillColor(HexColor("#D5DCE6"))
    c.drawString(22 * mm, H - 60 * mm, "Кошелёк приложения  →  банковская карта  →  шлюз Forte")

    # three pills
    items = [
        ("01", "Ссылка на оплату", "Пополнение кошелька\nна странице банка"),
        ("02", "Списание с карты", "Отдельный запрос,\nесли кошелька не хватает"),
        ("03", "Статус и callback", "Проверка оплаты\nи уведомление мойки"),
    ]
    x = 22 * mm
    for num, t, d in items:
        rounded(c, x, 32 * mm, 95 * mm, 42 * mm, r=8, fill=NAVY2, stroke=None)
        c.setFillColor(ACCENT)
        c.setFont("Sans-Bold", 14)
        c.drawString(x + 6 * mm, 62 * mm, num)
        c.setFillColor(WHITE)
        c.setFont("Sans-Bold", 12)
        c.drawString(x + 6 * mm, 54 * mm, t)
        c.setFillColor(HexColor("#C5CDDB"))
        c.setFont("Sans", 9)
        yy = 46 * mm
        for ln in d.split("\n"):
            c.drawString(x + 6 * mm, yy, ln)
            yy -= 4.4 * mm
        x += 102 * mm

    c.setFillColor(HexColor("#8A94A8"))
    c.setFont("Sans", 9)
    c.drawString(22 * mm, 14 * mm, "Версия 1.0  ·  август 2026  ·  14 слайдов  ·  payments + client_payment_info")
    c.drawRightString(W - 16 * mm, 14 * mm, "1  /  14")


def s02_task(c):
    chrome(c, 2, "1.  Задача", "Зачем это нужно")
    y = H - 28 * mm
    for t in [
        "В приложении мойки есть внутренний кошелёк: с него оплачивают мойку и на него кладут деньги.",
        "Пополнение кошелька идёт через этот сервер — платёжный шлюз Forte.",
        "Если в кошельке не хватает средств — нужно списать недостачу с привязанной карты, отдельным запросом.",
        "Если на карте тоже пусто — долг живёт на мойке; когда деньги появятся, списание повторяется.",
    ]:
        y = bullet(c, 10 * mm, y, t, W - 22 * mm, size=11)

    table(
        c,
        10 * mm,
        y - 2 * mm,
        [55 * mm, 130 * mm, 130 * mm],
        ["Требование", "Как выглядит для клиента", "Что делает система"],
        [
            ["Пополнить кошелёк", "Открывает ссылку и платит на странице банка", "CreateOrder → ссылка → callback мойке"],
            ["Мойка при пустом кошельке", "Не вводит карту повторно", "execTran Recurring по сохранённому токену"],
            ["Нет денег на карте", "Мойка / долг по правилам сервиса", "Отказ банка → долг + ретрай на мойке"],
            ["Карты клиентов", "Видит маску карты в приложении", "Токен Forte хранит сервер моек, не шлюз"],
        ],
        font=8,
    )


def s03_roles(c):
    chrome(c, 3, "2.  Кто за что отвечает", "Границы систем")
    cols = [
        (NAVY, "Приложение мойки", ["Экран мойки и кошелька", "Согласие на автосписание", "Показ маски карты"]),
        (TEAL, "Сервер моек", ["Баланс кошелька", "Долги и ретраи charge", "Картотека: токен Forte, маска", "Решение: кошелёк или карта"]),
        (ACCENT, "Этот сервер — шлюз", ["Создать заказ и ссылку", "execTran без клиента", "Опрос getOrder (cron)", "Callback на мойку"]),
        (GOLD, "Forte Bank", ["Платёжная страница, 3DS", "Списание с карты", "CoF — сохранённая карта", "Отказ, если нет денег"]),
    ]
    x = 8 * mm
    for color, title, lines in cols:
        rounded(c, x, 22 * mm, 78 * mm, 128 * mm, r=8, fill=WHITE, stroke=LINE)
        c.setFillColor(color)
        c.roundRect(x, 132 * mm, 78 * mm, 18 * mm, 8, fill=1, stroke=0)
        c.rect(x, 132 * mm, 78 * mm, 9 * mm, fill=1, stroke=0)
        c.setFillColor(WHITE)
        c.setFont("Sans-Bold", 10)
        c.drawCentredString(x + 39 * mm, 139 * mm, title)
        ty = 122 * mm
        for i, ln in enumerate(lines):
            c.setFillColor(color)
            c.circle(x + 7 * mm, ty + 1.4 * mm, 1.3 * mm, fill=1, stroke=0)
            c.setFillColor(TEXT)
            c.setFont("Sans", 9)
            for wln in wrap(c, ln, "Sans", 9, 62 * mm):
                c.drawString(x + 12 * mm, ty, wln)
                ty -= 4.6 * mm
            ty -= 4.5 * mm
        x += 82 * mm

    c.setFillColor(MUTED)
    c.setFont("Sans", 9)
    c.drawString(
        10 * mm,
        14 * mm,
        "Шлюз не знает баланс кошелька. Мойка говорит: «дай ссылку» или «спиши N тенге с карты».",
    )


def s04_forte(c):
    chrome(c, 4, "3.  Что умеет и чего нет у Forte", "Ограничения банка")
    table(
        c,
        10 * mm,
        H - 28 * mm,
        [155 * mm, 160 * mm],
        ["Есть в API Forte", "Нет в API Forte"],
        [
            ["CreateOrder — создать заказ и ссылку на оплату", "Проверить баланс карты заранее"],
            ["CoF (Card on File) — сохранить карту при первой оплате", "Узнать, что клиент пополнил банковскую карту"],
            ["execTran Recurring — списать без клиента и без CVV", "Поставить списание «в очередь до пополнения карты»"],
            ["getOrder — статус заказа (FullyPaid, Declined…)", "Входящий webhook от банка (только опрос)"],
            ["HTTP Basic Auth, статусы ордера, коды ошибок", "Холд «на будущее», если денег сейчас нет"],
        ],
        font=9,
    )
    rounded(c, 10 * mm, 18 * mm, W - 20 * mm, 28 * mm, r=7, fill=RED_SOFT, stroke=ACCENT, sw=0.8)
    c.setFillColor(ACCENT)
    c.setFont("Sans-Bold", 11)
    c.drawString(16 * mm, 38 * mm, "Ключевой вывод")
    c.setFillColor(TEXT)
    c.setFont("Sans", 10)
    c.drawString(
        16 * mm,
        26 * mm,
        "«Списать, когда на карту положат» — это не функция банка. Это ретраи charge на сервере моек плюс погашение долга после успешного пополнения кошелька.",
    )


def s05_flow(c):
    chrome(c, 5, "4.  Бизнес-цепочка", "Кошелёк  →  карта  →  долг")

    def node(x, y, w, h, t, fill, tc=WHITE):
        rounded(c, x, y, w, h, r=6, fill=fill, stroke=None)
        c.setFillColor(tc)
        c.setFont("Sans-Bold", 8.5)
        lines = wrap(c, t, "Sans-Bold", 8.5, w - 6 * mm)
        start = y + h / 2 + (len(lines) - 1) * 2.2 * mm
        for i, ln in enumerate(lines):
            c.drawCentredString(x + w / 2, start - i * 4.2 * mm, ln)

    def tag(x, y, t, col):
        c.setFillColor(col)
        c.setFont("Sans-Bold", 7)
        c.drawCentredString(x, y, t)

    node(12 * mm, 128 * mm, 50 * mm, 16 * mm, "Клиент на мойке", NAVY)
    arrow_h(c, 64 * mm, 78 * mm, 136 * mm, TEAL)
    node(78 * mm, 128 * mm, 58 * mm, 16 * mm, "Хватает в кошельке?", TEAL)
    tag(144 * mm, 140 * mm, "да", OK)
    c.setStrokeColor(OK)
    c.setLineWidth(1.3)
    c.line(136 * mm, 136 * mm, 152 * mm, 136 * mm)
    node(152 * mm, 128 * mm, 70 * mm, 16 * mm, "Списать с кошелька", OK)
    node(230 * mm, 128 * mm, 90 * mm, 16 * mm, "Forte не вызывается  ·  готово", HexColor("#2E4A3A"), WHITE)

    tag(111 * mm, 118 * mm, "нет", ACCENT)
    arrow_v(c, 107 * mm, 128 * mm, 108 * mm, ACCENT)
    node(78 * mm, 92 * mm, 58 * mm, 16 * mm, "Карта сохранена?", ACCENT)
    tag(70 * mm, 108 * mm, "нет", GOLD)
    node(12 * mm, 92 * mm, 52 * mm, 16 * mm, "Ссылка  POST /link", GOLD, NAVY)
    c.setStrokeColor(GOLD)
    c.setFillColor(GOLD)
    c.setLineWidth(1.4)
    c.line(64 * mm, 100 * mm, 76 * mm, 100 * mm)
    p = c.beginPath()
    p.moveTo(64 * mm, 100 * mm)
    p.lineTo(64 * mm + 3.2 * mm, 100 * mm + 1.4 * mm)
    p.lineTo(64 * mm + 3.2 * mm, 100 * mm - 1.4 * mm)
    p.close()
    c.drawPath(p, fill=1, stroke=0)
    tag(144 * mm, 108 * mm, "да", ACCENT)
    arrow_h(c, 136 * mm, 152 * mm, 100 * mm, ACCENT)
    node(152 * mm, 92 * mm, 70 * mm, 16 * mm, "POST /charge  недостача", NAVY2)
    arrow_h(c, 222 * mm, 236 * mm, 100 * mm, MUTED)
    node(236 * mm, 100 * mm, 84 * mm, 14 * mm, "Успех → кошелёк+ → мойка", OK)
    node(236 * mm, 80 * mm, 84 * mm, 14 * mm, "Отказ → ДОЛГ на мойке", BAD)

    arrow_v(c, 278 * mm, 80 * mm, 58 * mm, BAD)
    node(152 * mm, 42 * mm, 168 * mm, 16 * mm, "Позже: ретрай /charge   или   клиент пополнил кошелёк по ссылке → долг гасится", GOLD, NAVY)

    c.setFillColor(MUTED)
    c.setFont("Sans", 8.5)
    c.drawString(12 * mm, 22 * mm, "Банк не ждёт пополнения карты. Ждёт мойка: повторяет charge или закрывает долг после FullyPaid по ссылке.")


def s06_ops(c):
    chrome(c, 6, "5.  Три операции шлюза", "Это весь API для мойки")
    ops = [
        ("A", TEAL, "Ссылка  ·  POST /v1/wash/forte/link", [
            "Клиент в приложении, готов платить сам",
            "save_card=true → заказ Order_COF (привязка карты)",
            "save_card=false → обычный Order_RID",
            "Ответ: link, order_id, payment_id",
            "Клиент платит на странице банка",
            "Cron getOrder → FullyPaid → callback + card{}",
        ]),
        ("B", ACCENT, "Списание  ·  POST /v1/wash/forte/charge", [
            "Кошелька не хватает, карта уже привязана",
            "Мойка шлёт client_rid и сумму — не PAN",
            "CreateOrder(Order_COF) + execTran Recurring",
            "Успех → callback success, кошелёк / долг",
            "PmoDecline → callback declined, долг на мойке",
            "Ретраи долга запускает мойка, не шлюз",
        ]),
        ("C", GOLD, "Статус  ·  getOrder + callback", [
            "У банка нет webhook — только опрос",
            "Cron раз в минуту по pending",
            "Или GET /v1/wash/forte/status/{order_id}",
            "Терминальный статус → один callback",
            "notified_at — без повторных уведомлений",
            "Мойка зачисляет баланс и закрывает долг",
        ]),
    ]
    x = 8 * mm
    for letter, color, title, lines in ops:
        rounded(c, x, 18 * mm, 105 * mm, 132 * mm, r=8, fill=WHITE, stroke=LINE)
        c.setFillColor(color)
        c.circle(x + 10 * mm, 138 * mm, 6 * mm, fill=1, stroke=0)
        c.setFillColor(WHITE)
        c.setFont("Sans-Bold", 14)
        c.drawCentredString(x + 10 * mm, 135.8 * mm, letter)
        c.setFillColor(NAVY)
        c.setFont("Sans-Bold", 9.5)
        for i, ln in enumerate(wrap(c, title, "Sans-Bold", 9.5, 84 * mm)):
            c.drawString(x + 20 * mm, 138 * mm - i * 4.5 * mm, ln)
        ty = 122 * mm
        for ln in lines:
            c.setFillColor(color)
            c.circle(x + 8 * mm, ty + 1.2 * mm, 1.1 * mm, fill=1, stroke=0)
            c.setFillColor(TEXT)
            c.setFont("Sans", 8.5)
            for wln in wrap(c, ln, "Sans", 8.5, 88 * mm):
                c.drawString(x + 13 * mm, ty, wln)
                ty -= 4.2 * mm
            ty -= 2.8 * mm
        x += 109 * mm


def s07_data(c):
    chrome(c, 7, "6.  Где какие данные", "Карты — на мойке, заказы — на шлюзе")

    rounded(c, 10 * mm, 22 * mm, 155 * mm, 128 * mm, r=8, fill=WHITE, stroke=TEAL, sw=1.2)
    c.setFillColor(TEAL)
    c.roundRect(10 * mm, 134 * mm, 155 * mm, 16 * mm, 8, fill=1, stroke=0)
    c.rect(10 * mm, 134 * mm, 155 * mm, 8 * mm, fill=1, stroke=0)
    c.setFillColor(WHITE)
    c.setFont("Sans-Bold", 13)
    c.drawCentredString(87.5 * mm, 140 * mm, "Сервер МОЕК")
    items_l = [
        ("wallet", "Баланс внутреннего кошелька"),
        ("debts", "Долг, если charge не прошёл"),
        ("client_cards", "src_token_id, маска, brand, consumer_id"),
        ("user", "Стабильный client_rid = consumer.rid"),
        ("запрет", "PAN, CVV, полный номер карты — не хранить"),
    ]
    ty = 122 * mm
    for k, v in items_l:
        fill = RED_SOFT if k == "запрет" else TEAL_SOFT
        rounded(c, 16 * mm, ty - 4 * mm, 143 * mm, 14 * mm, r=4, fill=fill, stroke=None)
        c.setFillColor(BAD if k == "запрет" else TEAL)
        c.setFont("Sans-Bold", 8)
        c.drawString(20 * mm, ty + 3 * mm, k)
        c.setFillColor(TEXT)
        c.setFont("Sans", 8.5)
        c.drawString(48 * mm, ty + 3 * mm, v)
        ty -= 18 * mm

    rounded(c, 175 * mm, 22 * mm, 155 * mm, 128 * mm, r=8, fill=WHITE, stroke=ACCENT, sw=1.2)
    c.setFillColor(ACCENT)
    c.roundRect(175 * mm, 134 * mm, 155 * mm, 16 * mm, 8, fill=1, stroke=0)
    c.rect(175 * mm, 134 * mm, 155 * mm, 8 * mm, fill=1, stroke=0)
    c.setFillColor(WHITE)
    c.setFont("Sans-Bold", 13)
    c.drawCentredString(252.5 * mm, 140 * mm, "Этот сервер — шлюз Forte")
    items_r = [
        ("payments", "Заказ банка: order_id, password, сумма, статус, ссылка"),
        ("client_payment_info", "Хвост мойки 1:1: rid, callback, ошибки"),
        ("card_payload", "Снимок токена только чтобы отдать мойке"),
        ("cron", "getOrder по pending, один callback"),
        ("запрет", "Не картотека клиентов и не баланс кошелька"),
    ]
    ty = 122 * mm
    for k, v in items_r:
        fill = HexColor("#F3E6E8") if k == "запрет" else RED_SOFT
        rounded(c, 181 * mm, ty - 4 * mm, 143 * mm, 14 * mm, r=4, fill=fill, stroke=None)
        c.setFillColor(BAD if k == "запрет" else ACCENT)
        c.setFont("Sans-Bold", 8)
        c.drawString(185 * mm, ty + 3 * mm, k)
        c.setFillColor(TEXT)
        c.setFont("Sans", 8.5)
        for ln in wrap(c, v, "Sans", 8.5, 100 * mm):
            c.drawString(230 * mm, ty + 3 * mm, ln)
            break
        ty -= 18 * mm


def s08_db(c):
    chrome(c, 8, "7.  База: payments + client_payment_info", "payments не расширяем — коннекторы не ломаем")
    c.setFillColor(NAVY)
    c.setFont("Sans-Bold", 11)
    c.drawString(10 * mm, H - 28 * mm, "payments  —  как сейчас, без новых столбцов")
    table(
        c,
        10 * mm,
        H - 32 * mm,
        [48 * mm, 70 * mm, 197 * mm],
        ["Поле", "Значение для мойки", "Роль"],
        [
            ["module_type", "wash", "Отделить от Bitrix / amo / AIOKK"],
            ["payment_type", "wash_forte_link | wash_forte_charge", "Тип операции шлюза"],
            ["order_id / password", "из ответа Forte", "Идентификатор заказа банка"],
            ["amount / currency", "сумма операции", "Пополнение или charge"],
            ["payment_status", "pending / success / declined…", "Наш статус"],
            ["generated_link", "ссылка HPP (у charge — null)", "Открыть в приложении"],
        ],
        font=7.5,
    )
    c.setFillColor(NAVY)
    c.setFont("Sans-Bold", 11)
    c.drawString(10 * mm, 78 * mm, "client_payment_info  —  новая таблица, связь 1:1  (payment_id UNIQUE → payments.id)")
    table(
        c,
        10 * mm,
        74 * mm,
        [48 * mm, 55 * mm, 212 * mm],
        ["Поле", "Тип", "Зачем"],
        [
            ["payment_id", "FK unique", "Один платёж — одна строка info"],
            ["client_rid", "varchar 64", "ID клиента мойки = consumer.rid в Forte"],
            ["wash_payment_id", "varchar 64", "ID операции на сервере моек"],
            ["callback_url", "varchar 500", "Куда слать статус"],
            ["operation_type", "link | charge", "Какой сценарий"],
            ["save_card / forte_status", "bool / varchar", "CoF и сырой статус банка"],
            ["error_* / notified_at", "varchar / ts", "Отказ банка и защита от двойного callback"],
            ["card_payload", "json null", "Снимок токена для мойки, не картотека"],
        ],
        font=7.5,
    )


def s09_contract(c):
    chrome(c, 9, "8.  Контракт API", "Мойка  ↔  шлюз")
    c.setFillColor(TEAL)
    c.setFont("Sans-Bold", 10)
    c.drawString(10 * mm, H - 27 * mm, "POST  /api/v1/wash/forte/link     и     POST  /api/v1/wash/forte/charge")
    table(
        c,
        10 * mm,
        H - 31 * mm,
        [48 * mm, 42 * mm, 42 * mm, 183 * mm],
        ["Поле", "link", "charge", "Смысл"],
        [
            ["amount, currency", "да", "да", "Сумма пополнения или недостачи"],
            ["description", "да", "да", "Текст в заказе банка"],
            ["client_rid", "да", "да", "Стабильный id клиента, consumer.rid"],
            ["wash_payment_id", "да", "да", "Ид операции мойки"],
            ["callback_url", "да", "да", "URL для уведомления"],
            ["hpp_redirect_url", "да", "нет", "Куда вернуть клиента со страницы банка"],
            ["save_card", "да (bool)", "нет", "true = Order_COF, привязка карты"],
            ["domain", "да", "да", "Идентификатор инстанса мойки"],
            ["данные карты", "нет", "нет", "PAN не передаём; Forte ищет CoF по rid"],
        ],
        font=7.5,
    )
    c.setFillColor(MUTED)
    c.setFont("Sans", 8)
    c.drawString(10 * mm, 18 * mm, "Ответ link: { payment_id, order_id, link, status: pending }     ·     Ответ charge: { payment_id, order_id, status: success | declined | pending }")


def s10_callback(c):
    chrome(c, 10, "8.  Контракт: callback шлюз → мойка", "После FullyPaid / Declined / Expired / Cancelled")
    rounded(c, 10 * mm, 88 * mm, 175 * mm, 62 * mm, r=7, fill=NAVY, stroke=None)
    c.setFillColor(GOLD)
    c.setFont("Sans-Bold", 9)
    c.drawString(16 * mm, 140 * mm, "JSON callback")
    payload = [
        '{',
        '  "wash_payment_id": "wp-456",',
        '  "order_id": 1000000000475,',
        '  "status": "success",',
        '  "amount": "1000.00",  "currency": "KZT",',
        '  "client_rid": "user-123",',
        '  "card": { "src_token_id": 1681, "mask": "444499******7777", ... }',
        '}',
    ]
    c.setFillColor(HexColor("#E8EEF6"))
    c.setFont("Mono", 8)
    yy = 132 * mm
    for ln in payload:
        c.drawString(16 * mm, yy, ln)
        yy -= 5.2 * mm

    table(
        c,
        192 * mm,
        H - 28 * mm,
        [42 * mm, 93 * mm],
        ["Поле", "Что делает мойка"],
        [
            ["status=success", "Зачислить кошелёк / закрыть долг"],
            ["status=declined", "Записать долг, запланировать ретрай"],
            ["card{}", "Сохранить в client_cards (после CoF)"],
            ["error_code", "Показать причину / лимит ретраев"],
            ["notified_at на шлюзе", "Повторно не шлём, если 2xx"],
        ],
        font=7.5,
    )
    rounded(c, 10 * mm, 18 * mm, W - 20 * mm, 62 * mm, r=7, fill=WHITE, stroke=LINE)
    c.setFillColor(NAVY)
    c.setFont("Sans-Bold", 11)
    c.drawString(16 * mm, 68 * mm, "GET  /api/v1/wash/forte/status/{order_id}     — страховка, если callback не дошёл")
    y = 56 * mm
    for t in [
        "Cron шлюза каждую минуту опрашивает pending (wash_forte_link и wash_forte_charge).",
        "Блок card заполняется после первой CoF-оплаты — мойка кладёт токен в свою таблицу client_cards.",
        "Ретрай долга = новый POST /charge = новая пара payments + client_payment_info. Старую строку не переписываем.",
    ]:
        y = bullet(c, 16 * mm, y, t, W - 40 * mm, size=9.5)


def s11_scenarios(c):
    chrome(c, 11, "9.  Сценарии 1–4", "Для согласования с продуктом")
    sc = [
        (TEAL, "1. Первое пополнение и привязка карты",
         ["Клиент: «Пополнить 1000»", "Мойка → POST /link (save_card=true)", "Клиент платит на странице Forte",
          "Cron → FullyPaid → callback + card{}", "Мойка: кошелёк +1000, сохранить карту"]),
        (NAVY, "2. Мойка, кошелька хватает",
         ["Кошелёк 2000, мойка 800", "Мойка списывает 800 у себя", "Forte не вызывается",
          "Шлюз и банк не участвуют", "Самый частый и дешёвый путь"]),
        (ACCENT, "3. Не хватает кошелька, карта с деньгами",
         ["Кошелёк 200, мойка 800, недостача 600", "Мойка → POST /charge 600", "Forte execTran OK",
          "Callback success → кошелёк +600", "Списать 800, открыть мойку"]),
        (GOLD, "4. На карте нет денег",
         ["charge → PmoDecline", "Мойка: долг 600 ₸", "Позже cron мойки снова /charge",
          "Или клиент сам /link", "После success долг гасится из кошелька"]),
    ]
    positions = [(8 * mm, 84 * mm), (172 * mm, 84 * mm), (8 * mm, 18 * mm), (172 * mm, 18 * mm)]
    for (color, title, lines), (x, y) in zip(sc, positions):
        rounded(c, x, y, 158 * mm, 62 * mm, r=7, fill=WHITE, stroke=LINE)
        c.setFillColor(color)
        c.rect(x, y + 56 * mm, 158 * mm, 6 * mm, fill=1, stroke=0)
        c.setFillColor(NAVY)
        c.setFont("Sans-Bold", 10)
        c.drawString(x + 4 * mm, y + 48 * mm, title)
        ty = y + 40 * mm
        for ln in lines:
            c.setFillColor(color)
            c.circle(x + 7 * mm, ty + 1.1 * mm, 1.1 * mm, fill=1, stroke=0)
            c.setFillColor(TEXT)
            c.setFont("Sans", 8.5)
            c.drawString(x + 12 * mm, ty, ln)
            ty -= 6.6 * mm


def s12_bank(c):
    chrome(c, 12, "10.  Запросы в банк Forte", "Спецификация e-commerce, сценарии 3.1 и CoF / Recurrent")
    table(
        c,
        10 * mm,
        H - 28 * mm,
        [38 * mm, 95 * mm, 82 * mm, 100 * mm],
        ["Шаг", "HTTP", "Тело (суть)", "Когда"],
        [
            ["1. Ссылка", "POST /order", "typeRid: Order_COF | Order_RID, consumer.rid, hppRedirectUrl", "Клиент пополняет кошелёк"],
            ["CoF-поля", "в том же POST", "hppCofCapturePurposes: Recurring, initiationEnvKind: Browser", "save_card=true"],
            ["2. Списание", "POST /order", "Order_COF, consumer.rid, initiationEnvKind: Server", "Недостача / ретрай долга"],
            ["2b. execTran", "POST /order/{id}/exec-tran", "phase: Single, conditions.cofUsage: Recurring", "Сразу после создания заказа"],
            ["3. Статус", "GET /order/{id}", "password, tranDetailLevel=2", "Cron и ручная проверка"],
            ["Auth", "заголовок всех запросов", "Authorization: Basic TerminalSys/login:password", "Всегда"],
        ],
        font=7.5,
    )
    table(
        c,
        10 * mm,
        68 * mm,
        [70 * mm, 50 * mm, 195 * mm],
        ["Статус Forte", "Наш статус", "Действие"],
        [
            ["Preparing", "pending", "Ждать, клиент ещё на странице банка"],
            ["FullyPaid", "success", "Callback мойке, зачислить / закрыть долг"],
            ["Declined", "declined", "Callback, долг и ретрай на мойке"],
            ["Expired / Cancelled", "expired / cancelled", "Callback, ссылка протухла или отменена"],
        ],
        font=8,
    )


def s13_plan(c):
    chrome(c, 13, "11.  Этапы внедрения", "Старый FortePaymentController не трогаем")
    table(
        c,
        10 * mm,
        H - 28 * mm,
        [22 * mm, 32 * mm, 261 * mm],
        ["Этап", "Где", "Работы"],
        [
            ["1", "Шлюз", "Миграция client_payment_info, модель, hasOne с payments"],
            ["2", "Шлюз", "ForteWashPaymentController: link, charge, check, autoCheckWashOrders"],
            ["3", "Шлюз", "Маршруты /v1/wash/forte/*, .env (FORTE_*), cron каждую минуту"],
            ["4", "Мойка", "Таблицы кошелька, долгов, client_cards (токен, маска, rid)"],
            ["5", "Мойка", "Вызовы link/charge, приём callback, зачисление баланса"],
            ["6", "Мойка", "Логика: кошелёк → charge недостачи → долг → ретрай с лимитом"],
            ["7", "Совместно", "Тест: ссылка, CoF, успешный charge, PmoDecline, повторный callback"],
            ["8", "Юр. / продукт", "Оферта автосписания, галочка согласия в приложении"],
        ],
        font=8.5,
    )
    rounded(c, 10 * mm, 18 * mm, W - 20 * mm, 22 * mm, r=6, fill=TEAL_SOFT, stroke=TEAL, sw=0.7)
    c.setFillColor(TEAL)
    c.setFont("Sans-Bold", 10)
    c.drawString(16 * mm, 30 * mm, "Параллельность")
    c.setFillColor(TEXT)
    c.setFont("Sans", 9)
    c.drawString(16 * mm, 22 * mm, "Этапы 1–3 (шлюз) и 4 (мойка) можно вести параллельно. Стыковка — этап 5. Коннекторы Bitrix/amo и AIOKK продолжают ходить в старый контроллер.")


def s14_rules(c):
    chrome(c, 14, "12.  Правила, риски, итог", "Что зафиксировать до разработки")
    table(
        c,
        10 * mm,
        H - 28 * mm,
        [18 * mm, 297 * mm],
        ["№", "Правило"],
        [
            ["1", "client_rid — стабильный id клиента мойки. Смена id = банк не найдёт сохранённую карту."],
            ["2", "PAN и CVV не храним ни на шлюзе, ни на мойке. Только токен Forte и маска."],
            ["3", "Баланс карты заранее не проверяем. Хватает / нет = ответ execTran."],
            ["4", "Ретраи долга — на мойке, с паузой и лимитом попыток (антифрод банка)."],
            ["5", "Одна операция мойки (wash_payment_id) — одна попытка на шлюзе. Повтор = новый платёж."],
            ["6", "Карта появляется только после успешного /link с save_card=true. Пока карты нет — только ссылка."],
            ["7", "Предавторизация (Auth) не решает «подождать пополнения карты»: холд встанет, только если деньги уже есть."],
        ],
        font=8,
    )

    # mini architecture
    c.setFillColor(NAVY)
    c.setFont("Sans-Bold", 10)
    c.drawString(10 * mm, 52 * mm, "Итоговая схема")
    boxes = [
        (10 * mm, "Приложение мойки", NAVY),
        (78 * mm, "Сервер моек", TEAL),
        (146 * mm, "Шлюз Forte", ACCENT),
        (214 * mm, "Forte Bank", GOLD),
    ]
    for x, t, col in boxes:
        rounded(c, x, 22 * mm, 62 * mm, 24 * mm, r=6, fill=col, stroke=None)
        c.setFillColor(WHITE if col != GOLD else NAVY)
        c.setFont("Sans-Bold", 9)
        c.drawCentredString(x + 31 * mm, 32 * mm, t)
    for x in (72 * mm, 140 * mm, 208 * mm):
        arrow_h(c, x, x + 6 * mm, 34 * mm, MUTED)
    c.setFillColor(MUTED)
    c.setFont("Sans", 7.5)
    c.drawCentredString(107 * mm, 18 * mm, "кошелёк / долг / карты")
    c.drawCentredString(175 * mm, 18 * mm, "/link  /charge  callback")
    c.drawCentredString(243 * mm, 18 * mm, "order  ·  execTran  ·  getOrder")


def main():
    c = canvas.Canvas(OUT, pagesize=(W, H))
    c.setTitle("Оплата мойки через Forte — кошелёк, карта, шлюз")
    c.setAuthor("Masnaget / Forte gateway")
    c.setSubject("Архитектура оплаты мойки: payments + client_payment_info")
    slides = [
        s01_title, s02_task, s03_roles, s04_forte, s05_flow, s06_ops,
        s07_data, s08_db, s09_contract, s10_callback, s11_scenarios,
        s12_bank, s13_plan, s14_rules,
    ]
    for fn in slides:
        fn(c)
        c.showPage()
    c.save()
    print(OUT)


if __name__ == "__main__":
    main()
