From e4199fc9c9bda26269093a98b0afe3607abc7633 Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Wed, 7 Jan 2026 15:11:28 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/models/0_20260102162851_init.py | 335 ----------------- migrations/models/0_20260107150827_init.py | 342 ++++++++++++++++++ .../1_20260102184542_add_check_constraint.py | 99 ----- .../1_20260107150903_add_check_constraint.py | 107 ++++++ migrations/models/2_20260103113235_update.py | 95 ----- ...0260106175850_move_creative_media_to_s3.py | 90 ----- migrations/models/4_20260106192017_update.py | 149 -------- migrations/models/5_20260107135803_update.py | 151 -------- src/adapter/postgres.py | 43 ++- src/domain/__init__.py | 2 + src/domain/creative.py | 5 - src/domain/placement.py | 23 +- src/domain/purchase.py | 7 +- src/dto/placement.py | 4 +- src/usecase/__init__.py | 8 +- .../analytics/get_channel_analytics.py | 10 +- .../analytics/get_overview_analytics.py | 16 +- .../analytics/get_placements_analytics.py | 4 +- .../channel/attach_channel_to_workspace.py | 1 + src/usecase/creative/create_creative.py | 1 - src/usecase/placement/create_placement.py | 84 ----- src/usecase/placement/delete_placement.py | 24 -- .../placement/fetch_placement_post_cycle.py | 4 - src/usecase/placement/get_placement.py | 14 +- src/usecase/placement/get_placements.py | 17 +- src/usecase/placement/update_placement.py | 61 ---- src/usecase/purchase/create_purchase.py | 1 - telegram_bot/screens/purchase_details.go | 155 ++++++-- .../screens/purchase_optional_details.go | 22 +- .../screens/select_channels_for_purchase.go | 23 +- 30 files changed, 696 insertions(+), 1201 deletions(-) delete mode 100644 migrations/models/0_20260102162851_init.py create mode 100644 migrations/models/0_20260107150827_init.py delete mode 100644 migrations/models/1_20260102184542_add_check_constraint.py create mode 100644 migrations/models/1_20260107150903_add_check_constraint.py delete mode 100644 migrations/models/2_20260103113235_update.py delete mode 100644 migrations/models/3_20260106175850_move_creative_media_to_s3.py delete mode 100644 migrations/models/4_20260106192017_update.py delete mode 100644 migrations/models/5_20260107135803_update.py delete mode 100644 src/usecase/placement/create_placement.py delete mode 100644 src/usecase/placement/delete_placement.py delete mode 100644 src/usecase/placement/update_placement.py diff --git a/migrations/models/0_20260102162851_init.py b/migrations/models/0_20260102162851_init.py deleted file mode 100644 index 0cc8634..0000000 --- a/migrations/models/0_20260102162851_init.py +++ /dev/null @@ -1,335 +0,0 @@ -from tortoise import BaseDBAsyncClient - -RUN_IN_TRANSACTION = True - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - CREATE TABLE IF NOT EXISTS "channel" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "telegram_id" BIGINT NOT NULL UNIQUE, - "title" VARCHAR(255) NOT NULL, - "username" VARCHAR(255) NOT NULL UNIQUE, - "access_hash" BIGINT, - "pts" INT - ); - CREATE INDEX IF NOT EXISTS "idx_channel_telegra_414c3b" ON "channel" ("telegram_id"); - CREATE INDEX IF NOT EXISTS "idx_channel_usernam_7fd87a" ON "channel" ("username"); - CREATE TABLE IF NOT EXISTS "post" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "message_id" INT NOT NULL, - "text" TEXT NOT NULL, - "deleted_from_channel_at" TIMESTAMPTZ, - "channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE, - CONSTRAINT "uid_post_channel_0a2cba" UNIQUE ("channel_id", "message_id") - ); - CREATE INDEX IF NOT EXISTS "idx_post_channel_197bed" ON "post" ("channel_id"); - CREATE TABLE IF NOT EXISTS "post_views_history" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "views_count" INT NOT NULL, - "fetched_at" TIMESTAMPTZ NOT NULL, - "post_id" UUID NOT NULL REFERENCES "post" ("id") ON DELETE CASCADE, - CONSTRAINT "uid_post_views__post_id_c6cc9f" UNIQUE ("post_id", "fetched_at") - ); - CREATE INDEX IF NOT EXISTS "idx_post_views__fetched_103eb5" ON "post_views_history" ("fetched_at"); - CREATE INDEX IF NOT EXISTS "idx_post_views__post_id_dfb1ca" ON "post_views_history" ("post_id"); - CREATE TABLE IF NOT EXISTS "user" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "telegram_id" BIGINT NOT NULL UNIQUE, - "username" VARCHAR(255), - "first_name" VARCHAR(255), - "last_name" VARCHAR(255) - ); - CREATE INDEX IF NOT EXISTS "idx_user_telegra_66ffbd" ON "user" ("telegram_id"); - CREATE TABLE IF NOT EXISTS "login_token" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "token" VARCHAR(255) NOT NULL UNIQUE, - "expires_at" TIMESTAMPTZ NOT NULL, - "used_at" TIMESTAMPTZ, - "user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE - ); - CREATE INDEX IF NOT EXISTS "idx_login_token_token_2582d2" ON "login_token" ("token"); - CREATE INDEX IF NOT EXISTS "idx_login_token_user_id_c85d7c" ON "login_token" ("user_id"); - CREATE TABLE IF NOT EXISTS "workspace" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "name" VARCHAR(255) NOT NULL - ); - CREATE TABLE IF NOT EXISTS "project" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "status" VARCHAR(8) NOT NULL DEFAULT 'active', - "purchase_invite_type_default" VARCHAR(10) NOT NULL DEFAULT 'approval', - "channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE, - "workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE, - CONSTRAINT "uid_project_workspa_2f80f2" UNIQUE ("workspace_id", "channel_id") - ); - CREATE INDEX IF NOT EXISTS "idx_project_channel_ce2466" ON "project" ("channel_id"); - CREATE INDEX IF NOT EXISTS "idx_project_workspa_2354fd" ON "project" ("workspace_id"); - COMMENT ON COLUMN "project"."status" IS 'ACTIVE: active\nINACTIVE: inactive\nARCHIVED: archived'; - COMMENT ON COLUMN "project"."purchase_invite_type_default" IS 'PUBLIC: public\nAPPROVAL: approval'; - CREATE TABLE IF NOT EXISTS "creative" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "name" VARCHAR(255) NOT NULL, - "text" TEXT NOT NULL, - "status" VARCHAR(8) NOT NULL DEFAULT 'active', - "project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE, - "workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE - ); - CREATE INDEX IF NOT EXISTS "idx_creative_project_0766a6" ON "creative" ("project_id"); - CREATE INDEX IF NOT EXISTS "idx_creative_workspa_2de546" ON "creative" ("workspace_id"); - COMMENT ON COLUMN "creative"."status" IS 'ACTIVE: active\nARCHIVED: archived'; - CREATE TABLE IF NOT EXISTS "purchase" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "status" VARCHAR(8) NOT NULL DEFAULT 'active', - "creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE, - "project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE, - "workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE - ); - CREATE INDEX IF NOT EXISTS "idx_purchase_creativ_6829cd" ON "purchase" ("creative_id"); - CREATE INDEX IF NOT EXISTS "idx_purchase_project_95b1c2" ON "purchase" ("project_id"); - CREATE INDEX IF NOT EXISTS "idx_purchase_workspa_8784a5" ON "purchase" ("workspace_id"); - CREATE INDEX IF NOT EXISTS "idx_purchase_project_dee224" ON "purchase" ("project_id", "status"); - COMMENT ON COLUMN "purchase"."status" IS 'ACTIVE: active\nARCHIVED: archived'; - CREATE TABLE IF NOT EXISTS "purchase_channel" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "status" VARCHAR(11) NOT NULL DEFAULT 'planned', - "planned_cost" DOUBLE PRECISION, - "comment" TEXT, - "invite_link" VARCHAR(512) NOT NULL, - "invite_link_type" VARCHAR(8) NOT NULL, - "channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE, - "purchase_id" UUID NOT NULL REFERENCES "purchase" ("id") ON DELETE CASCADE, - CONSTRAINT "uid_purchase_ch_purchas_f7c86e" UNIQUE ("purchase_id", "channel_id") - ); - CREATE INDEX IF NOT EXISTS "idx_purchase_ch_channel_2b1264" ON "purchase_channel" ("channel_id"); - CREATE INDEX IF NOT EXISTS "idx_purchase_ch_purchas_1fe84a" ON "purchase_channel" ("purchase_id"); - COMMENT ON COLUMN "purchase_channel"."status" IS 'PLANNED: planned\nAPPROVED: approved\nREJECTED: rejected\nIN_PROGRESS: in_progress\nCOMPLETED: completed'; - COMMENT ON COLUMN "purchase_channel"."invite_link_type" IS 'PUBLIC: public\nAPPROVAL: approval'; - CREATE TABLE IF NOT EXISTS "placement" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "wanted_placement_date" TIMESTAMPTZ NOT NULL, - "cost" DOUBLE PRECISION, - "comment" TEXT, - "invite_link" VARCHAR(512) NOT NULL, - "status" VARCHAR(8) NOT NULL DEFAULT 'active', - "creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE, - "placement_channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE, - "post_id" UUID REFERENCES "post" ("id") ON DELETE SET NULL, - "project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE, - "purchase_id" UUID REFERENCES "purchase" ("id") ON DELETE CASCADE, - "purchase_channel_id" UUID REFERENCES "purchase_channel" ("id") ON DELETE CASCADE, - "workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE - ); - CREATE INDEX IF NOT EXISTS "idx_placement_creativ_32111d" ON "placement" ("creative_id"); - CREATE INDEX IF NOT EXISTS "idx_placement_placeme_20cf53" ON "placement" ("placement_channel_id"); - CREATE INDEX IF NOT EXISTS "idx_placement_post_id_e4a16e" ON "placement" ("post_id"); - CREATE INDEX IF NOT EXISTS "idx_placement_project_2c6aa4" ON "placement" ("project_id"); - CREATE INDEX IF NOT EXISTS "idx_placement_purchas_3e3775" ON "placement" ("purchase_id"); - CREATE INDEX IF NOT EXISTS "idx_placement_purchas_61f9e6" ON "placement" ("purchase_channel_id"); - CREATE INDEX IF NOT EXISTS "idx_placement_workspa_632b20" ON "placement" ("workspace_id"); - COMMENT ON COLUMN "placement"."status" IS 'ACTIVE: active\nARCHIVED: archived'; - CREATE TABLE IF NOT EXISTS "subscription" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "invite_link" VARCHAR(512) NOT NULL, - "status" VARCHAR(12) NOT NULL DEFAULT 'active', - "unsubscribed_at" TIMESTAMPTZ, - "placement_id" UUID NOT NULL REFERENCES "placement" ("id") ON DELETE CASCADE, - "subscriber_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE, - CONSTRAINT "uid_subscriptio_placeme_8fcb8f" UNIQUE ("placement_id", "subscriber_id") - ); - CREATE INDEX IF NOT EXISTS "idx_subscriptio_invite__f2058d" ON "subscription" ("invite_link"); - CREATE INDEX IF NOT EXISTS "idx_subscriptio_placeme_5decb2" ON "subscription" ("placement_id"); - CREATE INDEX IF NOT EXISTS "idx_subscriptio_subscri_e75729" ON "subscription" ("subscriber_id"); - COMMENT ON COLUMN "subscription"."status" IS 'ACTIVE: active\nUNSUBSCRIBED: unsubscribed'; - CREATE TABLE IF NOT EXISTS "workspace_invite" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "status" VARCHAR(8) NOT NULL DEFAULT 'pending', - "invited_by_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE, - "user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE, - "workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE, - CONSTRAINT "uid_workspace_i_workspa_abd9c4" UNIQUE ("workspace_id", "user_id") - ); - CREATE INDEX IF NOT EXISTS "idx_workspace_i_invited_3ed9f8" ON "workspace_invite" ("invited_by_id"); - CREATE INDEX IF NOT EXISTS "idx_workspace_i_user_id_a96e1e" ON "workspace_invite" ("user_id"); - CREATE INDEX IF NOT EXISTS "idx_workspace_i_workspa_71e9d2" ON "workspace_invite" ("workspace_id"); - COMMENT ON COLUMN "workspace_invite"."status" IS 'PENDING: pending\nACCEPTED: accepted\nREVOKED: revoked'; - CREATE TABLE IF NOT EXISTS "workspace_user" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "status" VARCHAR(7) NOT NULL DEFAULT 'active', - "user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE, - "workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE, - CONSTRAINT "uid_workspace_u_workspa_13c46c" UNIQUE ("workspace_id", "user_id") - ); - CREATE INDEX IF NOT EXISTS "idx_workspace_u_user_id_246318" ON "workspace_user" ("user_id"); - CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_a403b0" ON "workspace_user" ("workspace_id"); - COMMENT ON COLUMN "workspace_user"."status" IS 'ACTIVE: active\nINVITED: invited\nBLOCKED: blocked'; - CREATE TABLE IF NOT EXISTS "workspace_user_permission" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "permission" VARCHAR(24) NOT NULL, - "workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE, - CONSTRAINT "uid_workspace_u_workspa_86f574" UNIQUE ("workspace_user_id", "permission") - ); - CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_b9b7b0" ON "workspace_user_permission" ("workspace_user_id"); - COMMENT ON COLUMN "workspace_user_permission"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write'; - CREATE TABLE IF NOT EXISTS "workspace_user_permission_scope" - ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "permission" VARCHAR(24) NOT NULL, - "channel_id" UUID REFERENCES "channel" ("id") ON DELETE CASCADE, - "creative_id" UUID REFERENCES "creative" ("id") ON DELETE CASCADE, - "placement_id" UUID REFERENCES "placement" ("id") ON DELETE CASCADE, - "project_id" UUID REFERENCES "project" ("id") ON DELETE CASCADE, - "workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE - ); - CREATE INDEX IF NOT EXISTS "idx_workspace_u_channel_d94b48" ON "workspace_user_permission_scope" ("channel_id"); - CREATE INDEX IF NOT EXISTS "idx_workspace_u_creativ_8da562" ON "workspace_user_permission_scope" ("creative_id"); - CREATE INDEX IF NOT EXISTS "idx_workspace_u_placeme_ea1846" ON "workspace_user_permission_scope" ("placement_id"); - CREATE INDEX IF NOT EXISTS "idx_workspace_u_project_bd7fea" ON "workspace_user_permission_scope" ("project_id"); - CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_82a35a" ON "workspace_user_permission_scope" ("workspace_user_id"); - COMMENT ON COLUMN "workspace_user_permission_scope"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write'; - CREATE TABLE IF NOT EXISTS "aerich" - ( - "id" SERIAL NOT NULL PRIMARY KEY, - "version" VARCHAR(255) NOT NULL, - "app" VARCHAR(100) NOT NULL, - "content" JSONB NOT NULL - );""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - """ - - -MODELS_STATE = ( - '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' -) diff --git a/migrations/models/0_20260107150827_init.py b/migrations/models/0_20260107150827_init.py new file mode 100644 index 0000000..8acef54 --- /dev/null +++ b/migrations/models/0_20260107150827_init.py @@ -0,0 +1,342 @@ +from tortoise import BaseDBAsyncClient + +RUN_IN_TRANSACTION = True + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + CREATE TABLE IF NOT EXISTS "channel" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "telegram_id" BIGINT NOT NULL UNIQUE, + "title" VARCHAR(255) NOT NULL, + "username" VARCHAR(255) NOT NULL UNIQUE, + "access_hash" BIGINT, + "pts" INT +); +CREATE INDEX IF NOT EXISTS "idx_channel_telegra_414c3b" ON "channel" ("telegram_id"); +CREATE INDEX IF NOT EXISTS "idx_channel_usernam_7fd87a" ON "channel" ("username"); +CREATE TABLE IF NOT EXISTS "post" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "message_id" INT NOT NULL, + "text" TEXT NOT NULL, + "deleted_from_channel_at" TIMESTAMPTZ, + "channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE, + CONSTRAINT "uid_post_channel_0a2cba" UNIQUE ("channel_id", "message_id") +); +CREATE INDEX IF NOT EXISTS "idx_post_channel_197bed" ON "post" ("channel_id"); +CREATE TABLE IF NOT EXISTS "post_views_history" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "views_count" INT NOT NULL, + "fetched_at" TIMESTAMPTZ NOT NULL, + "post_id" UUID NOT NULL REFERENCES "post" ("id") ON DELETE CASCADE, + CONSTRAINT "uid_post_views__post_id_c6cc9f" UNIQUE ("post_id", "fetched_at") +); +CREATE INDEX IF NOT EXISTS "idx_post_views__fetched_103eb5" ON "post_views_history" ("fetched_at"); +CREATE INDEX IF NOT EXISTS "idx_post_views__post_id_dfb1ca" ON "post_views_history" ("post_id"); +CREATE TABLE IF NOT EXISTS "telegram_user" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "telegram_id" BIGINT NOT NULL UNIQUE, + "username" VARCHAR(255), + "first_name" VARCHAR(255), + "last_name" VARCHAR(255) +); +CREATE INDEX IF NOT EXISTS "idx_telegram_us_telegra_3a3100" ON "telegram_user" ("telegram_id"); +CREATE TABLE IF NOT EXISTS "user" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "telegram_user_id" UUID NOT NULL UNIQUE REFERENCES "telegram_user" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_user_telegra_444a03" ON "user" ("telegram_user_id"); +CREATE TABLE IF NOT EXISTS "login_token" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "token" VARCHAR(255) NOT NULL UNIQUE, + "expires_at" TIMESTAMPTZ NOT NULL, + "used_at" TIMESTAMPTZ, + "user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_login_token_token_2582d2" ON "login_token" ("token"); +CREATE INDEX IF NOT EXISTS "idx_login_token_user_id_c85d7c" ON "login_token" ("user_id"); +CREATE TABLE IF NOT EXISTS "workspace" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "name" VARCHAR(255) NOT NULL +); +CREATE TABLE IF NOT EXISTS "project" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "status" VARCHAR(8) NOT NULL DEFAULT 'active', + "purchase_invite_type_default" VARCHAR(10) NOT NULL DEFAULT 'approval', + "channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE, + "workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE, + CONSTRAINT "uid_project_workspa_2f80f2" UNIQUE ("workspace_id", "channel_id") +); +CREATE INDEX IF NOT EXISTS "idx_project_channel_ce2466" ON "project" ("channel_id"); +CREATE INDEX IF NOT EXISTS "idx_project_workspa_2354fd" ON "project" ("workspace_id"); +COMMENT ON COLUMN "project"."status" IS 'ACTIVE: active\nINACTIVE: inactive\nARCHIVED: archived'; +COMMENT ON COLUMN "project"."purchase_invite_type_default" IS 'PUBLIC: public\nAPPROVAL: approval'; +CREATE TABLE IF NOT EXISTS "creative" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "name" VARCHAR(255) NOT NULL, + "text" TEXT NOT NULL, + "media_type" VARCHAR(32), + "media_file_id" VARCHAR(512), + "media_s3_key" VARCHAR(512), + "buttons" JSONB NOT NULL, + "status" VARCHAR(8) NOT NULL DEFAULT 'active', + "project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_creative_project_0766a6" ON "creative" ("project_id"); +COMMENT ON COLUMN "creative"."status" IS 'ACTIVE: active\nARCHIVED: archived'; +CREATE TABLE IF NOT EXISTS "purchase" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "status" VARCHAR(8) NOT NULL DEFAULT 'active', + "placement_at" TIMESTAMPTZ, + "payment_at" TIMESTAMPTZ, + "cost_type" VARCHAR(8), + "cost_value" DOUBLE PRECISION, + "cost_before_bargain" DOUBLE PRECISION, + "purchase_type" VARCHAR(16), + "format" TEXT, + "comment" TEXT, + "creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE, + "project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_purchase_creativ_6829cd" ON "purchase" ("creative_id"); +CREATE INDEX IF NOT EXISTS "idx_purchase_project_95b1c2" ON "purchase" ("project_id"); +CREATE INDEX IF NOT EXISTS "idx_purchase_project_dee224" ON "purchase" ("project_id", "status"); +COMMENT ON COLUMN "purchase"."status" IS 'ACTIVE: active\nARCHIVED: archived'; +COMMENT ON COLUMN "purchase"."cost_type" IS 'FIXED: fixed\nCPM: cpm'; +COMMENT ON COLUMN "purchase"."purchase_type" IS 'SELF_PROMO: self_promo\nSTANDARD: standard'; +CREATE TABLE IF NOT EXISTS "purchase_channel" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "status" VARCHAR(11) NOT NULL DEFAULT 'planned', + "placement_at" TIMESTAMPTZ, + "cost_type" VARCHAR(8), + "cost_value" DOUBLE PRECISION, + "cost_before_bargain" DOUBLE PRECISION, + "format" TEXT, + "comment" TEXT, + "invite_link" VARCHAR(512) NOT NULL, + "invite_link_type" VARCHAR(8) NOT NULL, + "channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE, + "purchase_id" UUID NOT NULL REFERENCES "purchase" ("id") ON DELETE CASCADE, + CONSTRAINT "uid_purchase_ch_purchas_f7c86e" UNIQUE ("purchase_id", "channel_id") +); +CREATE INDEX IF NOT EXISTS "idx_purchase_ch_channel_2b1264" ON "purchase_channel" ("channel_id"); +CREATE INDEX IF NOT EXISTS "idx_purchase_ch_purchas_1fe84a" ON "purchase_channel" ("purchase_id"); +COMMENT ON COLUMN "purchase_channel"."status" IS 'PLANNED: planned\nAPPROVED: approved\nREJECTED: rejected\nIN_PROGRESS: in_progress\nCOMPLETED: completed'; +COMMENT ON COLUMN "purchase_channel"."cost_type" IS 'FIXED: fixed\nCPM: cpm'; +COMMENT ON COLUMN "purchase_channel"."invite_link_type" IS 'PUBLIC: public\nAPPROVAL: approval'; +CREATE TABLE IF NOT EXISTS "placement" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "wanted_placement_date" TIMESTAMPTZ NOT NULL, + "cost" DOUBLE PRECISION, + "comment" TEXT, + "status" VARCHAR(8) NOT NULL DEFAULT 'active', + "creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE, + "post_id" UUID REFERENCES "post" ("id") ON DELETE SET NULL, + "project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE, + "purchase_channel_id" UUID NOT NULL REFERENCES "purchase_channel" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_placement_creativ_32111d" ON "placement" ("creative_id"); +CREATE INDEX IF NOT EXISTS "idx_placement_post_id_e4a16e" ON "placement" ("post_id"); +CREATE INDEX IF NOT EXISTS "idx_placement_project_2c6aa4" ON "placement" ("project_id"); +CREATE INDEX IF NOT EXISTS "idx_placement_purchas_61f9e6" ON "placement" ("purchase_channel_id"); +COMMENT ON COLUMN "placement"."status" IS 'ACTIVE: active\nARCHIVED: archived'; +CREATE TABLE IF NOT EXISTS "subscription" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "invite_link" VARCHAR(512) NOT NULL, + "status" VARCHAR(12) NOT NULL DEFAULT 'active', + "unsubscribed_at" TIMESTAMPTZ, + "placement_id" UUID NOT NULL REFERENCES "placement" ("id") ON DELETE CASCADE, + "telegram_user_id" UUID NOT NULL REFERENCES "telegram_user" ("id") ON DELETE CASCADE, + CONSTRAINT "uid_subscriptio_placeme_75b40b" UNIQUE ("placement_id", "telegram_user_id") +); +CREATE INDEX IF NOT EXISTS "idx_subscriptio_invite__f2058d" ON "subscription" ("invite_link"); +CREATE INDEX IF NOT EXISTS "idx_subscriptio_placeme_5decb2" ON "subscription" ("placement_id"); +CREATE INDEX IF NOT EXISTS "idx_subscriptio_telegra_8be9bb" ON "subscription" ("telegram_user_id"); +COMMENT ON COLUMN "subscription"."status" IS 'ACTIVE: active\nUNSUBSCRIBED: unsubscribed'; +CREATE TABLE IF NOT EXISTS "workspace_invite" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "status" VARCHAR(8) NOT NULL DEFAULT 'pending', + "invited_by_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE, + "user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE, + "workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE, + CONSTRAINT "uid_workspace_i_workspa_abd9c4" UNIQUE ("workspace_id", "user_id") +); +CREATE INDEX IF NOT EXISTS "idx_workspace_i_invited_3ed9f8" ON "workspace_invite" ("invited_by_id"); +CREATE INDEX IF NOT EXISTS "idx_workspace_i_user_id_a96e1e" ON "workspace_invite" ("user_id"); +CREATE INDEX IF NOT EXISTS "idx_workspace_i_workspa_71e9d2" ON "workspace_invite" ("workspace_id"); +COMMENT ON COLUMN "workspace_invite"."status" IS 'PENDING: pending\nACCEPTED: accepted\nREVOKED: revoked'; +CREATE TABLE IF NOT EXISTS "workspace_user" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "status" VARCHAR(7) NOT NULL DEFAULT 'active', + "user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE, + "workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE, + CONSTRAINT "uid_workspace_u_workspa_13c46c" UNIQUE ("workspace_id", "user_id") +); +CREATE INDEX IF NOT EXISTS "idx_workspace_u_user_id_246318" ON "workspace_user" ("user_id"); +CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_a403b0" ON "workspace_user" ("workspace_id"); +COMMENT ON COLUMN "workspace_user"."status" IS 'ACTIVE: active\nINVITED: invited\nBLOCKED: blocked'; +CREATE TABLE IF NOT EXISTS "workspace_user_permission" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "permission" VARCHAR(24) NOT NULL, + "workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE, + CONSTRAINT "uid_workspace_u_workspa_86f574" UNIQUE ("workspace_user_id", "permission") +); +CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_b9b7b0" ON "workspace_user_permission" ("workspace_user_id"); +COMMENT ON COLUMN "workspace_user_permission"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write'; +CREATE TABLE IF NOT EXISTS "workspace_user_permission_scope" ( + "id" UUID NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ, + "permission" VARCHAR(24) NOT NULL, + "channel_id" UUID REFERENCES "channel" ("id") ON DELETE CASCADE, + "creative_id" UUID REFERENCES "creative" ("id") ON DELETE CASCADE, + "placement_id" UUID REFERENCES "placement" ("id") ON DELETE CASCADE, + "project_id" UUID REFERENCES "project" ("id") ON DELETE CASCADE, + "workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_workspace_u_channel_d94b48" ON "workspace_user_permission_scope" ("channel_id"); +CREATE INDEX IF NOT EXISTS "idx_workspace_u_creativ_8da562" ON "workspace_user_permission_scope" ("creative_id"); +CREATE INDEX IF NOT EXISTS "idx_workspace_u_placeme_ea1846" ON "workspace_user_permission_scope" ("placement_id"); +CREATE INDEX IF NOT EXISTS "idx_workspace_u_project_bd7fea" ON "workspace_user_permission_scope" ("project_id"); +CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_82a35a" ON "workspace_user_permission_scope" ("workspace_user_id"); +COMMENT ON COLUMN "workspace_user_permission_scope"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write'; +CREATE TABLE IF NOT EXISTS "aerich" ( + "id" SERIAL NOT NULL PRIMARY KEY, + "version" VARCHAR(255) NOT NULL, + "app" VARCHAR(100) NOT NULL, + "content" JSONB NOT NULL +);""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + """ + + +MODELS_STATE = ( + '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' + 'v0' +) diff --git a/migrations/models/1_20260102184542_add_check_constraint.py b/migrations/models/1_20260102184542_add_check_constraint.py deleted file mode 100644 index f26ab76..0000000 --- a/migrations/models/1_20260102184542_add_check_constraint.py +++ /dev/null @@ -1,99 +0,0 @@ -from tortoise import BaseDBAsyncClient - -RUN_IN_TRANSACTION = True - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - -- Add CHECK constraint: exactly one FK must be set - ALTER TABLE "workspace_user_permission_scope" - ADD CONSTRAINT "check_exactly_one_scope" - CHECK ( - (("project_id" IS NOT NULL)::int + - ("creative_id" IS NOT NULL)::int + - ("placement_id" IS NOT NULL)::int + - ("channel_id" IS NOT NULL)::int) = 1 - ); - - COMMENT ON TABLE "workspace_user_permission_scope" - IS 'Permission scoped to specific entity (project/creative/placement/channel)'; \ - """ - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "workspace_user_permission_scope" - DROP CONSTRAINT IF EXISTS "check_exactly_one_scope"; \ - """ - - -MODELS_STATE = ( - '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' -) diff --git a/migrations/models/1_20260107150903_add_check_constraint.py b/migrations/models/1_20260107150903_add_check_constraint.py new file mode 100644 index 0000000..0cb2d87 --- /dev/null +++ b/migrations/models/1_20260107150903_add_check_constraint.py @@ -0,0 +1,107 @@ +from tortoise import BaseDBAsyncClient + +RUN_IN_TRANSACTION = True + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + -- Add CHECK constraint: exactly one FK must be set + ALTER TABLE "workspace_user_permission_scope" + ADD CONSTRAINT "check_exactly_one_scope" + CHECK ( + (("project_id" IS NOT NULL)::int + + ("creative_id" IS NOT NULL)::int + + ("placement_id" IS NOT NULL)::int + + ("channel_id" IS NOT NULL)::int) = 1 + ); + + COMMENT ON TABLE "workspace_user_permission_scope" + IS 'Permission scoped to specific entity (project/creative/placement/channel)'; \ + """ + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE "workspace_user_permission_scope" + DROP CONSTRAINT IF EXISTS "check_exactly_one_scope"; \ + """ + + +MODELS_STATE = ( + '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' + 'v0' +) diff --git a/migrations/models/2_20260103113235_update.py b/migrations/models/2_20260103113235_update.py deleted file mode 100644 index 2bbf382..0000000 --- a/migrations/models/2_20260103113235_update.py +++ /dev/null @@ -1,95 +0,0 @@ -from tortoise import BaseDBAsyncClient - -RUN_IN_TRANSACTION = True - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "creative" ADD "buttons" JSONB NOT NULL; - ALTER TABLE "creative" ADD "media_data" BYTEA; - ALTER TABLE "creative" ADD "media_file_id" VARCHAR(512); - ALTER TABLE "creative" ADD "media_type" VARCHAR(32);""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "creative" DROP COLUMN "buttons"; - ALTER TABLE "creative" DROP COLUMN "media_data"; - ALTER TABLE "creative" DROP COLUMN "media_file_id"; - ALTER TABLE "creative" DROP COLUMN "media_type";""" - - -MODELS_STATE = ( - "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" - "a4" -) diff --git a/migrations/models/3_20260106175850_move_creative_media_to_s3.py b/migrations/models/3_20260106175850_move_creative_media_to_s3.py deleted file mode 100644 index 3eb8a08..0000000 --- a/migrations/models/3_20260106175850_move_creative_media_to_s3.py +++ /dev/null @@ -1,90 +0,0 @@ -from tortoise import BaseDBAsyncClient - -RUN_IN_TRANSACTION = True - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "creative" ADD "media_s3_key" VARCHAR(512); - ALTER TABLE "creative" DROP COLUMN "media_data";""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "creative" ADD "media_data" BYTEA; - ALTER TABLE "creative" DROP COLUMN "media_s3_key";""" - - -MODELS_STATE = ( - "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" - "XdLO/D8vv/8PAyERnQ==" -) diff --git a/migrations/models/4_20260106192017_update.py b/migrations/models/4_20260106192017_update.py deleted file mode 100644 index dba4085..0000000 --- a/migrations/models/4_20260106192017_update.py +++ /dev/null @@ -1,149 +0,0 @@ -from tortoise import BaseDBAsyncClient - -RUN_IN_TRANSACTION = True - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "purchase" - ADD "comment" TEXT; - ALTER TABLE "purchase" - ADD "payment_at" TIMESTAMPTZ; - ALTER TABLE "purchase" - ADD "format" TEXT; - ALTER TABLE "purchase" - ADD "cost_before_bargain" DOUBLE PRECISION; - ALTER TABLE "purchase" - ADD "placement_at" TIMESTAMPTZ; - ALTER TABLE "purchase" - ADD "cost_type" VARCHAR(8); - ALTER TABLE "purchase" - ADD "cost_value" DOUBLE PRECISION; - ALTER TABLE "purchase" - ADD "purchase_type" VARCHAR(16); - ALTER TABLE "purchase_channel" - ADD "format" TEXT; - ALTER TABLE "purchase_channel" - ADD "cost_before_bargain" DOUBLE PRECISION; - ALTER TABLE "purchase_channel" - ADD "placement_at" TIMESTAMPTZ; - ALTER TABLE "purchase_channel" - ADD "cost_type" VARCHAR(8); - ALTER TABLE "purchase_channel" - ADD "cost_value" DOUBLE PRECISION; - ALTER TABLE "purchase_channel" - DROP COLUMN "planned_cost"; - COMMENT ON COLUMN "purchase"."cost_type" IS 'FIXED: fixed\nCPM: cpm'; - COMMENT ON COLUMN "purchase"."purchase_type" IS 'SELF_PROMO: self_promo\nSTANDARD: standard'; - COMMENT ON COLUMN "purchase_channel"."cost_type" IS 'FIXED: fixed\nCPM: cpm';""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - ALTER TABLE "purchase" - DROP COLUMN "comment"; - ALTER TABLE "purchase" - DROP COLUMN "payment_at"; - ALTER TABLE "purchase" - DROP COLUMN "format"; - ALTER TABLE "purchase" - DROP COLUMN "cost_before_bargain"; - ALTER TABLE "purchase" - DROP COLUMN "placement_at"; - ALTER TABLE "purchase" - DROP COLUMN "cost_type"; - ALTER TABLE "purchase" - DROP COLUMN "cost_value"; - ALTER TABLE "purchase" - DROP COLUMN "purchase_type"; - ALTER TABLE "purchase_channel" - ADD "planned_cost" DOUBLE PRECISION; - ALTER TABLE "purchase_channel" - DROP COLUMN "format"; - ALTER TABLE "purchase_channel" - DROP COLUMN "cost_before_bargain"; - ALTER TABLE "purchase_channel" - DROP COLUMN "placement_at"; - ALTER TABLE "purchase_channel" - DROP COLUMN "cost_type"; - ALTER TABLE "purchase_channel" - DROP COLUMN "cost_value";""" - - -MODELS_STATE = ( - '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' - '49ruDVq8qUI6aKVL2pn/5+X3/wGuY8FA' -) diff --git a/migrations/models/5_20260107135803_update.py b/migrations/models/5_20260107135803_update.py deleted file mode 100644 index 2b43fd2..0000000 --- a/migrations/models/5_20260107135803_update.py +++ /dev/null @@ -1,151 +0,0 @@ -from tortoise import BaseDBAsyncClient - -RUN_IN_TRANSACTION = True - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX IF EXISTS "uid_user_telegra_66ffbd"; - ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "uid_subscriptio_placeme_8fcb8f"; - ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "fk_subscrip_user_6b9f4b8e"; - CREATE TABLE IF NOT EXISTS "telegram_user" ( - "id" UUID NOT NULL PRIMARY KEY, - "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - "deleted_at" TIMESTAMPTZ, - "telegram_id" BIGINT NOT NULL UNIQUE, - "username" VARCHAR(255), - "first_name" VARCHAR(255), - "last_name" VARCHAR(255) -); -CREATE INDEX IF NOT EXISTS "idx_telegram_us_telegra_3a3100" ON "telegram_user" ("telegram_id"); - INSERT INTO "telegram_user" ( - "id", - "created_at", - "updated_at", - "deleted_at", - "telegram_id", - "username", - "first_name", - "last_name" -) -SELECT - "id", - "created_at", - "updated_at", - "deleted_at", - "telegram_id", - "username", - "first_name", - "last_name" -FROM "user" -WHERE "telegram_id" IS NOT NULL -ON CONFLICT ("id") DO NOTHING; - ALTER TABLE "subscription" RENAME COLUMN "subscriber_id" TO "telegram_user_id"; - ALTER TABLE "user" ADD "telegram_user_id" UUID UNIQUE; - UPDATE "user" SET "telegram_user_id" = "id"; - ALTER TABLE "user" ALTER COLUMN "telegram_user_id" SET NOT NULL; - ALTER TABLE "user" DROP COLUMN "first_name"; - ALTER TABLE "user" DROP COLUMN "username"; - ALTER TABLE "user" DROP COLUMN "telegram_id"; - ALTER TABLE "user" DROP COLUMN "last_name"; - ALTER TABLE "subscription" ADD CONSTRAINT "fk_subscrip_telegram_ab72b5bf" FOREIGN KEY ("telegram_user_id") REFERENCES "telegram_user" ("id") ON DELETE CASCADE; - CREATE UNIQUE INDEX IF NOT EXISTS "uid_subscriptio_placeme_75b40b" ON "subscription" ("placement_id", "telegram_user_id"); - ALTER TABLE "user" ADD CONSTRAINT "fk_user_telegram_548d951e" FOREIGN KEY ("telegram_user_id") REFERENCES "telegram_user" ("id") ON DELETE CASCADE;""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - DROP INDEX IF EXISTS "uid_subscriptio_placeme_75b40b"; - ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "fk_subscrip_telegram_ab72b5bf"; - ALTER TABLE "user" DROP CONSTRAINT IF EXISTS "fk_user_telegram_548d951e"; - ALTER TABLE "user" ADD "first_name" VARCHAR(255); - ALTER TABLE "user" ADD "username" VARCHAR(255); - ALTER TABLE "user" ADD "telegram_id" BIGINT NOT NULL UNIQUE; - ALTER TABLE "user" ADD "last_name" VARCHAR(255); - ALTER TABLE "user" DROP COLUMN "telegram_user_id"; - ALTER TABLE "subscription" RENAME COLUMN "telegram_user_id" TO "subscriber_id"; - DROP TABLE IF EXISTS "telegram_user"; - ALTER TABLE "subscription" ADD CONSTRAINT "fk_subscrip_user_6b9f4b8e" FOREIGN KEY ("subscriber_id") REFERENCES "user" ("id") ON DELETE CASCADE; - CREATE UNIQUE INDEX IF NOT EXISTS "uid_subscriptio_placeme_8fcb8f" ON "subscription" ("placement_id", "subscriber_id");""" - - -MODELS_STATE = ( - "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" - "1lQhHbSyJe3k/3n5/X8nR5KJ" -) diff --git a/src/adapter/postgres.py b/src/adapter/postgres.py index a875fc9..56436af 100644 --- a/src/adapter/postgres.py +++ b/src/adapter/postgres.py @@ -291,11 +291,11 @@ class Postgres(DatabaseBase, Database): ) async def get_purchase(self, workspace_id: uuid.UUID, purchase_id: uuid.UUID) -> domain.Purchase | None: - return await domain.Purchase.get_or_none(id=purchase_id, workspace_id=workspace_id) + return await domain.Purchase.get_or_none(id=purchase_id, project__workspace_id=workspace_id) async def get_project_purchases(self, workspace_id: uuid.UUID, project_id: uuid.UUID) -> list[domain.Purchase]: return ( - await domain.Purchase.filter(workspace_id=workspace_id, project_id=project_id) + await domain.Purchase.filter(project__workspace_id=workspace_id, project_id=project_id) .prefetch_related('channels', 'channels__channel') .all() ) @@ -315,6 +315,13 @@ class Postgres(DatabaseBase, Database): .first() ) + async def get_purchase_channel_by_id(self, purchase_channel_id: uuid.UUID) -> domain.PurchaseChannel | None: + return ( + await domain.PurchaseChannel.filter(id=purchase_channel_id) + .prefetch_related('channel', 'purchase', 'purchase__project') + .first() + ) + async def get_purchase_channels_by_project(self, project_id: uuid.UUID) -> list[domain.PurchaseChannel]: return ( await domain.PurchaseChannel.filter( @@ -378,9 +385,9 @@ class Postgres(DatabaseBase, Database): await domain.PurchaseChannel.filter(purchase_id=purchase_id, channel_id=channel_id).delete() async def get_creative(self, workspace_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None: - return await domain.Creative.get_or_none(id=creative_id, workspace_id=workspace_id).prefetch_related( - 'project', 'project__channel' - ) + return await domain.Creative.get_or_none( + id=creative_id, project__workspace_id=workspace_id + ).prefetch_related('project', 'project__channel') async def update_creative(self, creative: domain.Creative) -> None: await creative.save() @@ -392,7 +399,7 @@ class Postgres(DatabaseBase, Database): include_archived: bool = False, allowed_project_ids: set[uuid.UUID] | None = None, ) -> list[domain.Creative]: - query = domain.Creative.filter(workspace_id=workspace_id) + query = domain.Creative.filter(project__workspace_id=workspace_id) if project_id: query = query.filter(project_id=project_id) @@ -410,8 +417,16 @@ class Postgres(DatabaseBase, Database): await domain.Creative.filter(id=creative_id).delete() async def get_placement(self, workspace_id: uuid.UUID, placement_id: uuid.UUID) -> domain.Placement | None: - return await domain.Placement.get_or_none(id=placement_id, workspace_id=workspace_id).prefetch_related( - 'project', 'project__channel', 'placement_channel', 'creative', 'post', 'post__channel' + return await domain.Placement.get_or_none( + id=placement_id, project__workspace_id=workspace_id + ).prefetch_related( + 'project', + 'project__channel', + 'purchase_channel', + 'purchase_channel__channel', + 'creative', + 'post', + 'post__channel', ) async def create_placement(self, placement: domain.Placement) -> None: @@ -431,7 +446,7 @@ class Postgres(DatabaseBase, Database): date_from: datetime.datetime | None = None, date_to: datetime.datetime | None = None, ) -> list[domain.Placement]: - query = domain.Placement.filter(workspace_id=workspace_id) + query = domain.Placement.filter(project__workspace_id=workspace_id) if project_id: query = query.filter(project_id=project_id) @@ -440,7 +455,7 @@ class Postgres(DatabaseBase, Database): return [] query = query.filter(project_id__in=list(allowed_project_ids)) if placement_channel_id: - query = query.filter(placement_channel_id=placement_channel_id) + query = query.filter(purchase_channel__channel_id=placement_channel_id) if creative_id: query = query.filter(creative_id=creative_id) @@ -454,7 +469,7 @@ class Postgres(DatabaseBase, Database): return ( await query.prefetch_related( - 'project', 'project__channel', 'placement_channel', 'creative', 'post', 'post__channel' + 'project', 'project__channel', 'purchase_channel', 'purchase_channel__channel', 'creative', 'post', 'post__channel' ) .order_by('-wanted_placement_date') .all() @@ -464,9 +479,9 @@ class Postgres(DatabaseBase, Database): await domain.Placement.filter(id=placement_id).delete() async def get_placement_by_invite_link(self, invite_link: str) -> domain.Placement | None: - return await domain.Placement.get_or_none(invite_link=invite_link).prefetch_related( - 'project', 'project__channel', 'placement_channel', 'creative' - ) + return await domain.Placement.get_or_none( + purchase_channel__invite_link=invite_link + ).prefetch_related('project', 'project__channel', 'purchase_channel', 'purchase_channel__channel', 'creative') async def create_subscription(self, subscription: domain.Subscription) -> None: await subscription.save() diff --git a/src/domain/__init__.py b/src/domain/__init__.py index 27dff0d..898a1a3 100644 --- a/src/domain/__init__.py +++ b/src/domain/__init__.py @@ -41,6 +41,7 @@ __all__ = ( 'WorkspaceAccessDenied', 'WorkspaceInviteNotFound', 'WorkspaceInviteAlreadyExists', + 'WorkspaceInviteAlreadyProcessed', 'WorkspaceMemberAlreadyExists', 'LoginTokenNotFound', 'LoginTokenExpired', @@ -84,6 +85,7 @@ from .error import ( UserNotFound, WorkspaceAccessDenied, WorkspaceInviteAlreadyExists, + WorkspaceInviteAlreadyProcessed, WorkspaceInviteNotFound, WorkspaceMemberAlreadyExists, WorkspaceNotFound, diff --git a/src/domain/creative.py b/src/domain/creative.py index 1549d29..aeea534 100644 --- a/src/domain/creative.py +++ b/src/domain/creative.py @@ -10,7 +10,6 @@ from .error import CreativeInviteLinkNotFound, CreativeMediaTooLarge, CreativeMu if TYPE_CHECKING: from .project import Project - from .workspace import Workspace class CreativeStatus(str, enum.Enum): @@ -27,15 +26,11 @@ class Creative(TimestampedModel): buttons = fields.JSONField(default=list) status = fields.CharEnumField(CreativeStatus, default=CreativeStatus.ACTIVE) - workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField( - 'models.Workspace', related_name='creatives', on_delete=fields.CASCADE, index=True - ) project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField( 'models.Project', related_name='creatives', on_delete=fields.CASCADE, index=True ) if TYPE_CHECKING: - workspace_id: UUID project_id: UUID class Meta: diff --git a/src/domain/placement.py b/src/domain/placement.py index d388370..f6b9f5c 100644 --- a/src/domain/placement.py +++ b/src/domain/placement.py @@ -7,12 +7,10 @@ from tortoise import fields from .base import TimestampedModel if TYPE_CHECKING: - from .channel import Channel from .creative import Creative from .post import Post from .project import Project - from .purchase import Purchase, PurchaseChannel - from .workspace import Workspace + from .purchase import PurchaseChannel class PlacementStatus(str, enum.Enum): @@ -31,27 +29,17 @@ class Placement(TimestampedModel): wanted_placement_date = fields.DatetimeField(db_column='placement_date') cost = fields.FloatField(null=True) comment = fields.TextField(null=True) - invite_link = fields.CharField(max_length=512) status = fields.CharEnumField(PlacementStatus, default=PlacementStatus.ACTIVE) - workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField( - 'models.Workspace', related_name='placements', on_delete=fields.CASCADE, index=True - ) project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField( 'models.Project', related_name='placements', on_delete=fields.CASCADE, index=True ) creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField( 'models.Creative', related_name='placements', on_delete=fields.CASCADE, index=True ) - placement_channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField( - 'models.Channel', related_name='placements', on_delete=fields.CASCADE, index=True - ) - purchase: fields.ForeignKeyRelation['Purchase'] | None = fields.ForeignKeyField( - 'models.Purchase', related_name='placements', on_delete=fields.CASCADE, null=True, index=True - ) - purchase_channel: fields.ForeignKeyRelation['PurchaseChannel'] | None = fields.ForeignKeyField( - 'models.PurchaseChannel', related_name='placements', on_delete=fields.CASCADE, null=True, index=True + purchase_channel: fields.ForeignKeyRelation['PurchaseChannel'] = fields.ForeignKeyField( + 'models.PurchaseChannel', related_name='placements', on_delete=fields.CASCADE, index=True ) post: fields.ForeignKeyRelation['Post'] | None = fields.ForeignKeyField( @@ -59,13 +47,10 @@ class Placement(TimestampedModel): ) if TYPE_CHECKING: - workspace_id: UUID project_id: UUID creative_id: UUID - placement_channel_id: UUID - purchase_id: UUID | None post_id: UUID | None - purchase_channel_id: UUID | None + purchase_channel_id: UUID class Meta: table = 'placement' diff --git a/src/domain/purchase.py b/src/domain/purchase.py index 8bba5c0..238f0b9 100644 --- a/src/domain/purchase.py +++ b/src/domain/purchase.py @@ -10,7 +10,6 @@ if TYPE_CHECKING: from .channel import Channel from .creative import Creative from .project import Project - from .workspace import Workspace class PurchaseStatus(enum.StrEnum): @@ -54,9 +53,9 @@ class Purchase(TimestampedModel): 'models.Creative', related_name='purchases', on_delete=fields.CASCADE, index=True ) - workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField( - 'models.Workspace', related_name='purchases', on_delete=fields.CASCADE, index=True - ) + if TYPE_CHECKING: + project_id: uuid.UUID + creative_id: uuid.UUID class Meta: table = 'purchase' diff --git a/src/dto/placement.py b/src/dto/placement.py index 7ea0250..4fc5e77 100644 --- a/src/dto/placement.py +++ b/src/dto/placement.py @@ -47,9 +47,7 @@ class GetPlacementInput(pydantic.BaseModel): class CreatePlacementInput(pydantic.BaseModel): - project_id: uuid.UUID - placement_channel_id: uuid.UUID - creative_id: uuid.UUID + purchase_channel_id: uuid.UUID placement_date: datetime.datetime cost: float | None = None comment: str | None = None diff --git a/src/usecase/__init__.py b/src/usecase/__init__.py index 08076bb..b368109 100644 --- a/src/usecase/__init__.py +++ b/src/usecase/__init__.py @@ -175,6 +175,8 @@ class Database(typing.Protocol): self, purchase_channel_id: UUID, purchase_id: UUID ) -> domain.PurchaseChannel | None: ... + async def get_purchase_channel_by_id(self, purchase_channel_id: UUID) -> domain.PurchaseChannel | None: ... + async def get_purchase_channels_by_project(self, project_id: UUID) -> list[domain.PurchaseChannel]: ... async def add_channel_to_purchase( @@ -225,6 +227,10 @@ class Database(typing.Protocol): date_to: datetime.datetime | None = None, ) -> list[domain.Placement]: ... + async def create_placement(self, placement: domain.Placement) -> None: ... + + async def update_placement(self, placement: domain.Placement) -> None: ... + async def delete_placement(self, placement_id: UUID) -> None: ... async def get_placement_by_invite_link(self, invite_link: str) -> domain.Placement | None: ... @@ -398,7 +404,7 @@ class Usecase: if purchase: return purchase - purchase = domain.Purchase(workspace_id=project.workspace_id, project_id=project.id, creative_id=creative.id) + purchase = domain.Purchase(project_id=project.id, creative_id=creative.id) await self.database.create_purchase(purchase) return purchase diff --git a/src/usecase/analytics/get_channel_analytics.py b/src/usecase/analytics/get_channel_analytics.py index f85afba..7c5dce3 100644 --- a/src/usecase/analytics/get_channel_analytics.py +++ b/src/usecase/analytics/get_channel_analytics.py @@ -47,8 +47,9 @@ async def get_channel_analytics( if input.project_id is None and allowed_project_ids is not None: channel_map: dict[UUID, domain.Channel] = {} for placement in placements: - if placement.placement_channel: - channel_map[placement.placement_channel_id] = placement.placement_channel + purchase_channel = placement.purchase_channel + if purchase_channel and purchase_channel.channel: + channel_map[purchase_channel.channel_id] = purchase_channel.channel channels = list(channel_map.values()) @dataclass @@ -69,7 +70,10 @@ async def get_channel_analytics( subscriptions_counts = await self.database.count_subscriptions_by_placement_batch(placement_ids) for p in placements: - stats = channel_stats.get(p.placement_channel_id) + purchase_channel = p.purchase_channel + if not purchase_channel: + continue + stats = channel_stats.get(purchase_channel.channel_id) if stats is None: continue diff --git a/src/usecase/analytics/get_overview_analytics.py b/src/usecase/analytics/get_overview_analytics.py index 3a6dfa5..d8e1f44 100644 --- a/src/usecase/analytics/get_overview_analytics.py +++ b/src/usecase/analytics/get_overview_analytics.py @@ -131,19 +131,21 @@ async def get_overview_analytics( project_meta: dict[UUID, domain.Project] = {} for placement in current_placements: - placement_date = placement.wanted_placement_date.date() + placement_day = placement.wanted_placement_date.date() project_meta[placement.project_id] = placement.project cost_value = placement.cost or 0.0 - cost_per_day[placement_date] += cost_value + cost_per_day[placement_day] += cost_value project_spending_map[placement.project_id] += cost_value subs = subs_per_placement_current.get(placement.id, 0) - channel_id = placement.placement_channel_id - if channel_id not in channel_stats: - channel_stats[channel_id] = ChannelAggregate(channel=placement.placement_channel) - channel_stats[channel_id].total_cost += cost_value - channel_stats[channel_id].total_subs += subs + purchase_channel = placement.purchase_channel + if purchase_channel: + channel_id = purchase_channel.channel_id + if channel_id not in channel_stats: + channel_stats[channel_id] = ChannelAggregate(channel=purchase_channel.channel) + channel_stats[channel_id].total_cost += cost_value + channel_stats[channel_id].total_subs += subs start_date = input.date_from.date() end_date = input.date_to.date() diff --git a/src/usecase/analytics/get_placements_analytics.py b/src/usecase/analytics/get_placements_analytics.py index 97021f6..28f0713 100644 --- a/src/usecase/analytics/get_placements_analytics.py +++ b/src/usecase/analytics/get_placements_analytics.py @@ -57,8 +57,8 @@ async def get_placements_analytics( id=p.id, project_id=p.project_id, project_channel_title=p.project.channel.title, - placement_channel_id=p.placement_channel_id, - placement_channel_title=p.placement_channel.title, + placement_channel_id=p.purchase_channel.channel_id, + placement_channel_title=p.purchase_channel.channel.title, creative_id=p.creative_id, creative_name=p.creative.name, placement_date=p.wanted_placement_date, diff --git a/src/usecase/channel/attach_channel_to_workspace.py b/src/usecase/channel/attach_channel_to_workspace.py index 0e67b8a..30c2e2e 100644 --- a/src/usecase/channel/attach_channel_to_workspace.py +++ b/src/usecase/channel/attach_channel_to_workspace.py @@ -47,4 +47,5 @@ async def attach_channel_to_workspace( title=channel.title, username=channel.username, status=project.status, + purchase_invite_type_default=project.purchase_invite_type_default, ) diff --git a/src/usecase/creative/create_creative.py b/src/usecase/creative/create_creative.py index 4efa647..50ffd1f 100644 --- a/src/usecase/creative/create_creative.py +++ b/src/usecase/creative/create_creative.py @@ -43,7 +43,6 @@ async def create_creative( buttons=[button.model_dump() for button in input.buttons], status=domain.CreativeStatus.ACTIVE, project_id=project.id, - workspace_id=workspace_id, ) await self.database.create_creative(creative) diff --git a/src/usecase/placement/create_placement.py b/src/usecase/placement/create_placement.py deleted file mode 100644 index 69e36d2..0000000 --- a/src/usecase/placement/create_placement.py +++ /dev/null @@ -1,84 +0,0 @@ -import logging -import uuid -from typing import TYPE_CHECKING - -import domain.purchase -from src import domain, dto - -if TYPE_CHECKING: - from .. import Usecase - -log = logging.getLogger(__name__) - - -async def create_placement( - self: 'Usecase', input: dto.CreatePlacementInput, user_id: uuid.UUID, workspace_id: uuid.UUID -) -> dto.PlacementOutput: - await self.ensure_workspace_permission( - workspace_id, - user_id, - domain.PermissionKey.PLACEMENTS_WRITE, - for_project_id=input.project_id, - ) - - project = await self.database.get_project(workspace_id, project_id=input.project_id) - if not project: - raise domain.ProjectNotFound(input.project_id) - - placement_channel = await self.database.get_channel(channel_id=input.placement_channel_id) - if not placement_channel: - raise domain.ChannelNotFound(input.placement_channel_id) - - creative = await self.database.get_creative(workspace_id, input.creative_id) - if not creative: - raise domain.CreativeNotFound(input.creative_id) - if creative.project_id != project.id: - raise domain.CreativeNotFound(input.creative_id) - - # Берем тип ссылки из настроек проекта - requires_approval = project.purchase_invite_type_default == domain.purchase.InviteLinkType.APPROVAL - - if project.channel.telegram_id is None: - raise domain.ChannelNotFound(project.channel.id) - - invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval) - log.info( - 'Created invite link for project=%s channel_telegram_id=%s requires_approval=%s invite_link=%s', - project.id, - project.channel.telegram_id, - requires_approval, - invite_link, - ) - - placement = domain.Placement( - project_id=input.project_id, - placement_channel_id=input.placement_channel_id, - creative_id=input.creative_id, - workspace_id=workspace_id, - placement_date=input.placement_date, - cost=input.cost, - comment=input.comment, - invite_link=invite_link, - status=domain.PlacementStatus.ACTIVE, - ) - - await self.database.create_placement(placement) - - return dto.PlacementOutput( - id=placement.id, - project_id=placement.project_id, - project_channel_title=project.channel.title, - placement_channel_id=placement.placement_channel_id, - placement_channel_title=placement_channel.title, - creative_id=placement.creative_id, - creative_name=creative.name, - placement_date=placement.wanted_placement_date, - cost=placement.cost, - comment=placement.comment, - ad_post_url=None, - invite_link_type=project.purchase_invite_type_default, - invite_link=placement.invite_link, - status=placement.status, - subscriptions_count=0, - created_at=placement.created_at, - ) diff --git a/src/usecase/placement/delete_placement.py b/src/usecase/placement/delete_placement.py deleted file mode 100644 index 21735d6..0000000 --- a/src/usecase/placement/delete_placement.py +++ /dev/null @@ -1,24 +0,0 @@ -import logging -from typing import TYPE_CHECKING - -from src import domain, dto - -if TYPE_CHECKING: - from .. import Usecase - -log = logging.getLogger(__name__) - - -async def delete_placement(self: 'Usecase', input: dto.DeletePlacementInput) -> None: - context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_WRITE - ) - - placement = await self.database.get_placement(input.workspace_id, input.placement_id) - if not placement: - log.warning('Placement %s not found for user %s', input.placement_id, input.user_id) - raise domain.PlacementNotFound(input.placement_id) - - context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_WRITE, placement.project_id) - - await self.database.delete_placement(input.placement_id) diff --git a/src/usecase/placement/fetch_placement_post_cycle.py b/src/usecase/placement/fetch_placement_post_cycle.py index 1251642..cf69361 100644 --- a/src/usecase/placement/fetch_placement_post_cycle.py +++ b/src/usecase/placement/fetch_placement_post_cycle.py @@ -44,12 +44,8 @@ async def fetch_placement_post_cycle(self: 'Usecase', interval_seconds: int) -> wanted_placement_date=post.created_at, cost=None, comment=None, - invite_link=purchase_channel.invite_link, - workspace_id=purchase_channel.purchase.workspace_id, project_id=purchase_channel.purchase.project_id, creative_id=purchase_channel.purchase.creative_id, - placement_channel_id=purchase_channel.channel_id, - purchase_id=purchase_channel.purchase_id, purchase_channel_id=purchase_channel.id, post=post, status=domain.PlacementStatus.ACTIVE, diff --git a/src/usecase/placement/get_placement.py b/src/usecase/placement/get_placement.py index 6a2499d..9fd10fa 100644 --- a/src/usecase/placement/get_placement.py +++ b/src/usecase/placement/get_placement.py @@ -22,6 +22,10 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id) ad_post_url = placement.post.url if placement.post else None + purchase_channel = placement.purchase_channel + if not purchase_channel or not purchase_channel.channel: + log.warning('Placement %s missing purchase channel data', placement.id) + raise domain.PurchaseChannelNotFound() subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id) @@ -29,17 +33,19 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl id=placement.id, project_id=placement.project_id, project_channel_title=placement.project.channel.title, - placement_channel_id=placement.placement_channel_id, - placement_channel_title=placement.placement_channel.title, + placement_channel_id=purchase_channel.channel_id, + placement_channel_title=purchase_channel.channel.title, creative_id=placement.creative_id, creative_name=placement.creative.name, placement_date=placement.wanted_placement_date, cost=placement.cost, comment=placement.comment, ad_post_url=ad_post_url, - invite_link_type=placement.project.purchase_invite_type_default, - invite_link=placement.invite_link, + invite_link_type=purchase_channel.invite_link_type, + invite_link=purchase_channel.invite_link, status=placement.status, subscriptions_count=subscriptions_count, + purchase_id=purchase_channel.purchase_id, + purchase_channel_id=purchase_channel.id, created_at=placement.created_at, ) diff --git a/src/usecase/placement/get_placements.py b/src/usecase/placement/get_placements.py index 7a9c931..0f1eb1e 100644 --- a/src/usecase/placement/get_placements.py +++ b/src/usecase/placement/get_placements.py @@ -1,3 +1,4 @@ +import logging from typing import TYPE_CHECKING from src import domain, dto @@ -5,6 +6,8 @@ from src import domain, dto if TYPE_CHECKING: from .. import Usecase +log = logging.getLogger(__name__) + async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> list[dto.PlacementOutput]: context = await self.ensure_workspace_permission( @@ -32,24 +35,30 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> list placement_outputs = [] for placement in placements: ad_post_url = placement.post.url if placement.post else None + purchase_channel = placement.purchase_channel + if not purchase_channel or not purchase_channel.channel: + log.warning('Placement %s missing purchase channel data', placement.id) + continue placement_outputs.append( dto.PlacementOutput( id=placement.id, project_id=placement.project_id, project_channel_title=placement.project.channel.title, - placement_channel_id=placement.placement_channel_id, - placement_channel_title=placement.placement_channel.title, + placement_channel_id=purchase_channel.channel_id, + placement_channel_title=purchase_channel.channel.title, creative_id=placement.creative_id, creative_name=placement.creative.name, placement_date=placement.wanted_placement_date, cost=placement.cost, comment=placement.comment, ad_post_url=ad_post_url, - invite_link_type=placement.project.purchase_invite_type_default, - invite_link=placement.invite_link, + invite_link_type=purchase_channel.invite_link_type, + invite_link=purchase_channel.invite_link, status=placement.status, subscriptions_count=subscriptions_counts.get(placement.id, 0), + purchase_id=purchase_channel.purchase_id, + purchase_channel_id=purchase_channel.id, created_at=placement.created_at, ) ) diff --git a/src/usecase/placement/update_placement.py b/src/usecase/placement/update_placement.py deleted file mode 100644 index 766bb41..0000000 --- a/src/usecase/placement/update_placement.py +++ /dev/null @@ -1,61 +0,0 @@ -import logging -import uuid -from typing import TYPE_CHECKING - -from src import domain, dto - -if TYPE_CHECKING: - from .. import Usecase - -log = logging.getLogger(__name__) - - -async def update_placement( - self: 'Usecase', - placement_id: uuid.UUID, - input: dto.UpdatePlacementInput, - user_id: uuid.UUID, - workspace_id: uuid.UUID, -) -> dto.PlacementOutput: - context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE) - - placement = await self.database.get_placement(workspace_id, placement_id) - if not placement: - log.warning('Placement %s not found for user %s', placement_id, user_id) - raise domain.PlacementNotFound(placement_id) - - context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_WRITE, placement.project_id) - - if input.placement_date is not None: - placement.wanted_placement_date = input.placement_date - if input.cost is not None: - placement.cost = input.cost - if input.comment is not None: - placement.comment = input.comment - if input.status is not None: - placement.status = input.status - - await self.database.update_placement(placement) - - ad_post_url = placement.post.url if placement.post else None - - subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id) - - return dto.PlacementOutput( - id=placement.id, - project_id=placement.project_id, - project_channel_title=placement.project.channel.title, - placement_channel_id=placement.placement_channel_id, - placement_channel_title=placement.placement_channel.title, - creative_id=placement.creative_id, - creative_name=placement.creative.name, - placement_date=placement.wanted_placement_date, - cost=placement.cost, - comment=placement.comment, - ad_post_url=ad_post_url, - invite_link_type=placement.project.purchase_invite_type_default, - invite_link=placement.invite_link, - status=placement.status, - subscriptions_count=subscriptions_count, - created_at=placement.created_at, - ) diff --git a/src/usecase/purchase/create_purchase.py b/src/usecase/purchase/create_purchase.py index cdb275b..12c52f7 100644 --- a/src/usecase/purchase/create_purchase.py +++ b/src/usecase/purchase/create_purchase.py @@ -99,7 +99,6 @@ async def create_purchase( details = input.details purchase = domain.Purchase( - workspace_id=workspace_id, project_id=project.id, creative_id=creative.id, placement_at=details.placement_at if details else None, diff --git a/telegram_bot/screens/purchase_details.go b/telegram_bot/screens/purchase_details.go index 8565709..8b5aa3b 100644 --- a/telegram_bot/screens/purchase_details.go +++ b/telegram_bot/screens/purchase_details.go @@ -3,7 +3,9 @@ package screens import ( "context" "fmt" + "strings" + "example.com/m/adapter/backend" "example.com/m/bot" "github.com/NicoNex/echotron/v3" "github.com/rs/zerolog/log" @@ -42,6 +44,8 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str return } + placementsByChannel := s.loadPlacementsByChannel(b, jwt, purchase) + // Получаем информацию о креативе creative, err := b.Backend.GetCreative( context.Background(), @@ -54,13 +58,18 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str creative = nil } + totalPlacements := 0 + for _, items := range placementsByChannel { + totalPlacements += len(items) + } + text := "… › Закупы › Детали\n\nДетали закупа\n\n" // Информация о креативе if creative != nil { - text += fmt.Sprintf("▪ Креатив: %s\n", creative.Name) + text += fmt.Sprintf("• Креатив: %s\n", creative.Name) } else { - text += fmt.Sprintf("▪ Креатив ID: %s\n", purchase.CreativeID) + text += fmt.Sprintf("• Креатив ID: %s\n", purchase.CreativeID) } // Статус закупа @@ -70,7 +79,9 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str statusSymbol = "■" statusText = "Архивирован" } - text += fmt.Sprintf("▪ Статус: %s %s\n\n", statusSymbol, statusText) + text += fmt.Sprintf("• Статус: %s %s\n", statusSymbol, statusText) + text += fmt.Sprintf("• Каналов: %d\n", len(purchase.Channels)) + text += fmt.Sprintf("• Размещений: %d\n\n", totalPlacements) // Информация о каналах if len(purchase.Channels) == 0 { @@ -79,20 +90,7 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str text += fmt.Sprintf("Каналы (%d):\n\n", len(purchase.Channels)) for i, ch := range purchase.Channels { - // Определяем статус - statusSymbol := "○" - switch ch.Status { - case "planned": - statusSymbol = "○" - case "approved": - statusSymbol = "◉" - case "rejected": - statusSymbol = "⊗" - case "in_progress": - statusSymbol = "◐" - case "completed": - statusSymbol = "●" - } + chStatusText := formatPurchaseChannelStatus(ch.Status) // Название канала channelName := "Без названия" @@ -102,16 +100,43 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str channelName = "@" + *ch.Channel.Username } - text += fmt.Sprintf("%d. %s %s\n", i+1, statusSymbol, channelName) + text += fmt.Sprintf("%d. %s\n", i+1, channelName) + text += fmt.Sprintf(" ◦ %s\n", chStatusText) // Стоимость if ch.Details != nil && ch.Details.Cost != nil { - text += fmt.Sprintf(" ▸ Стоимость: %.0f₽\n", ch.Details.Cost.Value) + text += fmt.Sprintf(" ◦ Стоимость: %.0f₽\n", ch.Details.Cost.Value) + } + + // Дата размещения (план) + if ch.Details != nil && ch.Details.PlacementAt != nil && *ch.Details.PlacementAt != "" { + text += fmt.Sprintf(" ◦ План: %s\n", *ch.Details.PlacementAt) } // Комментарий if ch.Comment != nil && *ch.Comment != "" { - text += fmt.Sprintf(" ▸ %s\n", *ch.Comment) + text += fmt.Sprintf(" ◦ %s\n", *ch.Comment) + } + + // Информация о размещении + placements := placementsByChannel[ch.ID] + if len(placements) == 0 { + text += " ◦ Размещение: нет\n" + } else { + placement := placements[0] + text += fmt.Sprintf(" ◦ Размещение: есть (%d)\n", len(placements)) + if placement.Status != "" { + text += fmt.Sprintf(" ◦ Статус размещения: %s\n", formatPlacementStatus(placement.Status)) + } + if placement.PlacementDate != "" { + text += fmt.Sprintf(" ◦ Дата: %s\n", placement.PlacementDate) + } + if placement.AdPostURL != nil && *placement.AdPostURL != "" { + text += fmt.Sprintf(" ◦ Пост: %s\n", *placement.AdPostURL) + } + if placement.SubscriptionsCount > 0 { + text += fmt.Sprintf(" ◦ Подписки: %d\n", placement.SubscriptionsCount) + } } // Тип invite link @@ -119,7 +144,7 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str if ch.InviteLinkType == "approval" { inviteLinkTypeText = "с одобрением" } - text += fmt.Sprintf(" ▸ Ссылка: %s\n", inviteLinkTypeText) + text += fmt.Sprintf(" ◦ Ссылка: %s\n", inviteLinkTypeText) text += "\n" } @@ -136,6 +161,94 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str b.Render(text, keyboard, mode) } +func (s *PurchaseDetails) loadPlacementsByChannel( + b *bot.Bot, + jwt string, + purchase *backend.Purchase, +) map[string][]backend.Placement { + placementsByChannel := make(map[string][]backend.Placement) + if purchase == nil || purchase.CreativeID == "" || len(purchase.Channels) == 0 { + return placementsByChannel + } + + projectID := s.ProjectID + creativeID := purchase.CreativeID + pageSize := 50 + maxPages := 5 + foundChannels := make(map[string]bool) + + for page := 1; page <= maxPages; page++ { + resp, err := b.Backend.GetPlacements( + context.Background(), + jwt, + s.WorkspaceID, + &projectID, + nil, + &creativeID, + false, + page, + pageSize, + ) + if err != nil { + log.Error().Err(err).Msg("Failed to get placements for purchase details") + break + } + + for _, placement := range resp.Items { + if placement.PurchaseID == nil || *placement.PurchaseID != purchase.ID { + continue + } + if placement.PurchaseChannelID == nil || *placement.PurchaseChannelID == "" { + continue + } + chID := *placement.PurchaseChannelID + placementsByChannel[chID] = append(placementsByChannel[chID], placement) + foundChannels[chID] = true + } + + if resp.Pages <= page || len(foundChannels) == len(purchase.Channels) { + break + } + } + + return placementsByChannel +} + +func formatPurchaseChannelStatus(status string) string { + switch status { + case "planned": + return "Планируется" + case "approved": + return "Согласовано" + case "rejected": + return "Отклонено" + case "in_progress": + return "В работе" + case "completed": + return "Завершено" + default: + return "Неизвестно" + } +} + +func formatPlacementStatus(status string) string { + normalized := strings.TrimSpace(strings.ToLower(status)) + switch normalized { + case "planned": + return "Планируется" + case "approved": + return "Согласовано" + case "rejected": + return "Отклонено" + case "in_progress": + return "В работе" + case "completed": + return "Размещено" + default: + return status + } +} + func (s *PurchaseDetails) HandleCallback(b *bot.Bot, u *echotron.Update) { if u.CallbackQuery == nil || u.CallbackQuery.Data == "" { return diff --git a/telegram_bot/screens/purchase_optional_details.go b/telegram_bot/screens/purchase_optional_details.go index 6d3fff7..f68b95c 100644 --- a/telegram_bot/screens/purchase_optional_details.go +++ b/telegram_bot/screens/purchase_optional_details.go @@ -1283,9 +1283,13 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) { } } - b.SendNew("✅ Закуп создан", bot.Keyboard( - bot.Row(bot.Button("● Готово", "done")), - )) + b.SetState(&PurchaseDetails{ + WorkspaceID: s.WorkspaceID, + ProjectID: s.ProjectID, + ProjectTitle: s.ProjectTitle, + PurchaseID: purchase.ID, + BackState: s.BackState, + }, bot.NewMessage) } func (s *PurchaseOptionalDetails) buildPurchaseDetails(purchaseType *string) *backend.PurchaseDetails { @@ -1449,5 +1453,15 @@ func (s *PurchaseOptionalDetails) injectInviteLink(text, inviteLink string) stri return text } re := regexp.MustCompile(`([^<]*)`) - return re.ReplaceAllString(text, fmt.Sprintf(`$1`, inviteLink)) + return re.ReplaceAllStringFunc(text, func(match string) string { + sub := re.FindStringSubmatch(match) + if len(sub) < 2 { + return match + } + inner := strings.TrimSpace(sub[1]) + if inner == "" { + return fmt.Sprintf(`%s`, inviteLink, inviteLink) + } + return fmt.Sprintf(`%s`, inviteLink, sub[1]) + }) } diff --git a/telegram_bot/screens/select_channels_for_purchase.go b/telegram_bot/screens/select_channels_for_purchase.go index c50df6a..1b480c8 100644 --- a/telegram_bot/screens/select_channels_for_purchase.go +++ b/telegram_bot/screens/select_channels_for_purchase.go @@ -245,22 +245,13 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) { return } - // Показываем успешное создание - successText := fmt.Sprintf(` -… › Создание › Готово - -Закуп создан - - ▪ ID: %s - ▪ Креатив: %s - ▪ Каналов: %d - -Закуп создан и готов к работе -`, purchase.ID, purchase.CreativeID, len(purchase.Channels)) - - b.Edit(successText, bot.Keyboard( - bot.Row(bot.Button("● Готово", "done")), - )) + b.SetState(&PurchaseDetails{ + WorkspaceID: s.WorkspaceID, + ProjectID: s.ProjectID, + ProjectTitle: s.ProjectTitle, + PurchaseID: purchase.ID, + BackState: s.BackState, + }, bot.EditMessage) } func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update) {