# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Repository scope

A single PHP script — [upd.php](upd.php) — that reconciles incoming bank-transfer email notifications with a Bitrix24 CRM and logs them to a Google Sheet. There is no build system, no tests, no package manifest. Code, comments, and most string literals are in Russian.

## Running

Designed for **PHP 5.6** in CLI mode (typically driven by cron):

```
/usr/bin/php5.6 /home/<user>/web/<host>/public_html/payments/upd.php <host>
```

The first argv sets `$_SERVER['HTTP_HOST']`, and `DOCUMENT_ROOT` is derived from `$argv[0]`. Run with `?nochat=1` (web context) or set `$nochat` to suppress Bitrix chat sends while debugging — Google Sheet writes still happen, so use a sandbox sheet.

`die('no new payments')` on an empty IMAP UNSEEN search is the normal idle path, not an error.

## External dependencies (not in this repo)

- `$_SERVER['DOCUMENT_ROOT'].'/s/gtable/google.class.api.php'` — provides `googletable` class with `addstr($row)`. Required; the script will not run without it.
- IMAP mailbox at `mail.hosting.reg.ru:993` (mailbox `payments@korzilla.ru`).
- Bitrix24 REST at `https://korzilla.bitrix24.ru/rest/...`. Two webhook tokens are used: one in `$auth` (composed into `$url` for `BXquery`) and a **separate hardcoded one inside `restCommand()`** at line 406. They are not interchangeable — `restCommand` is used only for `im.message.add` chat sends.
- Credentials in source are placeholders (`111111`, `2222222`); real secrets live on the deployment host.

## Pipeline (high-level)

The main `foreach ($mails_id as $num)` loop is the whole program:

1. **Filter by subject** — only `Поступление на счёт в валюте счёта` (Альфа-Банк / Best2Pay) and `Движение средств по счету` (АКИБАНК) are processed; everything else is marked seen and skipped.
2. **Decode body** — the body is base64 inside MIME. Three fallbacks in order: `$re` (with `--boundary--`), `$re_simple` (no separators), then a last-resort raw-base64 sniff. АКИБАНК bodies are converted from `windows-1251` to UTF-8; Альфа bodies are assumed UTF-8.
3. **`regexMail($bodyDecode, $tema)`** — branches on subject. Returns a 1-indexed-style array where the call site reads `[2]=плательщик`, `[3]=сумма`, `[5]=ИНН`, `[7]=назначение платежа`. The Альфа branch has a primary regex plus a Best2Pay fallback (card payments where `ИНН:` is literally `Оплата картой через Best2pay` and there may be no payer name).
4. **Resolve invoice → deal → owner**:
   - `searchDeal($accountNumber)` queries the **smart-invoice** entity (`crm.item.list?entityTypeId=31`) with `filter[%title]=<accountNumber>` (substring match on title). The customer-facing invoice number (`26/670`-style — 2-digit year; the older 4-digit `2026/670` form is out of circulation) lives in `title`, NOT in the `accountNumber` field — `accountNumber` is a separate internal counter. The function then fetches product rows via `crm.item.productrow.list` (filter `=ownerType=SI, =ownerId=<id>`) since `crm.item.get` does not return them, derives `inv_type_id` from product names, and returns the deal/responsible.
   - The invoice number is extracted from the payment description by regex `$re3` (matches `26/670`-style, 2-digit year, slash-only; tolerates the legacy 4-digit `2026/670` form, and a `(?!\/\d)` guard rejects slash-dates like `26/05/2026`); `$re3_contract` is a Best2Pay-only fallback for `№К0199`-style contract refs (matched against `договору №...`, optional letter prefix and `-КМ/-МКМ/-ПКМ/-КР` suffix).
   - **When `$re3_contract` matched** (`$isContract`), resolution goes through `searchDealByContract($contract)` instead: it filters `crm.deal.list` by the contract field `%UF_CRM_1495110302` (values like `К0009-КМ от 22.04.2026`), takes the newest deal, then finds that deal's newest linked smart-invoice (`crm.item.list?entityTypeId=31&filter[=parentId2]=<dealId>`) for `setStatusAccount`/sheet details. Deliberately does **not** fall back to the `%title` substring search — that would mis-match a random invoice on a date fragment.
   - If no invoice is found, `searchINN($inn)` falls back to matching company by INN (`crm.requisite.list`) and picking the most recent deal whose `OPPORTUNITY` equals the payment amount.
