xync-db 0.1.4__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xync_db-0.1.4 → xync_db-0.2.0}/PKG-INFO +1 -1
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/models/0002_triggers.py +3 -5
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/models/__init__.py +231 -28
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/typs/db/ad.py +1 -1
- {xync_db-0.1.4 → xync_db-0.2.0}/.gitignore +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/README.md +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/pyproject.toml +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/__init__.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/config.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/enums.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/exceptions.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/graph.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/i18n.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/logo.png +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/CLAUDE.md +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/__init__.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/models/0001_initial.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/models/0003_files_seed.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/models/0004_all_seed.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/models/__init__.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/models/seed/__init__.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/models/seed/a_actor_seed.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/models/seed/a_tg_seed.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/models/seed/b_htx_seed.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/migrations/models/seed/xync_files.sql +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/models/CLAUDE.md +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/py.typed +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/shared.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/typs/__init__.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/typs/db/__init__.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/typs/db/actor.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/typs/db/common.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/typs/db/cred.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/typs/db/order.py +0 -0
- {xync_db-0.1.4 → xync_db-0.2.0}/src/xync_db/typs/db/pm.py +0 -0
|
@@ -237,11 +237,9 @@ CREATE OR REPLACE FUNCTION cred_del() returns trigger as $cred_del_trg$
|
|
|
237
237
|
DECLARE
|
|
238
238
|
cxs jsonb;
|
|
239
239
|
BEGIN
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
PERFORM pg_notify('creds_del', jsonb_build_array(OLD.person_id, cxs)::varchar);
|
|
244
|
-
END IF;
|
|
240
|
+
SELECT jsonb_agg(jsonb_build_array(ex_id, exid)) INTO cxs FROM credex WHERE cred_id = OLD.id;
|
|
241
|
+
IF cxs IS NOT NULL THEN
|
|
242
|
+
PERFORM pg_notify('creds_del', jsonb_build_array(OLD.person_id, cxs)::varchar);
|
|
245
243
|
END IF;
|
|
246
244
|
RETURN OLD;
|
|
247
245
|
END
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
import re
|
|
5
5
|
import struct
|
|
6
6
|
import sys
|
|
7
|
+
from collections.abc import Collection
|
|
7
8
|
from datetime import datetime
|
|
8
9
|
from decimal import Decimal
|
|
9
10
|
from time import time
|
|
@@ -40,6 +41,7 @@ from tortoise.functions import Sum
|
|
|
40
41
|
from tortoise.migrations.constraints import CheckConstraint
|
|
41
42
|
from tortoise.signals import pre_save
|
|
42
43
|
from tortoise.timezone import now
|
|
44
|
+
from tortoise.transactions import in_transaction
|
|
43
45
|
|
|
44
46
|
# noinspection PyUnusedImports
|
|
45
47
|
from x_auth.models import ( # noqa: F401
|
|
@@ -1163,6 +1165,64 @@ class Ad(Model, TsTrait):
|
|
|
1163
1165
|
"price",
|
|
1164
1166
|
}
|
|
1165
1167
|
|
|
1168
|
+
@staticmethod
|
|
1169
|
+
async def _sync_pms(ad: Ad, pms: Collection[Pm] | None) -> None:
|
|
1170
|
+
"""Синк m2m pms (clear+add), только если набор передан и отличается от текущего.
|
|
1171
|
+
Дедуп и diff — по `pm.id`, НЕ через set[Pm]: XyncPay-Pm имеет id=0, а Tortoise.Model.__hash__
|
|
1172
|
+
трактует pk=0 как unsaved (`not self.pk`) → набор с ним невозможно захэшировать."""
|
|
1173
|
+
if pms is None:
|
|
1174
|
+
return
|
|
1175
|
+
new = {p.id: p for p in pms} # dedup by id (Pm.id==0 → unhashable, нельзя set[Pm])
|
|
1176
|
+
if {p.id for p in await ad.pms} != new.keys():
|
|
1177
|
+
await ad.pms.clear()
|
|
1178
|
+
if new:
|
|
1179
|
+
await ad.pms.add(*new.values())
|
|
1180
|
+
|
|
1181
|
+
@classmethod
|
|
1182
|
+
async def upsert(
|
|
1183
|
+
cls,
|
|
1184
|
+
*,
|
|
1185
|
+
id: int | None = None,
|
|
1186
|
+
since: datetime | None = None,
|
|
1187
|
+
pms: Collection[Pm] | None = None,
|
|
1188
|
+
**fields,
|
|
1189
|
+
) -> tuple[Ad, bool]:
|
|
1190
|
+
"""ЕДИНСТВЕННЫЙ writer строки Ad (single-writer-правило, как Cred.upsert). Три режима:
|
|
1191
|
+
- правка по `id` (write-back exid 0→real, смена статуса) → fields пишутся через filter().update()
|
|
1192
|
+
(без бампа updated_at — иначе сломали бы stale-guard синка ниже);
|
|
1193
|
+
- синк с биржи (передан правдивый `exid`) → upsert по (exid, maker_id) со stale-guard `since`
|
|
1194
|
+
(DB-first: если строку правили локально ПОЗЖЕ чтения биржи — биржевой снимок устарел, не
|
|
1195
|
+
затираем, иначе летит «обратный» mads_upd);
|
|
1196
|
+
- создание локальной заготовки (exid=0 — он НЕ уникален per maker, поэтому именно create,
|
|
1197
|
+
а не upsert).
|
|
1198
|
+
`pms` (Collection[Pm] | None) — m2m: синкаем только если передан (дедуп по id внутри _sync_pms —
|
|
1199
|
+
НЕ set[Pm], XyncPay-Pm имеет id=0 и невхэшируем). Запись строки + clear+add pms всегда в
|
|
1200
|
+
ОДНОЙ транзакции (и в синке, и в create), иначе AFTER INSERT/UPDATE-триггер (ads_new/ads_upd) шлёт
|
|
1201
|
+
NOTIFY на коммите строки — до записи pms — и хэндлер прочитает объяву с пустым/промежуточным набором.
|
|
1202
|
+
Внешние id (maker/pair_side/pms/cond) резолвит вызывающий (держит ex_client-контекст: e2x_actor/
|
|
1203
|
+
e2x_pm); модель пишет скаляры."""
|
|
1204
|
+
if id is not None:
|
|
1205
|
+
await cls.filter(id=id).update(**fields)
|
|
1206
|
+
return await cls.get(id=id), False
|
|
1207
|
+
if fields.get("exid"):
|
|
1208
|
+
unq = {"exid": fields.pop("exid"), "maker_id": fields.pop("maker_id")}
|
|
1209
|
+
async with in_transaction():
|
|
1210
|
+
if since is not None:
|
|
1211
|
+
existing = await cls.select_for_update().get_or_none(**unq)
|
|
1212
|
+
if existing and existing.updated_at and existing.updated_at >= since:
|
|
1213
|
+
logging.info(
|
|
1214
|
+
f"Ad.upsert: skip stale exchange overwrite ad#{existing.id} "
|
|
1215
|
+
f"(db updated_at {existing.updated_at} >= exchange read {since})"
|
|
1216
|
+
)
|
|
1217
|
+
return existing, False
|
|
1218
|
+
ad, created = await cls.update_or_create(defaults=fields, **unq)
|
|
1219
|
+
await cls._sync_pms(ad, pms)
|
|
1220
|
+
return ad, created
|
|
1221
|
+
async with in_transaction(): # create + pms одной транзакцией: ads_new (AFTER INSERT) доставляется
|
|
1222
|
+
ad = await cls.create(**fields) # на COMMIT — к этому моменту pms уже целиком видны хэндлеру
|
|
1223
|
+
await cls._sync_pms(ad, pms) # новая локальная заготовка exid=0
|
|
1224
|
+
return ad, True
|
|
1225
|
+
|
|
1166
1226
|
class Meta:
|
|
1167
1227
|
table_description = "P2P Advertisements"
|
|
1168
1228
|
unique_together = (("exid", "maker"),)
|
|
@@ -1222,6 +1282,34 @@ class MyAd(Model): # Road
|
|
|
1222
1282
|
"ad__pair_side__pair__coin", "ad__pair_side__pair__cur", "ad__maker__ex"
|
|
1223
1283
|
)
|
|
1224
1284
|
|
|
1285
|
+
@classmethod
|
|
1286
|
+
async def upsert(
|
|
1287
|
+
cls,
|
|
1288
|
+
*,
|
|
1289
|
+
id: int | None = None,
|
|
1290
|
+
ad_id: int | None = None,
|
|
1291
|
+
credexs: list[CredEx] | None = None,
|
|
1292
|
+
**fields,
|
|
1293
|
+
) -> tuple[MyAd, bool]:
|
|
1294
|
+
"""ЕДИНСТВЕННЫЙ writer строки MyAd (single-writer-правило, как CredEx.upsert для CredEx). Режимы:
|
|
1295
|
+
- правка по `id` (hex/shared_at от ad_share, blocked, target_place) → fields через filter().update();
|
|
1296
|
+
- upsert по `ad_id` (1:1 с Ad) → fields в defaults.
|
|
1297
|
+
`credexs` (list[CredEx] | None) — m2m: синкаем (clear+add), только если передан НЕПУСТОЙ список
|
|
1298
|
+
(пустой бы вайпнул связь — этот случай вызывающий обрабатывает сам, не передавая credexs). Запись
|
|
1299
|
+
строки my_ad + clear+add credexs идут в ОДНОЙ транзакции: иначе AFTER INSERT-триггер my_ad шлёт
|
|
1300
|
+
my_ads_new на коммите строки — до записи credexs — и my_ad_new (фронт-создание, exid=0) запостит
|
|
1301
|
+
объяву на биржу с пустым набором PM."""
|
|
1302
|
+
async with in_transaction(): # строка + credexs одной транзакцией: my_ads_new (AFTER INSERT)
|
|
1303
|
+
if id is not None: # доставляется на COMMIT → my_ad_new прочитает уже полные credexs, а не
|
|
1304
|
+
await cls.filter(id=id).update(**fields) # пустые (иначе объява улетит на биржу без PM)
|
|
1305
|
+
mad, created = await cls.get(id=id), False
|
|
1306
|
+
else:
|
|
1307
|
+
mad, created = await cls.update_or_create(defaults=fields, ad_id=ad_id)
|
|
1308
|
+
if credexs:
|
|
1309
|
+
await mad.credexs.clear()
|
|
1310
|
+
await mad.credexs.add(*credexs)
|
|
1311
|
+
return mad, created
|
|
1312
|
+
|
|
1225
1313
|
@classmethod
|
|
1226
1314
|
#
|
|
1227
1315
|
def new(cls, coin_id: int, cur_id: int, min_fiat: int, is_hot: bool = False): ...
|
|
@@ -1407,30 +1495,66 @@ class Cred(Model):
|
|
|
1407
1495
|
name = f", имя: {self.name}" if self.name else ""
|
|
1408
1496
|
return f"`{self.detail}`{name}{xtr}"
|
|
1409
1497
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1498
|
+
_UNSET: ClassVar = object() # sentinel: «поле не передано» (отличаем от явного None, который НУЛЯЕТ)
|
|
1499
|
+
|
|
1500
|
+
@staticmethod
|
|
1501
|
+
async def _classify_ovr_pm(detail: str, extra: str | None) -> int | None:
|
|
1502
|
+
"""Вывести «оверрайд-PM» по detail/extra — ЧИСТЫЙ расчёт, без записи. XyncPay → 0 (hot,
|
|
1503
|
+
размножается по биржам); Volet/AdvCash (или его R/E/U/T/L/D-карточный паттерн) → Pm `volet`;
|
|
1504
|
+
иначе None. Зовётся ТОЛЬКО из `upsert`, чтобы ovr_pm писался в той же записи, что и сам Cred
|
|
1505
|
+
(а не отдельным self-write/реактивным хендлером — см. single-writer-правило)."""
|
|
1506
|
+
extra = extra or ""
|
|
1507
|
+
if "XyncPay" in detail or "XyncPay" in extra:
|
|
1508
|
+
return 0
|
|
1509
|
+
low_d, low_e = detail.lower(), extra.lower()
|
|
1510
|
+
if (
|
|
1511
|
+
"volet" in low_d
|
|
1512
|
+
or "advcash" in low_d
|
|
1513
|
+
or "volet" in low_e
|
|
1514
|
+
or "advcash" in low_e
|
|
1515
|
+
or re.match(r"[REUTLD]\d{4} ?\d{4} ?\d{4}\b", detail)
|
|
1424
1516
|
or re.match(r"[REUTLD]\d{4} ?\d{4} ?\d{4}\b", extra)
|
|
1425
1517
|
):
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1518
|
+
return (await Pm.get(norm="volet")).id
|
|
1519
|
+
return None
|
|
1520
|
+
|
|
1521
|
+
@classmethod
|
|
1522
|
+
async def upsert(
|
|
1523
|
+
cls,
|
|
1524
|
+
*,
|
|
1525
|
+
id: int | None = None,
|
|
1526
|
+
pmcur_id: int | None = None,
|
|
1527
|
+
pm_id: int | None = None,
|
|
1528
|
+
cur_id: int | None = None,
|
|
1529
|
+
person_id: int | None = None,
|
|
1530
|
+
detail: str,
|
|
1531
|
+
name: str | None = None,
|
|
1532
|
+
extra: str | None = _UNSET,
|
|
1533
|
+
qr: bytes | None = _UNSET,
|
|
1534
|
+
) -> tuple[Cred, bool]:
|
|
1535
|
+
"""ЕДИНСТВЕННЫЙ writer строки Cred (single-writer-правило). Классифицирует ovr_pm и пишет его
|
|
1536
|
+
В ТОЙ ЖЕ записи. Два режима:
|
|
1537
|
+
- правка существующего: передан `id` → обновляем detail/name(/extra/qr) + пере-классификация;
|
|
1538
|
+
- создание/upsert: person_id + (pmcur_id ИЛИ pm_id+cur_id) → ключ (person, pmcur, detail).
|
|
1539
|
+
Внешние id (pmcur/pm/cur/person) резолвит вызывающий (он держит ex_client-контекст: e2x_actor/
|
|
1540
|
+
e2x_pm); модель принимает готовые скаляры и только пишет. extra/qr с дефолтом-сентинелом: не
|
|
1541
|
+
переданы — не трогаем (иначе случайно занулили бы существующий qr при правке detail с фронта)."""
|
|
1542
|
+
df: dict = {
|
|
1543
|
+
"name": name,
|
|
1544
|
+
"ovr_pm_id": await cls._classify_ovr_pm(detail, None if extra is cls._UNSET else extra),
|
|
1545
|
+
}
|
|
1546
|
+
if extra is not cls._UNSET:
|
|
1547
|
+
df["extra"] = extra
|
|
1548
|
+
if qr is not cls._UNSET:
|
|
1549
|
+
df["qr"] = qr
|
|
1550
|
+
if id is not None:
|
|
1551
|
+
df["detail"] = detail
|
|
1552
|
+
cred = await cls.get(id=id)
|
|
1553
|
+
cred.update_from_dict(df)
|
|
1554
|
+
await cred.save()
|
|
1555
|
+
return cred, False
|
|
1556
|
+
pmcur_id = pmcur_id or (await PmCur.get(pm_id=pm_id, cur_id=cur_id)).id
|
|
1557
|
+
return await cls.update_or_create(df, pmcur_id=pmcur_id, person_id=person_id, detail=detail)
|
|
1434
1558
|
|
|
1435
1559
|
class Meta:
|
|
1436
1560
|
table_description = "Currency accounts"
|
|
@@ -1446,6 +1570,14 @@ class CredEx(Model):
|
|
|
1446
1570
|
|
|
1447
1571
|
_name: ClassVar = {"exid"}
|
|
1448
1572
|
|
|
1573
|
+
@classmethod
|
|
1574
|
+
async def upsert(cls, *, cred_id: int, ex_id: int, exid: int) -> CredEx:
|
|
1575
|
+
"""ЕДИНСТВЕННЫЙ writer CredEx (single-writer-правило): ключ (cred, ex), ставит exid. Покрывает
|
|
1576
|
+
и стаб exid=0 (реактивный фанаут на биржи), и реальный exid (parse в `credex_save` / ответ биржи
|
|
1577
|
+
в хендлере `credex_new`). Больше НИКТО не пишет CredEx напрямую."""
|
|
1578
|
+
credex, _ = await cls.update_or_create({"exid": exid}, cred_id=cred_id, ex_id=ex_id)
|
|
1579
|
+
return credex
|
|
1580
|
+
|
|
1449
1581
|
class Meta:
|
|
1450
1582
|
table_description = "Credential on Exchange"
|
|
1451
1583
|
unique_together = (("ex", "exid"),)
|
|
@@ -1472,6 +1604,18 @@ class Fiat(Model):
|
|
|
1472
1604
|
__import__(module_name)
|
|
1473
1605
|
return sys.modules[module_name].FiatEpyd
|
|
1474
1606
|
|
|
1607
|
+
@classmethod
|
|
1608
|
+
async def upsert(cls, cred_id: int, amount: int, target: int | None = None, min_deposit: int | None = None) -> Fiat:
|
|
1609
|
+
"""ЕДИНСТВЕННЫЙ writer Fiat-баланса (single-writer-правило, как Asset.upsert). Апсертит по
|
|
1610
|
+
cred_id (OneToOne), ставя amount (+ target/min_deposit, если переданы). Резолв cred остаётся
|
|
1611
|
+
у вызывающего; модель только пишет."""
|
|
1612
|
+
defaults = {"amount": amount}
|
|
1613
|
+
if target is not None:
|
|
1614
|
+
defaults["target"] = target
|
|
1615
|
+
if min_deposit is not None:
|
|
1616
|
+
defaults["min_deposit"] = min_deposit
|
|
1617
|
+
return (await cls.update_or_create(defaults, cred_id=cred_id))[0]
|
|
1618
|
+
|
|
1475
1619
|
|
|
1476
1620
|
class Limit(Model):
|
|
1477
1621
|
pmcur: ForeignKeyRelation[PmCur] = ForeignKeyField("models.PmCur", on_update=CASCADE)
|
|
@@ -1528,6 +1672,18 @@ class Asset(Model):
|
|
|
1528
1672
|
__import__(module_name)
|
|
1529
1673
|
return sys.modules[module_name].AssetEpyd
|
|
1530
1674
|
|
|
1675
|
+
@classmethod
|
|
1676
|
+
async def upsert(
|
|
1677
|
+
cls, actor_id: int, coin_id: int, agent_id: int, free: int, freeze: int | None = None, typ: AddrExType = None
|
|
1678
|
+
) -> Asset:
|
|
1679
|
+
"""ЕДИНСТВЕННЫЙ writer Asset-баланса (single-writer-правило). Резолвит Addr по (actor, coin),
|
|
1680
|
+
создаёт при отсутствии, апсертит Asset по натуральному ключу (addr, agent, typ), ставя free
|
|
1681
|
+
(и freeze, если передан). Резолв actor/agent остаётся у вызывающего (client держит контекст)."""
|
|
1682
|
+
addr, _ = await Addr.get_or_create(actor_id=actor_id, coin_id=coin_id)
|
|
1683
|
+
defaults = {"free": free} if freeze is None else {"free": free, "freeze": freeze}
|
|
1684
|
+
asset, _ = await cls.update_or_create(defaults, addr=addr, agent_id=agent_id, typ=typ or AddrExType.found)
|
|
1685
|
+
return asset
|
|
1686
|
+
|
|
1531
1687
|
|
|
1532
1688
|
class PayReq(Model, TsTrait):
|
|
1533
1689
|
pay_until: datetime = DatetimeSecField()
|
|
@@ -1633,6 +1789,32 @@ class Order(Model, TsTrait):
|
|
|
1633
1789
|
)
|
|
1634
1790
|
return not is_cold
|
|
1635
1791
|
|
|
1792
|
+
_terminal_statuses: ClassVar = frozenset({OrderStatus.completed, OrderStatus.canceled})
|
|
1793
|
+
|
|
1794
|
+
async def set_status(self, status: OrderStatus, *, force: bool = False) -> bool:
|
|
1795
|
+
"""ЕДИНСТВЕННЫЙ writer Order.status (single-writer-правило, как Cred/Ad.upsert).
|
|
1796
|
+
Идемпотентен (тот же статус → no-op, False) и защищает терминальные статусы:
|
|
1797
|
+
из completed/canceled никуда без force — это снимает дублированные guard'ы из всех
|
|
1798
|
+
listener'ов (Htx/Mexc «не откатывай completed→paid», Bybit «игнор эха canceled→paid»,
|
|
1799
|
+
«status != current»). Возвращает True, если статус изменён и записан в БД."""
|
|
1800
|
+
if self.status == status:
|
|
1801
|
+
return False
|
|
1802
|
+
if not force and self.status in self._terminal_statuses:
|
|
1803
|
+
logging.info(f"Order#{self.exid}: skip {self.status.name}→{status.name} (terminal, no force)")
|
|
1804
|
+
return False
|
|
1805
|
+
self.status = status
|
|
1806
|
+
await self.save(update_fields=["status", "updated_at"]) # updated_at auto_now
|
|
1807
|
+
return True
|
|
1808
|
+
|
|
1809
|
+
async def set_hot(self, user: User) -> Hot:
|
|
1810
|
+
"""ЕДИНСТВЕННЫЙ writer Order.hot_id (single-writer-правило, как set_status).
|
|
1811
|
+
Привязывает ордер к последнему открытому прогреву юзера (get_last_hot) — назначение
|
|
1812
|
+
hot-сессии шло одинаково из 3 мест (api-хендлер на created, контроллер is-you, бот is_you):
|
|
1813
|
+
order.hot = await user.get_last_hot(); save(["hot_id"]). Возвращает назначенный Hot."""
|
|
1814
|
+
self.hot = await user.get_last_hot()
|
|
1815
|
+
await self.save(update_fields=["hot_id"])
|
|
1816
|
+
return self.hot
|
|
1817
|
+
|
|
1636
1818
|
# когда прилетел новый ордер, и мы уже знаем юзера которому он принадлежит, и его последний hot # noqa: RUF003
|
|
1637
1819
|
async def hot_process(self) -> OrderResult:
|
|
1638
1820
|
await self.fetch_related(
|
|
@@ -1672,8 +1854,7 @@ class Order(Model, TsTrait):
|
|
|
1672
1854
|
) or OrderErr.maker_balance_deficit
|
|
1673
1855
|
res.data = (self.amount - payment) * 0.01
|
|
1674
1856
|
elif self.ad.pair_side.is_sell: # успех и мы продавец - отпускаем (ордер уже paid: покупатель оплатил)
|
|
1675
|
-
self.
|
|
1676
|
-
await self.save(update_fields=["status"])
|
|
1857
|
+
await self.set_status(OrderStatus.completed)
|
|
1677
1858
|
res.data = payment.amount * 0.01
|
|
1678
1859
|
else: # успех и мы покупатель - ждем когда продавец отпустит
|
|
1679
1860
|
res.data = OrderResult.Order.load(self)
|
|
@@ -1714,16 +1895,15 @@ class Order(Model, TsTrait):
|
|
|
1714
1895
|
tsf = await Transfer.create(
|
|
1715
1896
|
amount=tx.amount, order_id=self.id, pmid=tx.id.hex, sender_acc=buyer.username_id
|
|
1716
1897
|
)
|
|
1717
|
-
|
|
1718
|
-
await self.
|
|
1898
|
+
# todo: запрещать ставить paid, если у ордера нет трансферов на сумму amount # noqa: RUF003 - intentional Russian
|
|
1899
|
+
await self.set_status(OrderStatus.paid)
|
|
1719
1900
|
return tsf
|
|
1720
1901
|
|
|
1721
1902
|
raise NotImplementedError("Pay method implemented now only for XyncPay pm")
|
|
1722
1903
|
|
|
1723
1904
|
async def cancel_request(self):
|
|
1724
1905
|
if not (payments := await self.fetch_related("transfers")):
|
|
1725
|
-
self.
|
|
1726
|
-
await self.save(update_fields=["status", "updated_at"])
|
|
1906
|
+
await self.set_status(OrderStatus.canceled)
|
|
1727
1907
|
return {"err": 0}
|
|
1728
1908
|
return {"err": payments}
|
|
1729
1909
|
|
|
@@ -1993,6 +2173,29 @@ class Msg(Model):
|
|
|
1993
2173
|
order: ForeignKeyRelation[Order] = ForeignKeyField("models.Order", "msgs", on_update=CASCADE)
|
|
1994
2174
|
sent_at: datetime | None = DatetimeSecField()
|
|
1995
2175
|
|
|
2176
|
+
@classmethod
|
|
2177
|
+
async def upsert(
|
|
2178
|
+
cls,
|
|
2179
|
+
order_id: int,
|
|
2180
|
+
*,
|
|
2181
|
+
to_maker: bool,
|
|
2182
|
+
txt: str | None = None,
|
|
2183
|
+
file_id: int | None = None,
|
|
2184
|
+
read: bool = False,
|
|
2185
|
+
sent_at: datetime | None = None,
|
|
2186
|
+
tg_mid: int | None = None,
|
|
2187
|
+
) -> tuple[Msg, bool]:
|
|
2188
|
+
"""ЕДИНСТВЕННЫЙ writer Msg (single-writer-правило). Натуральный ключ (order, txt, file) —
|
|
2189
|
+
апсертит чат-сообщение ордера из всех источников: исходящее через бота, входящее по WS,
|
|
2190
|
+
импорт истории. Раньше 3 расходящихся пути с разными стратегиями конфликта (bot Msg.create /
|
|
2191
|
+
Bybit bulk_create(ignore_conflicts) / in_agent update_or_create)."""
|
|
2192
|
+
return await cls.update_or_create(
|
|
2193
|
+
{"to_maker": to_maker, "read": read, "sent_at": sent_at or now(), "tg_mid": tg_mid},
|
|
2194
|
+
order_id=order_id,
|
|
2195
|
+
txt=txt,
|
|
2196
|
+
file_id=file_id,
|
|
2197
|
+
)
|
|
2198
|
+
|
|
1996
2199
|
# todo: required txt or file
|
|
1997
2200
|
class PydanticMeta(Model.PydanticMeta):
|
|
1998
2201
|
max_recursion: int = 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|