diff --git a/migrations/models/0_20260107150827_init.py b/migrations/models/0_20260107150827_init.py index 8acef54..9b8f5ec 100644 --- a/migrations/models/0_20260107150827_init.py +++ b/migrations/models/0_20260107150827_init.py @@ -1,3 +1,4 @@ +# ruff: noqa from tortoise import BaseDBAsyncClient RUN_IN_TRANSACTION = True diff --git a/migrations/models/1_20260107150903_add_check_constraint.py b/migrations/models/1_20260107150903_add_check_constraint.py index 0cb2d87..cc189a3 100644 --- a/migrations/models/1_20260107150903_add_check_constraint.py +++ b/migrations/models/1_20260107150903_add_check_constraint.py @@ -1,3 +1,4 @@ +# ruff: noqa from tortoise import BaseDBAsyncClient RUN_IN_TRANSACTION = True diff --git a/migrations/models/2_20260115142040_refactor_purchase_to_placement_publication.py b/migrations/models/2_20260115142040_refactor_purchase_to_placement_post.py similarity index 65% rename from migrations/models/2_20260115142040_refactor_purchase_to_placement_publication.py rename to migrations/models/2_20260115142040_refactor_purchase_to_placement_post.py index dc41d49..d536737 100644 --- a/migrations/models/2_20260115142040_refactor_purchase_to_placement_publication.py +++ b/migrations/models/2_20260115142040_refactor_purchase_to_placement_post.py @@ -1,3 +1,4 @@ +# ruff: noqa from tortoise import BaseDBAsyncClient RUN_IN_TRANSACTION = True @@ -55,8 +56,8 @@ async def upgrade(db: BaseDBAsyncClient) -> str: FROM "purchase_channel" pc JOIN "purchase" p ON pc.purchase_id = p.id; - -- Шаг 4: Создать таблицу publication (бывший placement) - CREATE TABLE "publication" ( + -- Шаг 4: Создать таблицу placement_post (бывший placement) + CREATE TABLE "placement_post" ( "id" UUID NOT NULL PRIMARY KEY, "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -71,8 +72,8 @@ async def upgrade(db: BaseDBAsyncClient) -> str: "project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE ); - -- Шаг 5: Мигрировать данные из old_placement в publication - INSERT INTO "publication" ( + -- Шаг 5: Мигрировать данные из old_placement в placement_post + INSERT INTO "placement_post" ( "id", "created_at", "updated_at", "deleted_at", "wanted_placement_date", "cost", "comment", "status", "creative_id", "placement_id", "post_id", "project_id" @@ -85,13 +86,13 @@ async def upgrade(db: BaseDBAsyncClient) -> str: -- Шаг 6: Обновить foreign keys для таблиц, ссылающихся на old_placement - -- 6.1: Обновить subscription (placement_id -> publication_id) + -- 6.1: Обновить subscription (placement_id -> placement_post_id) -- Удалить constraint ДО переименования колонки ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "subscription_placement_id_fkey"; -- Переименовать колонку - ALTER TABLE "subscription" RENAME COLUMN "placement_id" TO "publication_id"; + ALTER TABLE "subscription" RENAME COLUMN "placement_id" TO "placement_post_id"; -- 6.2: Обновить workspace_user_permission_scope.placement_id -- Удалить старый foreign key constraint ДО обновления данных @@ -109,22 +110,22 @@ async def upgrade(db: BaseDBAsyncClient) -> str: ON "placement" ("project_id", "status"); CREATE INDEX "idx_placement_channel_fb3968" ON "placement" ("channel_id"); - CREATE INDEX "idx_publication_creativ_1c6807" - ON "publication" ("creative_id"); - CREATE INDEX "idx_publication_placeme_bd5bc9" - ON "publication" ("placement_id"); - CREATE INDEX "idx_publication_post_id_998755" - ON "publication" ("post_id"); - CREATE INDEX "idx_publication_project_94129d" - ON "publication" ("project_id"); + CREATE INDEX "idx_placement_post_creativ_1c6807" + ON "placement_post" ("creative_id"); + CREATE INDEX "idx_placement_post_placeme_bd5bc9" + ON "placement_post" ("placement_id"); + CREATE INDEX "idx_placement_post_post_id_998755" + ON "placement_post" ("post_id"); + CREATE INDEX "idx_placement_post_project_94129d" + ON "placement_post" ("project_id"); ALTER TABLE "subscription" - ADD CONSTRAINT "fk_subscrip_publicat_1959be9e" - FOREIGN KEY ("publication_id") - REFERENCES "publication" ("id") ON DELETE CASCADE; + ADD CONSTRAINT "fk_subscrip_placem_1959be9e" + FOREIGN KEY ("placement_post_id") + REFERENCES "placement_post" ("id") ON DELETE CASCADE; - CREATE UNIQUE INDEX "uid_subscriptio_publica_578bfb" - ON "subscription" ("publication_id", "telegram_user_id"); + CREATE UNIQUE INDEX "uid_subscriptio_placem_578bfb" + ON "subscription" ("placement_post_id", "telegram_user_id"); ALTER TABLE "workspace_user_permission_scope" ADD CONSTRAINT "fk_workspac_placemen_8c5d3f2a" @@ -150,9 +151,9 @@ CPM: cpm'; IS 'SELF_PROMO: self_promo STANDARD: standard'; - COMMENT ON TABLE "publication" + COMMENT ON TABLE "placement_post" IS 'Публикация, мониторимая системой (бывший Placement)'; - COMMENT ON COLUMN "publication"."status" + COMMENT ON COLUMN "placement_post"."status" IS 'ACTIVE: active ARCHIVED: archived'; @@ -273,7 +274,7 @@ async def downgrade(db: BaseDBAsyncClient) -> str: "project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE ); - -- Шаг 7: Мигрировать данные из publication в placement + -- Шаг 7: Мигрировать данные из placement_post в placement INSERT INTO "placement" ( "id", "created_at", "updated_at", "deleted_at", "wanted_placement_date", "cost", "comment", "status", @@ -283,17 +284,17 @@ async def downgrade(db: BaseDBAsyncClient) -> str: pub.id, pub.created_at, pub.updated_at, pub.deleted_at, pub.wanted_placement_date, pub.cost, pub.comment, pub.status, pub.creative_id, pub.placement_id, pub.post_id, pub.project_id - FROM "publication" pub; + FROM "placement_post" pub; -- Шаг 8: Обновить foreign keys обратно - -- 8.1: Обновить subscription (publication_id -> placement_id) + -- 8.1: Обновить subscription (placement_post_id -> placement_id) -- Удалить новый foreign key constraint ALTER TABLE "subscription" - DROP CONSTRAINT IF EXISTS "fk_subscrip_publicat_1959be9e"; + DROP CONSTRAINT IF EXISTS "fk_subscrip_placem_1959be9e"; -- Переименовать колонку обратно - ALTER TABLE "subscription" RENAME COLUMN "publication_id" TO "placement_id"; + ALTER TABLE "subscription" RENAME COLUMN "placement_post_id" TO "placement_id"; -- 8.2: Обновить workspace_user_permission_scope.placement_id обратно -- Удалить constraint на новый placement @@ -303,7 +304,7 @@ async def downgrade(db: BaseDBAsyncClient) -> str: -- Обновить данные обратно: новые placement_id -> старые placement_id UPDATE "workspace_user_permission_scope" wups SET placement_id = pub.id - FROM "publication" pub + FROM "placement_post" pub WHERE wups.placement_id = pub.placement_id; -- Шаг 9: Создать индексы и constraints @@ -326,86 +327,86 @@ async def downgrade(db: BaseDBAsyncClient) -> str: REFERENCES "placement" ("id") ON DELETE CASCADE; -- Шаг 10: Удалить новые таблицы - DROP TABLE IF EXISTS "publication"; + DROP TABLE IF EXISTS "placement_post"; DROP TABLE IF EXISTS "new_placement"; """ MODELS_STATE = ( - "eJztXWtz2roW/SsMn3rm5nbCIwnN3LkzhJCWlgADpO09TcdjjCC+MTbHNkkzZ/rfj+QHlv" - "zCL7AN+wtNJW3ZXluvvbS19Xd1pcyRpL3vPPGyjKTqdeXvqsyvEP7DnXVWqfLrtZNBEnR+" - "JhllBarQTNNVXtBx8oKXNIST5kgTVHGti4qMU+WNJJFERcAFRXnpJG1k8a8N4nRlifQnpO" - "KMHz9xsijP0S+k2f9dP3MLEUlz5mXFOXm2kc7pb2sj7eGhd3tnlCSPm3GCIm1WslN6/aY/" - "KfK2+GYjzt8TGZK3RDJSeR3Nqc8gb2l9sJ1kvjFO0NUN2r7q3EmYowW/kQgY1f8sNrJAMK" - "gYTyI/zf9WY8AjKDKBVpR1gsXfv82vcr7ZSK2SR3U+tcfvGpd/GF+paPpSNTINRKq/DUFe" - "501RA1cHSEFF5LM5XvcCeotzdHGF/EFlJV3gzi3R9/YfSUC2ExyUnRZmw2zDlwzTKv6G+V" - "CW3iwNhmA87d13J9P2/Yh8yUrT/pIMiNrTLsmpG6lvrtR3pkoU3D/MfrOtpPKtN/1UIf+t" - "/DkcdN2K25ab/lkl78RvdIWTlVeOn1ONzU61gcElHcVu1vOEimUlQbG5KtZ6eUeveCxGyf" - "TKSmagV+ttD6jWkqjR/uzQDqpjdSxVfsX5zWU34rIn6/56dAm6FImh2s+ElrJDLslT/v2h" - "Xm80rurnjcvWRfPq6qJ13sJljVfyZl2FqPum97E3mLLaIwm/WYxFXUJedPEaRw3A1hZwoY" - "o/paAD3Yr/xUlIXupP+L/1i4sQzL62x8ZKAZdytfuBlVU381gQNxpSjb9j4EjLZAPl3hvo" - "/oHkBQFpGvfEa0/xerxLMFGPP/xgnU+XX+uaF9xAZK3S5UK0XmteNVuNy+YWyG1KGH4mVs" - "SkWjxTtgBJmPHC8yuvzjkmhwJV4gW0QrIftjeW7N2XMZJ445O8iFrG5ciup5iD6W+7edip" - "fgswsg5ICwOuoswIqMr/kZAaBLOWMuOA1JWoabhqThMU0tVSAfJNUZ+1Ne4gD3jyHG3rnp" - "CqCzkcBYJEhhSlrgQNMt6sVX3lTuFlfmm8NXk2eZLNTxHaQXxBVT/uys47CyWv6FLAXgF7" - "BSQHsFegWGCvgL2Kzl7FJQSyJQOOglfR0S+fXjDFqUG83y+/tl9YCMOae/f7lGnpNlDv7t" - "vf/2Bae384+GgXp4Dt9Ic3LjxXaC7yJiwxGiYrlQjbHIgApnU26hEaZ6Me2DZJlh+UC1FC" - "vuz0LjQpwVICelGLgiguFQipkeeHqdbgntFbfEgdOUDURnS20XX8UC+YnyfDgT+YlIgLxw" - "cZf+CPuSjoZxVJ1PSf+xpUKdNqthElXZS19+SBe7KuCBbhQ617VHWtFkgF7qFW03l94wM8" - "acVdebMywO/hV+RlAXmU4EgfbiqrYiPfsvlZjKvtzrT3tXtdMQs8ygQdnHCLU1ThCSeZrS" - "hmg29FaO6twMbecjd1i3LzHY2DmQNWKksGwQt5xtsxyfkCD8HtQdEL4Z2iInEpf0FvnpZ7" - "APJyP9B5aDmcrPKvWzrK1TrwN5omlgFxe9Jp33arv2FzICYlvJlJomB8aFo0nJrKjAdQ5I" - "enyPvKUpSnyjOSqz4kOZV7FkaTS6Qcp28LAlMOTDkQqsCUg2KBKQemPIafpz2DRvZBtAXA" - "cc6CEP1ai1gjCfoCK1nOMe6YOsNGSzZVaTCeFUiFakweihIBEoqAkQED9WBVU1TQdtJPVK" - "OIyz3t03h2WCgf25mhqIJN5zVTbJfhXH3cnDfr5+S3Yf5eGb+C8XtBfpsfnL8bc+O3Zaac" - "VYz8hvG/hfE3XVPd+J0Z6QuqDrNuZKZUHGEryRQQqJdBVHVG1c06Vd2Mrrryzvi3ZpSaOX" - "LNFvXiHyqjjSo88RqyDpxW/rVN+cNNuANCfgjtplV+0Iy3tevyE7gW4FrAJAeu5XQVC1zL" - "sdomJfRLwGtFPLnPfRwTRv32YED8EKwij3J7NBoPTdeENZ7ZX0jauPu525mSNBWRqZ6k9Q" - "YcLvhx3J1MriuizOGyBGXtUe4M70f9rlFcUFZro/EmcW+o1SKwPLVaIMlDslweDvaaOUEv" - "dMtCP8y5H675t6SqZCRBkTkrUsD1hbjU7h5TmQrydV2s3vW+k4FvIf4ig2RndI8HwfUqyf" - "CXsXeXAdILL218YL6TFD7AIZwVc4G7IHJl6x+3w4ebfrcyGnc7vUnPclzcdggj0zBl/5JE" - "kywad9t9PzBnaKGoiJvx6hI/Nz6qXnmA1z1PpxkWvLXkPDZMuv07smi6H15XNCQtyJpppT" - "zKeAAd3LbHeNggXzLn1WTLpcsoyyX3QE8tly7dQwZuniu/2TX4+IgjURIP8kOfH8ELYpuz" - "jYopJQKg+oIqyi94LOEkUX6OswfuEivLkacDHHqgkEk1AvvVkzPM1RGegXodbPAavq+2vd" - "vu2/YuLxVhsWay8TG3QFmpE9kFZWCzoi7ExY0VO0Hg4OgHHP3I4+iHX+fNAD46Rktp8XON" - "ShEAdAImp8XPqam88DGTYYqTR3DaBk7b5H3axgjm5ucrZAV5C3ETsktkerTmh6t7rZCm4T" - "cn//sJx272slgDV5Aj9xgAV5AjVSy4ghzrziU17XkUGRgOmBXKJrL6AXpjBmGBITDV/jhw" - "e2RYqMqKsxdnyQcYn2pgtMnbTwJI4Wp6kg5Ykn2wJC8ietW4J1HTFVVMywkQq/YrqfCTUd" - "9boUf4YsZqOSFehGkpARyJuzWF8yUc3Zrf9sGeGE+xxnGkC0/mkh6oE6BOwMIG6gQUC9TJ" - "sRsz5hQrKBs/Z7hA7sQldarkCbVmiNkLWMnDjG6HswpL1wvodWBUpxtHBIx5e3czrbtNZn" - "dl5eVr4zSKQsW5sNyY/Kwyx8MpxBijCmVsgb3afgcWbBQRAmYYmGGwWgczDBQLZtixL0BL" - "GMwg8iULvYGdIsrFvnjBirjEWedlSIWc/cUJVbOrzkMqjD5Xc4jDOLXzKCchz4NPQp7DeZ" - "wU6zwHNvciOypwbrkTgQ52re2GkMGutW87zAC/b3RdpUXQ3cOS7/zbx1RSbm9ne2Qnn71+" - "uKMmd7+HwuIBp2Zy8A6hmo4fBcm2rBAa0lUwUrDd2oIKCVuj4rOakVd5KobrpZPeXJihZJ" - "nQsFSoWSveK6JCzLao8mZ9CzMabY0Sq3mCxdIP+BA1WKw9MvlHzz3+T4Zbhg5lRABZfOSc" - "IpDFR6pYIIuPlSx+5WWiDSeuGkE+rk4DKylntz0m/Qr+3hThYQx9VHeycQshutseTrYd8x" - "ZVIbejIIpW0iha2zktpkufS+4UocvLEXL3+FlMvCBgW1TYwtxHIWAbBGzLL2Abc4Vb2gaY" - "7aZRXk3QNRNGwPCgHuB5zRaJHcAn3Wll8NDvR9vL1Taz7Vum3KGaUFUV2nI/7JYUA4vPnp" - "QbtuBNKc1dMusjys6ml9WwdNyqliq/4uxbNcFRHvY+gCKHvQ9QLOx9HDs3Xuw7JjJedR7g" - "ioljZnUfBpOHm0ln3LshzO5GtpZqs2TsbiT0Q8D3Yk+/UZIZxysOw1PeB8k9q/XInKBH8k" - "R4QTYapcu0iQGgn+yJQBhGrbI+k2npmaydgPOiaTydbTfDxbSvDLCcWvU9WNWVFky/jlek" - "6AcM0D5cj1sRwVyPpwlkS/YAjbOX6QVonCO39oHGOVLFAo1zrHbSdib1m8tuxGVg5DmXYD" - "aR53ZPaCk7pBl37kO93mhc1c8bl62L5tXVRet8G4DOmxUWie6m95EEo2O0541ORxYpxt8e" - "gIN5MlqmJH6QLEtTv7iIQNPgUoE8jZHnivMnqprOxcWSlQI0bTQlPgGYjNBpY+mx9sGlYC" - "8uBexIGozZUEZTBf/sRi6iqV+wY8ExTO0gE3u3aQ0WdfgCBCxqMLzAogbFgkWd/bRXdDXG" - "s6gLuHW2b/s6w52zKGtpSVmKGBnlGaVdSvdJTVNSUTHHvcB1IeNCYviJGw42WYXT6Rm1lR" - "gTKq4XAOMPDBlrsgy/VDJQvKZo5K1m2+A80EbzgUbvfe0zp9o7dqIM+li1TAjCYNOWiXoI" - "9i3Yt9ksl8EMAvsWFAv27YnYt3E3jFLtFeXRBQu0WWSdQE8bMTbLs/i5GCtgu4HtduiTye" - "7mEmZ3OC0qgvVhMRHZGyHeS7zgYDLYJ7CMBfsEFAv2yanYJyU8SLtG8pyox6PA6qg7uO0N" - "Pl5XrCKPcrvT6Y6mRoREQUBr/CaP8rj7dfiFJKnoRXkuRMxEc40z52ZvMbdAPYIncnTQ7T" - "AcE7bTO2sJ92olhi7kmCrcDHWW6GYo/7EvAxBLfyrVM6DvBjCjg72lh66g53hZxiiMF9nt" - "bsxSWcCJACcCnEihbC7gRE5GscCJACdSGE4kxq3mX3sGH2ItNB/lm/6wY9AhM0kREtIhYa" - "d+bTrkKpAOufJEGAOLHix6sOiLYVbFt+jBIE1tkPpd/bufS3+LuUqM5NhRrFuRi4TSwfgM" - "qh3tYjbYJheV4+DWrNje6A56AeE8EmgPoD3AOgbaAxQLtMex0x7sTJuE+mBryNmFvdq+ve" - "8NuLuHfv+6ws9XoswtcLlHeTQefu52phNu3G3fXldsr3GO9FAq99u4N+1S2a+qqKNHuYOl" - "CKVii9vXdtnyTr5VgVPAqmHUb3e6992B8wb2tVXbd3BK2G/hFLFqaQ/a/f9Nex27Erxck9" - "50UbDrcPJJyxk+TLlOv9f5MqFLvopYQxudEyRReNbwu39qDwbd/vbTnnhZxssz+8vsXPvD" - "7GzjjZLQRvVmlNMCzeDDAk03c+S7novPhpwemxSFEskqhPQefOhz50bKsPnstmmjW2xbGz" - "iB2Wba5tkbb2CZ7WEYAMvs6BfwYJkdqWLBMgPLDCwzsMyKaZlZ7xTTJGOlUizL8rp6OeXG" - "vutS8MiwsWKnh5v7JvCowLnlThA5c1SNixsjdXqoAe8EvFMxeSefvp0BlNGDpOTVoXdfps" - "cMWbvBs+fUDNDrUFWVFT7XEiNC47On1iyaH11XWRF0rzUiNEFzNZxFC3RqKit8rGlQJNK9" - "jVRReKr60OtWzlkYkc47ZQrDlwfeaxX1OitLgel48pQ8n3mbVb3WvGq2GpfN7SVW25QwL3" - "b7nqpgevwFqcEMjT96lMiJx3ujRznSNWKAaBUvJ4C18/MIAOJSwfezkzwXaaDIuu88+3ky" - "HAQQBo6IC8gHGX/gj7ko6GcVSdT0n8WENQRF8tUM02uD9+6+/d2Na6c/vHGbKqSCm3hBCb" - "OfXn7/A5u7TFM=" + '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' + 'OfXn7/A5u7TFM=' ) diff --git a/shared/jwt_base.py b/shared/jwt_base.py index 99951eb..1601ca4 100644 --- a/shared/jwt_base.py +++ b/shared/jwt_base.py @@ -9,4 +9,4 @@ class JWTConfig(BaseModel): class JWTBase: def __init__(self, config: JWTConfig) -> None: - self.config = config \ No newline at end of file + self.config = config diff --git a/shared/telegram_base.py b/shared/telegram_base.py index 26fbff5..2f8d7c6 100644 --- a/shared/telegram_base.py +++ b/shared/telegram_base.py @@ -12,4 +12,3 @@ class TelegramBase: def __init__(self, config: TelegramConfig) -> None: self.bot: Bot = Bot(token=config.TOKEN) logging.info('Telegram bot initialized') - diff --git a/src/adapter/postgres.py b/src/adapter/postgres.py index 327fbe0..2dde1fa 100644 --- a/src/adapter/postgres.py +++ b/src/adapter/postgres.py @@ -392,9 +392,9 @@ class Postgres(DatabaseBase): ) @staticmethod - async def get_publication(workspace_id: uuid.UUID, publication_id: uuid.UUID) -> domain.Publication | None: - return await domain.Publication.get_or_none( - id=publication_id, placement__project__workspace_id=workspace_id + async def get_placement_post(workspace_id: uuid.UUID, placement_post_id: uuid.UUID) -> domain.PlacementPost | None: + return await domain.PlacementPost.get_or_none( + id=placement_post_id, placement__project__workspace_id=workspace_id ).prefetch_related( 'placement', 'placement__project', @@ -406,7 +406,7 @@ class Postgres(DatabaseBase): ) @staticmethod - async def get_workspace_publications( + async def get_workspace_placement_posts( workspace_id: uuid.UUID, project_id: uuid.UUID | None = None, placement_channel_id: uuid.UUID | None = None, @@ -415,8 +415,8 @@ class Postgres(DatabaseBase): allowed_project_ids: set[uuid.UUID] | None = None, date_from: datetime.datetime | None = None, date_to: datetime.datetime | None = None, - ) -> list[domain.Publication]: - query = domain.Publication.filter(placement__project__workspace_id=workspace_id) + ) -> list[domain.PlacementPost]: + query = domain.PlacementPost.filter(placement__project__workspace_id=workspace_id) if project_id: query = query.filter(placement__project_id=project_id) @@ -435,7 +435,7 @@ class Postgres(DatabaseBase): query = query.filter(created_at__lte=date_to) if not include_archived: - query = query.filter(status=domain.PublicationStatus.ACTIVE) + query = query.filter(status=domain.PlacementPostStatus.ACTIVE) return ( await query.prefetch_related( @@ -452,12 +452,12 @@ class Postgres(DatabaseBase): ) @staticmethod - async def create_publication(publication: domain.Publication) -> None: - await publication.save() + async def create_placement_post(placement_post: domain.PlacementPost) -> None: + await placement_post.save() @staticmethod - async def get_publication_by_invite_link(invite_link: str) -> domain.Publication | None: - return await domain.Publication.get_or_none(placement__invite_link=invite_link).prefetch_related( + async def get_placement_post_by_invite_link(invite_link: str) -> domain.PlacementPost | None: + return await domain.PlacementPost.get_or_none(placement__invite_link=invite_link).prefetch_related( 'placement', 'placement__project', 'placement__project__channel', @@ -471,26 +471,28 @@ class Postgres(DatabaseBase): await subscription.save() @staticmethod - async def get_subscription_by_subscriber_and_publication( - telegram_user_id: uuid.UUID, publication_id: uuid.UUID + async def get_subscription_by_subscriber_and_placement_post( + telegram_user_id: uuid.UUID, placement_post_id: uuid.UUID ) -> domain.Subscription | None: - return await domain.Subscription.get_or_none(telegram_user_id=telegram_user_id, publication_id=publication_id) + return await domain.Subscription.get_or_none( + telegram_user_id=telegram_user_id, placement_post_id=placement_post_id + ) @staticmethod async def update_subscription(subscription: domain.Subscription) -> None: await subscription.save() @staticmethod - async def get_subscriptions_for_publications( - publication_ids: list[uuid.UUID], + async def get_subscriptions_for_placement_posts( + placement_post_ids: list[uuid.UUID], *, date_from: datetime.datetime | None = None, date_to: datetime.datetime | None = None, ) -> list[domain.Subscription]: - if not publication_ids: + if not placement_post_ids: return [] - query = domain.Subscription.filter(publication_id__in=publication_ids) + query = domain.Subscription.filter(placement_post_id__in=placement_post_ids) if date_from: query = query.filter(created_at__gte=date_from) @@ -506,10 +508,10 @@ class Postgres(DatabaseBase): return ( await domain.Subscription.filter( telegram_user_id=telegram_user_id, - publication__placement__project_id=project_id, + placement_post__placement__project_id=project_id, status=domain.SubscriptionStatus.ACTIVE, ) - .prefetch_related('publication', 'telegram_user') + .prefetch_related('placement_post', 'telegram_user') .all() ) @@ -520,10 +522,10 @@ class Postgres(DatabaseBase): return ( await domain.Subscription.filter( telegram_user_id=telegram_user_id, - publication__placement__project_id=project_id, + placement_post__placement__project_id=project_id, status=domain.SubscriptionStatus.ACTIVE, ) - .prefetch_related('publication', 'telegram_user') + .prefetch_related('placement_post', 'telegram_user') .first() ) @@ -555,22 +557,22 @@ class Postgres(DatabaseBase): # Count methods @staticmethod - async def count_publications_by_creative(creative_id: uuid.UUID) -> int: - return await domain.Publication.filter(placement__creative_id=creative_id).count() + async def count_placement_posts_by_creative(creative_id: uuid.UUID) -> int: + return await domain.PlacementPost.filter(placement__creative_id=creative_id).count() @staticmethod - async def count_subscriptions_by_publication(publication_id: uuid.UUID) -> int: - return await domain.Subscription.filter(publication_id=publication_id).count() + async def count_subscriptions_by_placement_post(placement_post_id: uuid.UUID) -> int: + return await domain.Subscription.filter(placement_post_id=placement_post_id).count() @staticmethod - async def count_publications_by_creative_batch(creative_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]: + async def count_placement_posts_by_creative_batch(creative_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]: if not creative_ids: return {} from tortoise.functions import Count results = ( - await domain.Publication.filter(placement__creative_id__in=creative_ids) + await domain.PlacementPost.filter(placement__creative_id__in=creative_ids) .group_by('placement__creative_id') .annotate(count=Count('id')) .values('placement__creative_id', 'count') @@ -580,22 +582,22 @@ class Postgres(DatabaseBase): return {cid: counts.get(cid, 0) for cid in creative_ids} @staticmethod - async def count_subscriptions_by_publication_batch(publication_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]: - if not publication_ids: + async def count_subscriptions_by_placement_post_batch(placement_post_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]: + if not placement_post_ids: return {} from tortoise.functions import Count results = ( - await domain.Subscription.filter(publication_id__in=publication_ids) - .group_by('publication_id') + await domain.Subscription.filter(placement_post_id__in=placement_post_ids) + .group_by('placement_post_id') .annotate(count=Count('id')) - .values('publication_id', 'count') + .values('placement_post_id', 'count') ) - counts = {row['publication_id']: row['count'] for row in results} - return {pid: counts.get(pid, 0) for pid in publication_ids} + counts = {row['placement_post_id']: row['count'] for row in results} + return {pid: counts.get(pid, 0) for pid in placement_post_ids} @staticmethod - async def has_publications_for_creative(creative_id: uuid.UUID) -> bool: - return await domain.Publication.filter(placement__creative_id=creative_id).exists() + async def has_placement_posts_for_creative(creative_id: uuid.UUID) -> bool: + return await domain.PlacementPost.filter(placement__creative_id=creative_id).exists() diff --git a/src/controller/http_v1/__init__.py b/src/controller/http_v1/__init__.py index 6bd1412..8be0075 100644 --- a/src/controller/http_v1/__init__.py +++ b/src/controller/http_v1/__init__.py @@ -5,7 +5,7 @@ from src.controller.http_v1.auth import auth_router from src.controller.http_v1.channels import channels_router from src.controller.http_v1.creatives import creatives_router from src.controller.http_v1.internal import internal_router -from src.controller.http_v1.placements import publications_router +from src.controller.http_v1.placements import placement_posts_router from src.controller.http_v1.projects import projects_router from src.controller.http_v1.purchases import placements_user_router from src.controller.http_v1.views import views_router @@ -23,7 +23,7 @@ api_v1_router.include_router(channels_router) api_v1_router.include_router(projects_router) api_v1_router.include_router(placements_user_router) # User-managed placements api_v1_router.include_router(creatives_router) -api_v1_router.include_router(publications_router) # System-managed publications +api_v1_router.include_router(placement_posts_router) # System-managed placement_posts api_v1_router.include_router(views_router) api_v1_router.include_router(analytics_router) api_v1_router.include_router(workspaces_router) diff --git a/src/controller/http_v1/placements.py b/src/controller/http_v1/placements.py index 60b69e0..7a5e68c 100644 --- a/src/controller/http_v1/placements.py +++ b/src/controller/http_v1/placements.py @@ -7,21 +7,21 @@ from fastapi.routing import APIRouter from src import deps, dto from src.adapter.jwt import JWTPayload -# Publications router - System-managed actual published posts -publications_router = APIRouter(prefix='/workspaces/{workspace_id}/publications', tags=['publications']) +# PlacementPosts router - System-managed actual published posts +placement_posts_router = APIRouter(prefix='/workspaces/{workspace_id}/placement_posts', tags=['placement_posts']) -@publications_router.get('') -async def list_publications( +@placement_posts_router.get('') +async def list_placement_posts( workspace_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], project_id: uuid.UUID | None = None, placement_channel_id: uuid.UUID | None = None, creative_id: uuid.UUID | None = None, include_archived: bool = False, -) -> dto.GetPublicationsOutput: - """List all publications (system-managed actual posts) in workspace""" - input = dto.GetPublicationsInput( +) -> dto.GetPlacementPostsOutput: + """List all placement_posts (system-managed actual posts) in workspace""" + input = dto.GetPlacementPostsInput( user_id=current_user.user_id, workspace_id=workspace_id, project_id=project_id, @@ -30,24 +30,24 @@ async def list_publications( include_archived=include_archived, ) - result = await deps.get_usecase().get_publications(input=input) + result = await deps.get_usecase().get_placement_posts(input=input) return result -@publications_router.get('/{publication_id}') -async def get_publication( - publication_id: uuid.UUID, +@placement_posts_router.get('/{placement_post_id}') +async def get_placement_post( + placement_post_id: uuid.UUID, workspace_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], -) -> dto.PublicationOutput: - """Get single publication by ID""" - input = dto.GetPublicationInput( - publication_id=publication_id, +) -> dto.PlacementPostOutput: + """Get single placement_post by ID""" + input = dto.GetPlacementPostInput( + placement_post_id=placement_post_id, user_id=current_user.user_id, workspace_id=workspace_id, ) - return await deps.get_usecase().get_publication(input=input) + return await deps.get_usecase().get_placement_post(input=input) -# Publication creation/updates are handled by worker; API exposes only read endpoints. +# PlacementPost creation/updates are handled by worker; API exposes only read endpoints. diff --git a/src/controller/http_v1/views.py b/src/controller/http_v1/views.py index 5052809..baeec9e 100644 --- a/src/controller/http_v1/views.py +++ b/src/controller/http_v1/views.py @@ -21,7 +21,7 @@ async def get_views_history( to_date: datetime.datetime | None = None, ) -> Page[dto.PostViewsHistoryOutput]: input_data = dto.GetViewsHistoryInput( - publication_id=placement_id, + placement_post_id=placement_id, user_id=current_user.user_id, workspace_id=workspace_id, from_date=from_date, diff --git a/src/controller/worker/fetch_placement_post.py b/src/controller/worker/fetch_placement_post.py index 415f96a..ac98a53 100644 --- a/src/controller/worker/fetch_placement_post.py +++ b/src/controller/worker/fetch_placement_post.py @@ -10,8 +10,8 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -class FetchPublicationPostWorker(WorkerBase): - """Worker that fetches posts from channels and creates Publications from Placements""" +class FetchPlacementPostWorker(WorkerBase): + """Worker that fetches posts from channels and creates PlacementPosts from Placements""" async def _cycle_func(self) -> None: - await deps.get_usecase().fetch_publication_post_cycle(self.config.INTERVAL_SECONDS) + await deps.get_usecase().fetch_placement_post_cycle(self.config.INTERVAL_SECONDS) diff --git a/src/domain/__init__.py b/src/domain/__init__.py index 1b9c101..476c60f 100644 --- a/src/domain/__init__.py +++ b/src/domain/__init__.py @@ -19,8 +19,8 @@ __all__ = ( 'Placement', 'PlacementStatus', 'PlacementType', - 'Publication', - 'PublicationStatus', + 'PlacementPost', + 'PlacementPostStatus', 'Creative', 'replace_invite_link_with_tag', 'validate_media_size', @@ -49,7 +49,7 @@ __all__ = ( 'ProjectNotFound', 'ProjectChannelConflict', 'PlacementNotFound', - 'PublicationNotFound', + 'PlacementPostNotFound', 'ChannelNotFound', 'ChannelAlreadyExists', 'ChannelNoAdminRights', @@ -83,9 +83,9 @@ from .error import ( LoginTokenExpired, LoginTokenNotFound, PlacementNotFound, + PlacementPostNotFound, ProjectChannelConflict, ProjectNotFound, - PublicationNotFound, TelegramChannelNotFound, UserByUsernameNotFound, UserNotFound, @@ -104,10 +104,10 @@ from .placement import ( PlacementStatus, PlacementType, ) +from .placement_post import PlacementPost, PlacementPostStatus, PostViewsAvailability from .post import Post from .post_views_history import PostViewsHistory from .project import Project, ProjectStatus -from .publication import PostViewsAvailability, Publication, PublicationStatus from .subscription import Subscription, SubscriptionStatus from .telegram_user import TelegramUser from .user import User diff --git a/src/domain/error.py b/src/domain/error.py index 7b0fcb5..85eafad 100644 --- a/src/domain/error.py +++ b/src/domain/error.py @@ -53,10 +53,10 @@ def PlacementNotFound(placement_id: uuid.UUID | None = None) -> HTTPException: return HTTPException(status.HTTP_404_NOT_FOUND, f'Placement {placement_id} not found') -def PublicationNotFound(publication_id: uuid.UUID | None = None) -> HTTPException: - if publication_id is None: - return HTTPException(status.HTTP_404_NOT_FOUND, 'Publication not found') - return HTTPException(status.HTTP_404_NOT_FOUND, f'Publication {publication_id} not found') +def PlacementPostNotFound(placement_post_id: uuid.UUID | None = None) -> HTTPException: + if placement_post_id is None: + return HTTPException(status.HTTP_404_NOT_FOUND, 'PlacementPost not found') + return HTTPException(status.HTTP_404_NOT_FOUND, f'PlacementPost {placement_post_id} not found') def ChannelAlreadyExists(telegram_id: int) -> HTTPException: @@ -90,7 +90,7 @@ def CreativeMediaTooLarge(max_bytes: int) -> HTTPException: def CreativeInUse(creative_id: uuid.UUID) -> HTTPException: return HTTPException( status.HTTP_400_BAD_REQUEST, - f'Creative {creative_id} is used in active publications and cannot be deleted', + f'Creative {creative_id} is used in active placement_posts and cannot be deleted', ) diff --git a/src/domain/publication.py b/src/domain/placement_post.py similarity index 68% rename from src/domain/publication.py rename to src/domain/placement_post.py index 7252fb1..9795773 100644 --- a/src/domain/publication.py +++ b/src/domain/placement_post.py @@ -12,10 +12,10 @@ if TYPE_CHECKING: from .post import Post from .project import Project -__all__ = ['Publication', 'PublicationStatus', 'PostViewsAvailability'] +__all__ = ['PlacementPost', 'PlacementPostStatus', 'PostViewsAvailability'] -class PublicationStatus(str, enum.Enum): +class PlacementPostStatus(str, enum.Enum): ACTIVE = 'active' # Пост найден и отслеживается ARCHIVED = 'archived' # Вручную архивировано @@ -27,25 +27,25 @@ class PostViewsAvailability(str, enum.Enum): MANUAL = 'manual' # Просмотры вводятся вручную -class Publication(TimestampedModel): +class PlacementPost(TimestampedModel): wanted_placement_date = fields.DatetimeField(db_column='placement_date') cost = fields.FloatField(null=True) comment = fields.TextField(null=True) - status = fields.CharEnumField(PublicationStatus, default=PublicationStatus.ACTIVE) + status = fields.CharEnumField(PlacementPostStatus, default=PlacementPostStatus.ACTIVE) project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField( - 'models.Project', related_name='publications', on_delete=fields.CASCADE, index=True + 'models.Project', related_name='placement_posts', on_delete=fields.CASCADE, index=True ) creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField( - 'models.Creative', related_name='publications', on_delete=fields.CASCADE, index=True + 'models.Creative', related_name='placement_posts', on_delete=fields.CASCADE, index=True ) placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField( - 'models.Placement', related_name='publications', on_delete=fields.CASCADE, index=True + 'models.Placement', related_name='placement_posts', on_delete=fields.CASCADE, index=True ) post: fields.ForeignKeyRelation['Post'] | None = fields.ForeignKeyField( - 'models.Post', related_name='publications', on_delete=fields.SET_NULL, null=True, index=True + 'models.Post', related_name='placement_posts', on_delete=fields.SET_NULL, null=True, index=True ) if TYPE_CHECKING: @@ -55,4 +55,4 @@ class Publication(TimestampedModel): placement_id: UUID class Meta: - table = 'publication' + table = 'placement_post' diff --git a/src/domain/subscription.py b/src/domain/subscription.py index c57707c..e97a916 100644 --- a/src/domain/subscription.py +++ b/src/domain/subscription.py @@ -7,7 +7,7 @@ from tortoise import fields from .base import TimestampedModel if TYPE_CHECKING: - from .publication import Publication + from .placement_post import PlacementPost from .telegram_user import TelegramUser @@ -21,17 +21,17 @@ class Subscription(TimestampedModel): status = fields.CharEnumField(SubscriptionStatus, default=SubscriptionStatus.ACTIVE) unsubscribed_at = fields.DatetimeField(null=True) - publication: fields.ForeignKeyRelation['Publication'] = fields.ForeignKeyField( - 'models.Publication', related_name='subscriptions', on_delete=fields.CASCADE, index=True + placement_post: fields.ForeignKeyRelation['PlacementPost'] = fields.ForeignKeyField( + 'models.PlacementPost', related_name='subscriptions', on_delete=fields.CASCADE, index=True ) telegram_user: fields.ForeignKeyRelation['TelegramUser'] = fields.ForeignKeyField( 'models.TelegramUser', related_name='subscriptions', on_delete=fields.CASCADE, index=True ) if TYPE_CHECKING: - publication_id: UUID + placement_post_id: UUID telegram_user_id: UUID class Meta: table = 'subscription' - unique_together = (('publication_id', 'telegram_user_id'),) + unique_together = (('placement_post_id', 'telegram_user_id'),) diff --git a/src/dto/__init__.py b/src/dto/__init__.py index da2002a..e9f0215 100644 --- a/src/dto/__init__.py +++ b/src/dto/__init__.py @@ -25,10 +25,10 @@ __all__ = ( 'GetPlacementsInput', 'GetPlacementsOutput', 'GetPlacementInput', - 'PublicationOutput', - 'GetPublicationsInput', - 'GetPublicationsOutput', - 'GetPublicationInput', + 'PlacementPostOutput', + 'GetPlacementPostsInput', + 'GetPlacementPostsOutput', + 'GetPlacementPostInput', 'CreativeButton', 'CreativeOutput', 'GetCreativesInput', @@ -132,10 +132,10 @@ from .creative import ( UpdateCreativeInput, ) from .placement import ( - GetPublicationInput, - GetPublicationsInput, - GetPublicationsOutput, - PublicationOutput, + GetPlacementPostInput, + GetPlacementPostsInput, + GetPlacementPostsOutput, + PlacementPostOutput, ) from .project import ( ArchiveProjectInput, diff --git a/src/dto/placement.py b/src/dto/placement.py index ca51ff4..cfbfe37 100644 --- a/src/dto/placement.py +++ b/src/dto/placement.py @@ -6,7 +6,7 @@ import pydantic from src import domain -class PublicationOutput(pydantic.BaseModel): +class PlacementPostOutput(pydantic.BaseModel): id: uuid.UUID project_id: uuid.UUID project_channel_title: str | None @@ -18,15 +18,15 @@ class PublicationOutput(pydantic.BaseModel): cost: float | None comment: str | None ad_post_url: str | None - invite_link_type: domain.purchase.InviteLinkType + invite_link_type: domain.InviteLinkType invite_link: str - status: domain.PublicationStatus + status: domain.PlacementPostStatus subscriptions_count: int placement_id: uuid.UUID # Ссылка на Placement created_at: datetime.datetime -class GetPublicationsInput(pydantic.BaseModel): +class GetPlacementPostsInput(pydantic.BaseModel): user_id: uuid.UUID workspace_id: uuid.UUID project_id: uuid.UUID | None = None @@ -35,11 +35,11 @@ class GetPublicationsInput(pydantic.BaseModel): include_archived: bool = False -class GetPublicationsOutput(pydantic.BaseModel): - publications: list[PublicationOutput] +class GetPlacementPostsOutput(pydantic.BaseModel): + placement_posts: list[PlacementPostOutput] -class GetPublicationInput(pydantic.BaseModel): - publication_id: uuid.UUID +class GetPlacementPostInput(pydantic.BaseModel): + placement_post_id: uuid.UUID user_id: uuid.UUID workspace_id: uuid.UUID diff --git a/src/dto/project.py b/src/dto/project.py index 344701d..708cecc 100644 --- a/src/dto/project.py +++ b/src/dto/project.py @@ -32,11 +32,11 @@ class ProjectOutput(pydantic.BaseModel): title: str username: str | None status: ProjectStatus - purchase_invite_type_default: domain.purchase.InviteLinkType + purchase_invite_type_default: domain.InviteLinkType class UpdateProjectInviteLinkTypeInput(pydantic.BaseModel): - purchase_invite_type_default: domain.purchase.InviteLinkType + purchase_invite_type_default: domain.InviteLinkType class GetWorkspaceProjectsInput(pydantic.BaseModel): diff --git a/src/dto/views.py b/src/dto/views.py index 5c647a8..c5dac33 100644 --- a/src/dto/views.py +++ b/src/dto/views.py @@ -17,7 +17,7 @@ class PostViewsHistoryOutput(pydantic.BaseModel): class GetViewsHistoryInput(pydantic.BaseModel): """Получить историю просмотров для публикации.""" - publication_id: uuid.UUID + placement_post_id: uuid.UUID user_id: uuid.UUID workspace_id: uuid.UUID from_date: datetime.datetime | None = None # Фильтр: с какой даты diff --git a/src/dto/workspace.py b/src/dto/workspace.py index 030bd23..03522ad 100644 --- a/src/dto/workspace.py +++ b/src/dto/workspace.py @@ -63,7 +63,7 @@ class WorkspaceMemberOutput(pydantic.BaseModel): # Определяем тип и ID scope на основе заполненных полей scope_type = None scope_id = None - + if scope.project_id: scope_type = domain.PermissionScopeType.PROJECT scope_id = scope.project_id @@ -76,7 +76,7 @@ class WorkspaceMemberOutput(pydantic.BaseModel): elif scope.channel_id: scope_type = domain.PermissionScopeType.CHANNEL scope_id = scope.channel_id - + if scope_type and scope_id: permissions_map.setdefault(scope.permission, []).append( WorkspacePermissionScopeOutput(type=scope_type, id=scope_id) diff --git a/src/main.py b/src/main.py index 1795bed..554c8bb 100644 --- a/src/main.py +++ b/src/main.py @@ -16,7 +16,7 @@ from src.adapter.s3 import S3 from src.adapter.telegram_bot import TelegramBot from src.config import settings from src.controller.http_v1 import api_router -from src.controller.worker.fetch_placement_post import FetchPublicationPostWorker +from src.controller.worker.fetch_placement_post import FetchPlacementPostWorker from src.usecase import Usecase @@ -24,11 +24,11 @@ from src.usecase import Usecase async def lifespan(_: FastAPI) -> AsyncGenerator[None]: await postgres.connect() await s3.connect() - await fetch_publication_post_worker.start() + await fetch_placement_post_worker.start() yield - await fetch_publication_post_worker.stop() + await fetch_placement_post_worker.stop() await s3.close() await postgres.close() @@ -51,7 +51,7 @@ usecase = Usecase( ) deps.set_usecase(usecase) -fetch_publication_post_worker = FetchPublicationPostWorker(config=WorkerConfig(INTERVAL_SECONDS=5)) +fetch_placement_post_worker = FetchPlacementPostWorker(config=WorkerConfig(INTERVAL_SECONDS=5)) app = FastAPI( lifespan=lifespan, diff --git a/src/usecase/__init__.py b/src/usecase/__init__.py index fa9dc5d..f97e035 100644 --- a/src/usecase/__init__.py +++ b/src/usecase/__init__.py @@ -27,9 +27,9 @@ from .creative.delete_creative import delete_creative from .creative.get_creative import get_creative from .creative.get_creatives import get_creatives from .creative.update_creative import update_creative -from .placement.fetch_publication_post_cycle import fetch_publication_post_cycle -from .placement.get_publication import get_publication -from .placement.get_publications import get_publications +from .placement.fetch_placement_post_cycle import fetch_placement_post_cycle +from .placement.get_placement_post import get_placement_post +from .placement.get_placement_posts import get_placement_posts from .project.archive_project import archive_project from .project.delete_project import delete_project from .project.disconnect_project_by_tg_id import disconnect_project_by_tg_id @@ -171,10 +171,10 @@ class Usecase: create_creative = create_creative update_creative = update_creative delete_creative = delete_creative - # Publication (system-managed) use cases - get_publications = get_publications - get_publication = get_publication - fetch_publication_post_cycle = fetch_publication_post_cycle + # PlacementPost (system-managed) use cases + get_placement_posts = get_placement_posts + get_placement_post = get_placement_post + fetch_placement_post_cycle = fetch_placement_post_cycle handle_subscription = handle_subscription handle_unsubscription = handle_unsubscription get_views_history = get_views_history diff --git a/src/usecase/analytics/get_channel_analytics.py b/src/usecase/analytics/get_channel_analytics.py index 7677ba2..b4f8503 100644 --- a/src/usecase/analytics/get_channel_analytics.py +++ b/src/usecase/analytics/get_channel_analytics.py @@ -29,7 +29,7 @@ async def get_channel_analytics( else: allowed_project_filter = allowed_project_ids - placements = await self.database.get_workspace_publications( + placements = await self.database.get_workspace_placement_posts( input.workspace_id, input.project_id, include_archived=False, @@ -38,8 +38,8 @@ async def get_channel_analytics( # Collect unique channels from placements channel_map: dict[UUID, domain.Channel] = {} - for publication in placements: - placement = publication.placement + for placement_post in placements: + placement = placement_post.placement if placement and placement.channel: channel_map[placement.channel_id] = placement.channel channels = list(channel_map.values()) @@ -59,22 +59,22 @@ async def get_channel_analytics( # Batch fetch subscriptions counts placement_ids = [p.id for p in placements] - subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids) + subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids) - for publication in placements: - placement = publication.placement + for placement_post in placements: + placement = placement_post.placement if not placement: continue stats = channel_stats.get(placement.channel_id) if stats is None: continue - stats.total_cost += publication.cost if publication.cost is not None else 0 - stats.total_subscriptions += subscriptions_counts.get(publication.id, 0) + stats.total_cost += placement_post.cost if placement_post.cost is not None else 0 + stats.total_subscriptions += subscriptions_counts.get(placement_post.id, 0) # Get views from batch data - if publication.post and publication.post.id in views_map: - views_count = views_map[publication.post.id][0] + if placement_post.post and placement_post.post.id in views_map: + views_count = views_map[placement_post.post.id][0] stats.total_views += views_count stats.placements_count += 1 diff --git a/src/usecase/analytics/get_creatives_analytics.py b/src/usecase/analytics/get_creatives_analytics.py index d465abe..04f11da 100644 --- a/src/usecase/analytics/get_creatives_analytics.py +++ b/src/usecase/analytics/get_creatives_analytics.py @@ -33,7 +33,7 @@ async def get_creatives_analytics( include_archived=False, allowed_project_ids=allowed_project_ids, ) - placements = await self.database.get_workspace_publications( + placements = await self.database.get_workspace_placement_posts( input.workspace_id, input.project_id, include_archived=False, @@ -55,7 +55,7 @@ async def get_creatives_analytics( # Batch fetch subscriptions counts placement_ids = [p.id for p in placements] - subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids) + subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids) for p in placements: stats = creative_stats.get(p.creative_id) diff --git a/src/usecase/analytics/get_overview_analytics.py b/src/usecase/analytics/get_overview_analytics.py index a7e2263..e83a3c2 100644 --- a/src/usecase/analytics/get_overview_analytics.py +++ b/src/usecase/analytics/get_overview_analytics.py @@ -55,7 +55,7 @@ async def get_overview_analytics( period_length = raw_duration if raw_duration.total_seconds() > 0 else datetime.timedelta(days=1) previous_period_start = input.date_from - period_length - placements = await self.database.get_workspace_publications( + placements = await self.database.get_workspace_placement_posts( input.workspace_id, project_id=input.project_id, include_archived=False, @@ -64,18 +64,18 @@ async def get_overview_analytics( date_to=input.date_to, ) - current_placements: list[domain.Publication] = [] - previous_placements: list[domain.Publication] = [] + current_placements: list[domain.PlacementPost] = [] + previous_placements: list[domain.PlacementPost] = [] - for publication in placements: - placement_date = publication.wanted_placement_date + for placement_post in placements: + placement_date = placement_post.wanted_placement_date if placement_date >= input.date_from and placement_date <= input.date_to: - current_placements.append(publication) + current_placements.append(placement_post) elif placement_date >= previous_period_start and placement_date < input.date_from: - previous_placements.append(publication) + previous_placements.append(placement_post) placement_ids = [p.id for p in placements] - subscriptions = await self.database.get_subscriptions_for_publications( + subscriptions = await self.database.get_subscriptions_for_placement_posts( placement_ids, date_from=previous_period_start, date_to=input.date_to ) @@ -89,30 +89,30 @@ async def get_overview_analytics( continue if created_at >= input.date_from and created_at <= input.date_to: - subs_per_placement_current[sub.publication_id] += 1 + subs_per_placement_current[sub.placement_post_id] += 1 subs_per_day_current[created_at.date()] += 1 elif created_at >= previous_period_start and created_at < input.date_from: - subs_per_placement_previous[sub.publication_id] += 1 + subs_per_placement_previous[sub.placement_post_id] += 1 post_ids = [p.post.id for p in placements if p.post] views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {} def _aggregate_totals( - placement_list: list[domain.Publication], subs_per_placement: dict[UUID, int] + placement_list: list[domain.PlacementPost], subs_per_placement: dict[UUID, int] ) -> tuple[float, int, int]: total_cost = 0.0 total_subscriptions = 0 total_views = 0 - for publication in placement_list: - if publication.cost is not None: - total_cost += publication.cost + for placement_post in placement_list: + if placement_post.cost is not None: + total_cost += placement_post.cost - subs = subs_per_placement.get(publication.id, 0) + subs = subs_per_placement.get(placement_post.id, 0) total_subscriptions += subs - if publication.post and publication.post.id in views_map: - total_views += views_map[publication.post.id][0] + if placement_post.post and placement_post.post.id in views_map: + total_views += views_map[placement_post.post.id][0] return total_cost, total_subscriptions, total_views @@ -130,16 +130,16 @@ async def get_overview_analytics( project_spending_map: dict[UUID, float] = defaultdict(float) project_meta: dict[UUID, domain.Project] = {} - for publication in current_placements: - placement_day = publication.wanted_placement_date.date() - project_meta[publication.project_id] = publication.project + for placement_post in current_placements: + placement_day = placement_post.wanted_placement_date.date() + project_meta[placement_post.project_id] = placement_post.project - cost_value = publication.cost or 0.0 + cost_value = placement_post.cost or 0.0 cost_per_day[placement_day] += cost_value - project_spending_map[publication.project_id] += cost_value - subs = subs_per_placement_current.get(publication.id, 0) + project_spending_map[placement_post.project_id] += cost_value + subs = subs_per_placement_current.get(placement_post.id, 0) - placement = publication.placement + placement = placement_post.placement if placement: channel_id = placement.channel_id if channel_id not in channel_stats: diff --git a/src/usecase/analytics/get_placements_analytics.py b/src/usecase/analytics/get_placements_analytics.py index 350ad80..6dea642 100644 --- a/src/usecase/analytics/get_placements_analytics.py +++ b/src/usecase/analytics/get_placements_analytics.py @@ -37,7 +37,7 @@ async def get_placements_analytics( context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id) allowed_project_ids = None - placements = await self.database.get_workspace_publications( + placements = await self.database.get_workspace_placement_posts( input.workspace_id, input.project_id, include_archived=False, @@ -50,7 +50,7 @@ async def get_placements_analytics( # Batch fetch subscriptions counts placement_ids = [p.id for p in placements] - subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids) + subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids) return [ dto.PlacementAnalyticsOutput( diff --git a/src/usecase/analytics/get_projects_analytics.py b/src/usecase/analytics/get_projects_analytics.py index ee246ae..6ca68d4 100644 --- a/src/usecase/analytics/get_projects_analytics.py +++ b/src/usecase/analytics/get_projects_analytics.py @@ -47,7 +47,7 @@ def _format_period_label(dt: datetime.datetime, grouping: dto.DateGrouping) -> s if week_start.month == week_end.month: return f'{week_start.day}-{week_end.day} {month_names[week_start.month - 1]}' else: - return f'{week_start.day} {month_names[week_start.month - 1]}-{week_end.day} {month_names[week_end.month - 1]}' + return f'{week_start.day} {month_names[week_start.month - 1]}-{week_end.day} {month_names[week_end.month - 1]}' # noqa: E501 case dto.DateGrouping.MONTH: # "дек 2024" month_names = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'] @@ -60,20 +60,20 @@ def _format_period_label(dt: datetime.datetime, grouping: dto.DateGrouping) -> s raise ValueError('Invalid date grouping') -def _get_grouping_date(publication: domain.Publication, date_grouping: dto.DateGroupingType) -> datetime.datetime: +def _get_grouping_date(placement_post: domain.PlacementPost, date_grouping: dto.DateGroupingType) -> datetime.datetime: match date_grouping: case dto.DateGroupingType.PLACEMENT_DATE: - return publication.wanted_placement_date + return placement_post.wanted_placement_date case dto.DateGroupingType.PURCHASE_DATE: - if publication.placement: - return publication.placement.created_at - return publication.wanted_placement_date # Fallback + if placement_post.placement: + return placement_post.placement.created_at + return placement_post.wanted_placement_date # Fallback case dto.DateGroupingType.LINK_DATE: - if publication.placement: - return publication.placement.created_at - return publication.wanted_placement_date # Fallback + if placement_post.placement: + return placement_post.placement.created_at + return placement_post.wanted_placement_date # Fallback case _: - return publication.wanted_placement_date + return placement_post.wanted_placement_date @dataclass @@ -183,7 +183,7 @@ async def get_projects_analytics( else: allowed_project_ids = set(input.project_ids) - placements = await self.database.get_workspace_publications( + placements = await self.database.get_workspace_placement_posts( input.workspace_id, project_id=None, include_archived=False, @@ -194,28 +194,28 @@ async def get_projects_analytics( # Фильтрация по датам на основе date_grouping filtered_placements = [] - for publication in placements: - grouping_date = _get_grouping_date(publication, input.date_grouping) + for placement_post in placements: + grouping_date = _get_grouping_date(placement_post, input.date_grouping) if input.date_from and grouping_date < input.date_from: continue if input.date_to and grouping_date > input.date_to: continue - filtered_placements.append(publication) + filtered_placements.append(placement_post) # Batch fetch views data post_ids = [p.post.id for p in filtered_placements if p.post] views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {} # Batch fetch subscriptions counts - publication_ids = [p.id for p in filtered_placements] - subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(publication_ids) + placement_post_ids = [p.id for p in filtered_placements] + subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids) # Группировка по периодам period_data: dict[str, PeriodMetrics] = defaultdict(PeriodMetrics) total_metrics = PeriodMetrics() - for publication in filtered_placements: - grouping_date = _get_grouping_date(publication, input.date_grouping) + for placement_post in filtered_placements: + grouping_date = _get_grouping_date(placement_post, input.date_grouping) period = _format_period(grouping_date, input.grouping) pd = period_data[period] @@ -223,25 +223,25 @@ async def get_projects_analytics( pd.purchases_count += 1 total_metrics.purchases_count += 1 - cost = publication.cost or 0.0 + cost = placement_post.cost or 0.0 pd.total_cost += cost total_metrics.total_cost += cost - subs_count = subscriptions_counts.get(publication.id, 0) + subs_count = subscriptions_counts.get(placement_post.id, 0) pd.total_subscriptions += subs_count pd.clicks_count += subs_count total_metrics.total_subscriptions += subs_count total_metrics.clicks_count += subs_count - if publication.post and publication.post.id in views_map: - views_count = views_map[publication.post.id][0] + if placement_post.post and placement_post.post.id in views_map: + views_count = views_map[placement_post.post.id][0] pd.total_views += views_count pd.reach_volume += views_count total_metrics.total_views += views_count total_metrics.reach_volume += views_count # Расчет скидок - placement = publication.placement + placement = placement_post.placement if placement and placement.cost_before_bargain and placement.cost_before_bargain > cost: discount = placement.cost_before_bargain - cost discount_percent = ( diff --git a/src/usecase/analytics/get_spending_analytics.py b/src/usecase/analytics/get_spending_analytics.py index a3a0a44..fb2195c 100644 --- a/src/usecase/analytics/get_spending_analytics.py +++ b/src/usecase/analytics/get_spending_analytics.py @@ -46,7 +46,7 @@ async def get_spending_analytics( context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id) allowed_project_ids = None - placements = await self.database.get_workspace_publications( + placements = await self.database.get_workspace_placement_posts( input.workspace_id, input.project_id, include_archived=False, @@ -76,7 +76,7 @@ async def get_spending_analytics( # Batch fetch subscriptions counts placement_ids = [p.id for p in filtered] - subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids) + subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids) # Группировка по периодам period_data: dict[str, PeriodData] = defaultdict(PeriodData) diff --git a/src/usecase/channel/attach_channel_to_workspace.py b/src/usecase/channel/attach_channel_to_workspace.py index 30c2e2e..5abbe4c 100644 --- a/src/usecase/channel/attach_channel_to_workspace.py +++ b/src/usecase/channel/attach_channel_to_workspace.py @@ -11,9 +11,7 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -async def attach_channel_to_workspace( - self: 'Usecase', input: dto.AttachChannelToWorkspaceInput -) -> dto.ProjectOutput: +async def attach_channel_to_workspace(self: 'Usecase', input: dto.AttachChannelToWorkspaceInput) -> dto.ProjectOutput: """Привязать канал к workspace (вызывается из Golang бота после выбора workspace пользователем)""" channel = await self.database.get_channel(channel_id=input.channel_id) diff --git a/src/usecase/channel/get_channel.py b/src/usecase/channel/get_channel.py index f70acec..7996921 100644 --- a/src/usecase/channel/get_channel.py +++ b/src/usecase/channel/get_channel.py @@ -19,4 +19,4 @@ async def get_channel(self: 'Usecase', input: dto.GetChannelInput) -> dto.Channe telegram_id=channel.telegram_id, title=channel.title, username=channel.username, - ) \ No newline at end of file + ) diff --git a/src/usecase/creative/delete_creative.py b/src/usecase/creative/delete_creative.py index c590368..7ade3be 100644 --- a/src/usecase/creative/delete_creative.py +++ b/src/usecase/creative/delete_creative.py @@ -22,9 +22,9 @@ async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> No context.ensure_project_permission(domain.PermissionKey.PROJECTS_WRITE, creative.project_id) - has_publications = await self.database.has_publications_for_creative(creative.id) - if has_publications: - log.warning('Creative %s is used in publications and cannot be deleted', input.creative_id) + has_placement_posts = await self.database.has_placement_posts_for_creative(creative.id) + if has_placement_posts: + log.warning('Creative %s is used in placement_posts and cannot be deleted', input.creative_id) raise domain.CreativeInUse(input.creative_id) media_key = creative.media_s3_key diff --git a/src/usecase/creative/get_creative.py b/src/usecase/creative/get_creative.py index 90cef7b..869de95 100644 --- a/src/usecase/creative/get_creative.py +++ b/src/usecase/creative/get_creative.py @@ -20,7 +20,7 @@ async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.Crea context.ensure_project_permission(domain.PermissionKey.PROJECTS_READ, creative.project_id) - placements_count = await self.database.count_publications_by_creative(creative.id) + placements_count = await self.database.count_placement_posts_by_creative(creative.id) return dto.CreativeOutput( id=creative.id, diff --git a/src/usecase/creative/get_creatives.py b/src/usecase/creative/get_creatives.py index 79516ab..1ad64a4 100644 --- a/src/usecase/creative/get_creatives.py +++ b/src/usecase/creative/get_creatives.py @@ -25,7 +25,7 @@ async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> list[d ) creative_ids = [c.id for c in creatives] - placements_counts = await self.database.count_publications_by_creative_batch(creative_ids) + placements_counts = await self.database.count_placement_posts_by_creative_batch(creative_ids) return [ dto.CreativeOutput( diff --git a/src/usecase/creative/update_creative.py b/src/usecase/creative/update_creative.py index d1ca369..6385ef4 100644 --- a/src/usecase/creative/update_creative.py +++ b/src/usecase/creative/update_creative.py @@ -69,7 +69,7 @@ async def update_creative( await self.database.update_creative(creative) - placements_count = await self.database.count_publications_by_creative(creative.id) + placements_count = await self.database.count_placement_posts_by_creative(creative.id) return dto.CreativeOutput( id=creative.id, diff --git a/src/usecase/placement/fetch_publication_post_cycle.py b/src/usecase/placement/fetch_placement_post_cycle.py similarity index 64% rename from src/usecase/placement/fetch_publication_post_cycle.py rename to src/usecase/placement/fetch_placement_post_cycle.py index 07d943f..6eebdd1 100644 --- a/src/usecase/placement/fetch_publication_post_cycle.py +++ b/src/usecase/placement/fetch_placement_post_cycle.py @@ -9,15 +9,13 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -async def fetch_publication_post_cycle(self: 'Usecase', interval_seconds: int) -> None: - """Находит посты в каналах по invite_link из Placement и создает Publication""" - log.debug('Starting fetch_publication_post_cycle') +async def fetch_placement_post_cycle(self: 'Usecase', interval_seconds: int) -> None: + """Находит посты в каналах по invite_link из Placement и создает PlacementPost""" + log.debug('Starting fetch_placement_post_cycle') # Получаем все approved Placements с invite_link placements = ( - await domain.Placement.filter( - status__in=[domain.PlacementStatus.APPROVED, domain.PlacementStatus.IN_PROGRESS] - ) + await domain.Placement.filter(status__in=[domain.PlacementStatus.APPROVED, domain.PlacementStatus.IN_PROGRESS]) .prefetch_related('channel', 'project') .all() ) @@ -42,23 +40,23 @@ async def fetch_publication_post_cycle(self: 'Usecase', interval_seconds: int) - for post in posts: # Проверяем, не создана ли уже публикация для этого поста - existing = await domain.Publication.filter(post_id=post.id).first() + existing = await domain.PlacementPost.filter(post_id=post.id).first() if existing: continue - publication = domain.Publication( + placement_post = domain.PlacementPost( placement_id=placement.id, post_id=post.id, - status=domain.PublicationStatus.ACTIVE, + status=domain.PlacementPostStatus.ACTIVE, ) - await self.database.create_publication(publication) + await self.database.create_placement_post(placement_post) created_count += 1 log.info( - 'Created publication %s for placement %s from post %s', - publication.id, + 'Created placement_post %s for placement %s from post %s', + placement_post.id, placement.id, post.id, ) - log.debug('Fetch publication post cycle completed. Created %s publications', created_count) + log.debug('Fetch placement_post post cycle completed. Created %s placement_posts', created_count) diff --git a/src/usecase/placement/get_publication.py b/src/usecase/placement/get_placement_post.py similarity index 56% rename from src/usecase/placement/get_publication.py rename to src/usecase/placement/get_placement_post.py index c5738fc..46915bc 100644 --- a/src/usecase/placement/get_publication.py +++ b/src/usecase/placement/get_placement_post.py @@ -9,47 +9,47 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -async def get_publication(self: 'Usecase', input: dto.GetPublicationInput) -> dto.PublicationOutput: +async def get_placement_post(self: 'Usecase', input: dto.GetPlacementPostInput) -> dto.PlacementPostOutput: """Получить одну публикацию по ID (system-managed post)""" context = await self.ensure_workspace_permission( input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ ) - publication = await self.database.get_publication(input.workspace_id, input.publication_id) - if not publication: - log.warning('Publication %s not found for user %s', input.publication_id, input.user_id) - raise domain.PublicationNotFound(input.publication_id) + placement_post = await self.database.get_placement_post(input.workspace_id, input.placement_post_id) + if not placement_post: + log.warning('PlacementPost %s not found for user %s', input.placement_post_id, input.user_id) + raise domain.PlacementPostNotFound(input.placement_post_id) - placement = publication.placement + placement = placement_post.placement if not placement: - log.error('Publication %s missing placement data', publication.id) - raise domain.PlacementNotFound(publication.placement_id) + log.error('PlacementPost %s missing placement data', placement_post.id) + raise domain.PlacementNotFound(placement_post.placement_id) context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id) if not placement.channel or not placement.project or not placement.creative: - log.error('Publication %s placement missing related data', publication.id) + log.error('PlacementPost %s placement missing related data', placement_post.id) raise domain.PlacementNotFound(placement.id) - ad_post_url = publication.post.url if publication.post else None - subscriptions_count = await self.database.count_subscriptions_by_publication(publication.id) + ad_post_url = placement_post.post.url if placement_post.post else None + subscriptions_count = await self.database.count_subscriptions_by_placement_post(placement_post.id) - return dto.PublicationOutput( - id=publication.id, + return dto.PlacementPostOutput( + id=placement_post.id, project_id=placement.project_id, project_channel_title=placement.project.channel.title if placement.project.channel else None, placement_channel_id=placement.channel_id, placement_channel_title=placement.channel.title, creative_id=placement.creative_id, creative_name=placement.creative.name, - placement_date=placement.placement_at or publication.created_at, + placement_date=placement.placement_at or placement_post.created_at, cost=placement.cost_value, comment=placement.comment, ad_post_url=ad_post_url, invite_link_type=placement.invite_link_type, invite_link=placement.invite_link, - status=publication.status, + status=placement_post.status, subscriptions_count=subscriptions_count, placement_id=placement.id, - created_at=publication.created_at, + created_at=placement_post.created_at, ) diff --git a/src/usecase/placement/get_publications.py b/src/usecase/placement/get_placement_posts.py similarity index 61% rename from src/usecase/placement/get_publications.py rename to src/usecase/placement/get_placement_posts.py index a103729..ff06c74 100644 --- a/src/usecase/placement/get_publications.py +++ b/src/usecase/placement/get_placement_posts.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -async def get_publications(self: 'Usecase', input: dto.GetPublicationsInput) -> dto.GetPublicationsOutput: +async def get_placement_posts(self: 'Usecase', input: dto.GetPlacementPostsInput) -> dto.GetPlacementPostsOutput: """Получить список публикаций (system-managed posts)""" context = await self.ensure_workspace_permission( input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ @@ -21,7 +21,7 @@ async def get_publications(self: 'Usecase', input: dto.GetPublicationsInput) -> context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, input.project_id) allowed_project_ids = None - publications = await self.database.get_workspace_publications( + placement_posts = await self.database.get_workspace_placement_posts( input.workspace_id, project_id=input.project_id, placement_channel_id=input.placement_channel_id, @@ -30,42 +30,42 @@ async def get_publications(self: 'Usecase', input: dto.GetPublicationsInput) -> allowed_project_ids=allowed_project_ids, ) - publication_ids = [p.id for p in publications] - subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(publication_ids) + placement_post_ids = [p.id for p in placement_posts] + subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids) - publication_outputs = [] - for publication in publications: - placement = publication.placement + placement_post_outputs = [] + for placement_post in placement_posts: + placement = placement_post.placement if not placement: - log.warning('Publication %s missing placement data', publication.id) + log.warning('PlacementPost %s missing placement data', placement_post.id) continue if not placement.channel or not placement.project or not placement.creative: - log.warning('Publication %s placement missing related data', publication.id) + log.warning('PlacementPost %s placement missing related data', placement_post.id) continue - ad_post_url = publication.post.url if publication.post else None + ad_post_url = placement_post.post.url if placement_post.post else None - publication_outputs.append( - dto.PublicationOutput( - id=publication.id, + placement_post_outputs.append( + dto.PlacementPostOutput( + id=placement_post.id, project_id=placement.project_id, project_channel_title=placement.project.channel.title if placement.project.channel else None, placement_channel_id=placement.channel_id, placement_channel_title=placement.channel.title, creative_id=placement.creative_id, creative_name=placement.creative.name, - placement_date=placement.placement_at or publication.created_at, + placement_date=placement.placement_at or placement_post.created_at, cost=placement.cost_value, comment=placement.comment, ad_post_url=ad_post_url, invite_link_type=placement.invite_link_type, invite_link=placement.invite_link, - status=publication.status, - subscriptions_count=subscriptions_counts.get(publication.id, 0), + status=placement_post.status, + subscriptions_count=subscriptions_counts.get(placement_post.id, 0), placement_id=placement.id, - created_at=publication.created_at, + created_at=placement_post.created_at, ) ) - return dto.GetPublicationsOutput(publications=publication_outputs) + return dto.GetPlacementPostsOutput(placement_posts=placement_post_outputs) diff --git a/src/usecase/project/archive_project.py b/src/usecase/project/archive_project.py index 585d7e4..1b367ff 100644 --- a/src/usecase/project/archive_project.py +++ b/src/usecase/project/archive_project.py @@ -28,4 +28,3 @@ async def archive_project(self: 'Usecase', input: dto.ArchiveProjectInput) -> dt status=project.status, purchase_invite_type_default=project.purchase_invite_type_default, ) - diff --git a/src/usecase/project/delete_project.py b/src/usecase/project/delete_project.py index 91c2480..3e28fed 100644 --- a/src/usecase/project/delete_project.py +++ b/src/usecase/project/delete_project.py @@ -14,4 +14,3 @@ async def delete_project(self: 'Usecase', workspace_id: uuid.UUID, project_id: u async with self.database.transaction(): await self.database.delete_project(workspace_id, project_id) - diff --git a/src/usecase/project/update_project_invite_link_type.py b/src/usecase/project/update_project_invite_link_type.py index d2467a4..e97b3b8 100644 --- a/src/usecase/project/update_project_invite_link_type.py +++ b/src/usecase/project/update_project_invite_link_type.py @@ -14,7 +14,7 @@ async def update_project_invite_link_type( self: 'Usecase', workspace_id: uuid.UUID, project_id: uuid.UUID, - purchase_invite_type_default: domain.purchase.InviteLinkType, + purchase_invite_type_default: domain.InviteLinkType, user_id: uuid.UUID, ) -> dto.ProjectOutput: await self.ensure_workspace_permission( diff --git a/src/usecase/purchase/create_placements.py b/src/usecase/purchase/create_placements.py index 7a2e881..7703316 100644 --- a/src/usecase/purchase/create_placements.py +++ b/src/usecase/purchase/create_placements.py @@ -117,19 +117,15 @@ async def create_placements( status=channel_input.status or domain.PlacementStatus.PLANNED, comment=channel_input.comment, # Приоритет: channel details > общие details - placement_at=(channel_details.placement_at if channel_details else None) or ( - details.placement_at if details else None - ), + placement_at=(channel_details.placement_at if channel_details else None) + or (details.placement_at if details else None), payment_at=details.payment_at if details else None, - cost_type=(channel_details.cost.type if channel_details and channel_details.cost else None) or ( - details.cost.type if details and details.cost else None - ), - cost_value=(channel_details.cost.value if channel_details and channel_details.cost else None) or ( - details.cost.value if details and details.cost else None - ), - cost_before_bargain=(channel_details.cost_before_bargain if channel_details else None) or ( - details.cost_before_bargain if details else None - ), + cost_type=(channel_details.cost.type if channel_details and channel_details.cost else None) + or (details.cost.type if details and details.cost else None), + cost_value=(channel_details.cost.value if channel_details and channel_details.cost else None) + or (details.cost.value if details and details.cost else None), + cost_before_bargain=(channel_details.cost_before_bargain if channel_details else None) + or (details.cost_before_bargain if details else None), placement_type=details.placement_type if details else None, format=(channel_details.format if channel_details else None) or (details.format if details else None), ) diff --git a/src/usecase/subscription/handle_subscription.py b/src/usecase/subscription/handle_subscription.py index 6465e96..f8ccd54 100644 --- a/src/usecase/subscription/handle_subscription.py +++ b/src/usecase/subscription/handle_subscription.py @@ -17,13 +17,13 @@ async def handle_subscription( first_name: str | None = None, last_name: str | None = None, ) -> None: - publication = await self.database.get_publication_by_invite_link(invite_link) - if not publication: - log.warning('Publication not found for invite_link: %s', invite_link) + placement_post = await self.database.get_placement_post_by_invite_link(invite_link) + if not placement_post: + log.warning('PlacementPost not found for invite_link: %s', invite_link) return - if not publication.placement: - log.error('Publication %s has no placement', publication.id) + if not placement_post.placement: + log.error('PlacementPost %s has no placement', placement_post.id) return subscriber = await self.database.get_telegram_user(telegram_id=user_telegram_id) @@ -42,7 +42,7 @@ async def handle_subscription( await self.database.update_telegram_user(subscriber) active_subscription = await self.database.get_active_subscription_by_subscriber_and_project( - subscriber.id, publication.placement.project_id + subscriber.id, placement_post.placement.project_id ) if active_subscription: @@ -50,18 +50,20 @@ async def handle_subscription( # Это не должно случиться (Telegram не даст подписаться дважды), # но если случилось - логируем и игнорируем log.warning( - 'User %s (telegram_id: %s) already has active subscription to channel %s via publication %s, ' - 'ignoring new subscription attempt via publication %s', + 'User %s (telegram_id: %s) already has active subscription to channel %s via placement_post %s, ' + 'ignoring new subscription attempt via placement_post %s', subscriber.id, user_telegram_id, - publication.placement.project_id, - active_subscription.publication_id, - publication.id, + placement_post.placement.project_id, + active_subscription.placement_post_id, + placement_post.id, ) return # Проверяем, была ли раньше подписка через ЭТУ публикацию (для реактивации) - existing_sub = await self.database.get_subscription_by_subscriber_and_publication(subscriber.id, publication.id) + existing_sub = await self.database.get_subscription_by_subscriber_and_placement_post( + subscriber.id, placement_post.id + ) if existing_sub and existing_sub.status == domain.SubscriptionStatus.UNSUBSCRIBED: existing_sub.status = domain.SubscriptionStatus.ACTIVE @@ -69,24 +71,24 @@ async def handle_subscription( await self.database.update_subscription(existing_sub) log.info( - 'Subscription reactivated: subscriber %s (telegram_id: %s) resubscribed via same publication %s', + 'Subscription reactivated: subscriber %s (telegram_id: %s) resubscribed via same placement_post %s', subscriber.id, user_telegram_id, - publication.id, + placement_post.id, ) return subscription = domain.Subscription( - publication_id=publication.id, + placement_post_id=placement_post.id, telegram_user_id=subscriber.id, invite_link=invite_link, ) await self.database.create_subscription(subscription) log.info( - 'Subscription created: subscriber %s (telegram_id: %s) subscribed via publication %s (invite_link: %s)', + 'Subscription created: subscriber %s (telegram_id: %s) subscribed via placement_post %s (invite_link: %s)', subscriber.id, user_telegram_id, - publication.id, + placement_post.id, invite_link, ) diff --git a/src/usecase/subscription/handle_unsubscription.py b/src/usecase/subscription/handle_unsubscription.py index a213db2..8f50ab6 100644 --- a/src/usecase/subscription/handle_unsubscription.py +++ b/src/usecase/subscription/handle_unsubscription.py @@ -39,8 +39,8 @@ async def handle_unsubscription(self: 'Usecase', user_telegram_id: int, channel_ await self.database.update_subscription(subscription) log.info( - 'Subscription marked as unsubscribed: subscriber %s (telegram_id: %s) unsubscribed from publication %s', + 'Subscription marked as unsubscribed: subscriber %s (telegram_id: %s) unsubscribed from placement_post %s', subscriber.id, user_telegram_id, - subscription.publication_id, + subscription.placement_post_id, ) diff --git a/src/usecase/views/get_views_history.py b/src/usecase/views/get_views_history.py index cb08cca..164a8c9 100644 --- a/src/usecase/views/get_views_history.py +++ b/src/usecase/views/get_views_history.py @@ -14,22 +14,22 @@ async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) -> input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ ) - publication = await self.database.get_publication(input.workspace_id, input.publication_id) - if not publication: - log.warning('Publication %s not found for user %s', input.publication_id, input.user_id) - raise domain.PublicationNotFound(input.publication_id) + placement_post = await self.database.get_placement_post(input.workspace_id, input.placement_post_id) + if not placement_post: + log.warning('PlacementPost %s not found for user %s', input.placement_post_id, input.user_id) + raise domain.PlacementPostNotFound(input.placement_post_id) - if not publication.placement: - log.error('Publication %s has no placement', publication.id) - raise domain.PlacementNotFound(publication.placement_id) + if not placement_post.placement: + log.error('PlacementPost %s has no placement', placement_post.id) + raise domain.PlacementNotFound(placement_post.placement_id) - context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, publication.placement.project_id) + context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement_post.placement.project_id) - if not publication.post: + if not placement_post.post: return [] histories = await self.database.get_views_history( - publication.post.id, + placement_post.post.id, from_date=input.from_date, to_date=input.to_date, ) diff --git a/src/usecase/workspace/accept_workspace_invite.py b/src/usecase/workspace/accept_workspace_invite.py index 62bd1fe..8f29475 100644 --- a/src/usecase/workspace/accept_workspace_invite.py +++ b/src/usecase/workspace/accept_workspace_invite.py @@ -33,4 +33,4 @@ async def accept_workspace_invite( else: await self.database.add_user_to_workspace(invite.workspace_id, user_id) - return dto.WorkspaceInviteOutput.from_domain(invite) \ No newline at end of file + return dto.WorkspaceInviteOutput.from_domain(invite)