5. **Service-type classification (`inv_type_id`)** — keyed into `$listDeals` (line 57) to produce `vidUslugi` and `typeUslugi` columns. Two classification stages exist:
   - In `searchDeal()`, based on invoice product names. The "контекстная реклама" branch checks the smart-invoice `title` (rather than the legacy `ORDER_TOPIC`) for "яндекс/директ/google/гугл" — keep this in mind when adjusting category rules.
   - At top level, based on the payment description text when `searchDeal` did not classify, with hosting/domain pricing tiers `$arrHostDomen` / `$arrHost` / `$arrDomen` disambiguating IDs 5/6/7.
6. **Organization (`$org`) is detected by name strings** in the email body: "Лебедев Виктор Шакирович" → ИП Лебедев, "Верховых Евгений Андреевич" → ИП Верховых, "КОРЗИЛЛА" → ООО Корзилла (default). This is fragile; changes to bank email formatting will break it.
7. **Side effects (in this order)**:
   - Append a row to the Google Sheet via `googletable->addstr($gglSheetArr)` — column order is fixed at lines 275-287 and is what the receiving sheet's columns expect.
   - If the invoice price equals the payment amount (compared via `price()`, normalizing `,`/`.`), call `setStatusAccount()` which calls `crm.item.update?entityTypeId=31` with `fields[stageId]=DT31_2:P` (the "Оплачен" stage; smart-invoice has a single category `id=2`, so the stage ID is hard-coded — if a second category is ever added in Bitrix24, this will need parametrizing).
   - Send the formatted message via `sendChat()` to the admin chat (`chat9344`) and either the responsible user's DM or the commerce chat (`chat1578`) when there is no responsible.
8. **IMAP flag bookkeeping** — only mark `\\seen \\flagged` after **both** chat sends succeed; on partial failure the message is explicitly cleared back to unseen so the next run retries, and a diagnostic is sent to user `6`.

## Things that look like bugs but are intentional / load-bearing

- "Системный платеж" (containing `для пополнения оборотных средств`) is silently marked seen and skipped (line 194).
- User `252` triggers a duplicate chat send to dialog `4174` inside `sendChat()` (line 387). This is a hand-coded routing rule, not dead code.
- The numerous `sleep(1..3)` + `flush()/ob_flush()` calls throughout are deliberate: they pace the Bitrix REST calls (rate-limit avoidance) and keep CLI/SSE-style output streaming. Do not "clean these up" without reason.
- `BXquery` returns `$array['result']` only when truthy; an empty/zero result becomes `null`. Callers rely on that (e.g. `if ($account['ID']>0)`).
- `imap_open(...) or die(...)` aborts the entire run on IMAP failure — there is no retry.
- The fall-through default `if (!$org) $org = "ООО Корзилла"` means an unrecognized payer is *always* attributed to ООО Корзилла in the sheet.

## Editing guidance

- Preserve the Russian strings exactly — they are matched against email content and Bitrix data, and are also what humans read in Bitrix chat / the Google Sheet.
- When adding a new service type, append to `$listDeals` and decide whether classification belongs in `searchDeal()` (invoice products) or in the top-level description-based block.
- When touching `regexMail()`, keep the return shape stable: callers index `[2]`, `[3]`, `[5]`, `[7]`. The Альфа primary regex and Best2Pay fallback share that contract.
- The Google Sheet column order at lines 275-287 is a wire format with the spreadsheet — reordering or inserting a column there silently misaligns every future row.
