feat: переименование доменной области
This commit is contained in:
@@ -1,221 +0,0 @@
|
|||||||
from tortoise import BaseDBAsyncClient
|
|
||||||
|
|
||||||
RUN_IN_TRANSACTION = True
|
|
||||||
|
|
||||||
|
|
||||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
|
||||||
return """
|
|
||||||
CREATE TABLE IF NOT EXISTS "subscriber" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"telegram_id" BIGINT NOT NULL UNIQUE,
|
|
||||||
"username" VARCHAR(255),
|
|
||||||
"first_name" VARCHAR(255),
|
|
||||||
"last_name" VARCHAR(255)
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_subscriber_telegra_5eef32" ON "subscriber" ("telegram_id");
|
|
||||||
CREATE TABLE IF NOT EXISTS "telegram_state" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"telegram_id" BIGINT NOT NULL UNIQUE,
|
|
||||||
"state" VARCHAR(24) NOT NULL,
|
|
||||||
"context" JSONB NOT NULL
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_telegram_st_telegra_750503" ON "telegram_state" ("telegram_id");
|
|
||||||
COMMENT ON COLUMN "telegram_state"."state" IS 'CREATIVE_WAITING_CHANNEL: creative_waiting_channel\nCREATIVE_WAITING_NAME: creative_waiting_name\nCREATIVE_WAITING_TEXT: creative_waiting_text';
|
|
||||||
CREATE TABLE IF NOT EXISTS "user" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"telegram_id" BIGINT NOT NULL UNIQUE,
|
|
||||||
"username" VARCHAR(255)
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_user_telegra_66ffbd" ON "user" ("telegram_id");
|
|
||||||
CREATE TABLE IF NOT EXISTS "external_channel" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"telegram_id" BIGINT NOT NULL UNIQUE,
|
|
||||||
"title" VARCHAR(255) NOT NULL,
|
|
||||||
"username" VARCHAR(255),
|
|
||||||
"description" TEXT,
|
|
||||||
"subscribers_count" INT,
|
|
||||||
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_external_ch_telegra_13cca5" ON "external_channel" ("telegram_id");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_external_ch_usernam_9b9fb4" ON "external_channel" ("username");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_external_ch_user_id_dd2b1c" ON "external_channel" ("user_id");
|
|
||||||
CREATE TABLE IF NOT EXISTS "login_token" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"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 TABLE IF NOT EXISTS "target_channel" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"telegram_id" BIGINT NOT NULL UNIQUE,
|
|
||||||
"title" VARCHAR(255) NOT NULL,
|
|
||||||
"username" VARCHAR(255),
|
|
||||||
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
|
||||||
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_target_chan_telegra_ab5a54" ON "target_channel" ("telegram_id");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_target_chan_usernam_a6cf8b" ON "target_channel" ("username");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_target_chan_user_id_c3bdc7" ON "target_channel" ("user_id");
|
|
||||||
COMMENT ON COLUMN "target_channel"."status" IS 'ACTIVE: active\nINACTIVE: inactive\nARCHIVED: archived';
|
|
||||||
CREATE TABLE IF NOT EXISTS "creative" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"name" VARCHAR(255) NOT NULL,
|
|
||||||
"text" TEXT NOT NULL,
|
|
||||||
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
|
||||||
"placements_count" INT NOT NULL DEFAULT 0,
|
|
||||||
"target_channel_id" UUID NOT NULL REFERENCES "target_channel" ("id") ON DELETE CASCADE,
|
|
||||||
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_creative_target__8e3b64" ON "creative" ("target_channel_id");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_creative_user_id_a27cfc" ON "creative" ("user_id");
|
|
||||||
COMMENT ON COLUMN "creative"."status" IS 'ACTIVE: active\nARCHIVED: archived';
|
|
||||||
CREATE TABLE IF NOT EXISTS "placement" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"placement_date" TIMESTAMPTZ NOT NULL,
|
|
||||||
"cost" DOUBLE PRECISION,
|
|
||||||
"comment" TEXT,
|
|
||||||
"ad_post_url" VARCHAR(512),
|
|
||||||
"invite_link_type" VARCHAR(8) NOT NULL,
|
|
||||||
"invite_link" VARCHAR(512) NOT NULL,
|
|
||||||
"external_channel_message_id" INT,
|
|
||||||
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
|
||||||
"subscriptions_count" INT NOT NULL DEFAULT 0,
|
|
||||||
"views_count" INT,
|
|
||||||
"views_availability" VARCHAR(11) NOT NULL DEFAULT 'unknown',
|
|
||||||
"last_views_fetch_at" TIMESTAMPTZ,
|
|
||||||
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
|
|
||||||
"external_channel_id" UUID NOT NULL REFERENCES "external_channel" ("id") ON DELETE CASCADE,
|
|
||||||
"target_channel_id" UUID NOT NULL REFERENCES "target_channel" ("id") ON DELETE CASCADE,
|
|
||||||
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_creativ_32111d" ON "placement" ("creative_id");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_externa_5b96c7" ON "placement" ("external_channel_id");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_target__ed9070" ON "placement" ("target_channel_id");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_user_id_7abaf9" ON "placement" ("user_id");
|
|
||||||
COMMENT ON COLUMN "placement"."invite_link_type" IS 'PUBLIC: public\nAPPROVAL: approval';
|
|
||||||
COMMENT ON COLUMN "placement"."status" IS 'PENDING: pending\nACTIVE: active\nDELETED: deleted\nARCHIVED: archived';
|
|
||||||
COMMENT ON COLUMN "placement"."views_availability" IS 'UNKNOWN: unknown\nAVAILABLE: available\nUNAVAILABLE: unavailable\nMANUAL: manual';
|
|
||||||
CREATE TABLE IF NOT EXISTS "placement_views_history" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"views_count" INT NOT NULL,
|
|
||||||
"fetched_at" TIMESTAMPTZ NOT NULL,
|
|
||||||
"placement_id" UUID NOT NULL REFERENCES "placement" ("id") ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_v_fetched_4c8f51" ON "placement_views_history" ("fetched_at");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_v_placeme_89e0f4" ON "placement_views_history" ("placement_id");
|
|
||||||
CREATE TABLE IF NOT EXISTS "subscription" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"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 "subscriber" ("id") ON DELETE CASCADE,
|
|
||||||
"target_channel_id" UUID NOT NULL REFERENCES "target_channel" ("id") ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
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");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_subscriptio_target__79117e" ON "subscription" ("target_channel_id");
|
|
||||||
COMMENT ON COLUMN "subscription"."status" IS 'ACTIVE: active\nUNSUBSCRIBED: unsubscribed';
|
|
||||||
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
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS "target_external_channel" (
|
|
||||||
"target_channel_id" UUID NOT NULL REFERENCES "target_channel" ("id") ON DELETE CASCADE,
|
|
||||||
"external_channel_id" UUID NOT NULL REFERENCES "external_channel" ("id") ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS "uidx_target_exte_target__ba70ec" ON "target_external_channel" ("target_channel_id", "external_channel_id");"""
|
|
||||||
|
|
||||||
|
|
||||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
|
||||||
return """
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
MODELS_STATE = (
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
309
migrations/models/0_20251214122113_init.py
Normal file
309
migrations/models/0_20251214122113_init.py
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS "telegram_state" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"telegram_id" BIGINT NOT NULL UNIQUE,
|
||||||
|
"state" VARCHAR(26) NOT NULL,
|
||||||
|
"context" JSONB NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_telegram_st_telegra_750503" ON "telegram_state" ("telegram_id");
|
||||||
|
COMMENT ON COLUMN "telegram_state"."state" IS 'CREATIVE_WAITING_WORKSPACE: creative_waiting_workspace\nCREATIVE_WAITING_CHANNEL: creative_waiting_channel\nCREATIVE_WAITING_NAME: creative_waiting_name\nCREATIVE_WAITING_TEXT: creative_waiting_text\nPROJECT_WAITING_WORKSPACE: project_waiting_workspace';
|
||||||
|
CREATE TABLE IF NOT EXISTS "user" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"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" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"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 TABLE IF NOT EXISTS "workspace" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"name" VARCHAR(255) NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "channel" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"telegram_id" BIGINT UNIQUE,
|
||||||
|
"username" VARCHAR(255),
|
||||||
|
"title" VARCHAR(255),
|
||||||
|
"verification_status" VARCHAR(10) NOT NULL DEFAULT 'unverified',
|
||||||
|
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
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 INDEX IF NOT EXISTS "idx_channel_workspa_4a6a23" ON "channel" ("workspace_id");
|
||||||
|
COMMENT ON COLUMN "channel"."verification_status" IS 'UNVERIFIED: unverified\nVERIFIED: verified\nFAILED: failed';
|
||||||
|
CREATE TABLE IF NOT EXISTS "project" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
|
"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';
|
||||||
|
CREATE TABLE IF NOT EXISTS "creative" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"name" VARCHAR(255) NOT NULL,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
|
"placements_count" INT NOT NULL DEFAULT 0,
|
||||||
|
"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 "placement" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"placement_date" TIMESTAMPTZ NOT NULL,
|
||||||
|
"cost" DOUBLE PRECISION,
|
||||||
|
"comment" TEXT,
|
||||||
|
"ad_post_url" VARCHAR(512),
|
||||||
|
"invite_link_type" VARCHAR(8) NOT NULL,
|
||||||
|
"invite_link" VARCHAR(512) NOT NULL,
|
||||||
|
"external_channel_message_id" INT,
|
||||||
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
|
"subscriptions_count" INT NOT NULL DEFAULT 0,
|
||||||
|
"views_count" INT,
|
||||||
|
"views_availability" VARCHAR(11) NOT NULL DEFAULT 'unknown',
|
||||||
|
"last_views_fetch_at" TIMESTAMPTZ,
|
||||||
|
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
|
||||||
|
"placement_channel_id" UUID NOT NULL REFERENCES "channel" ("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_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_project_2c6aa4" ON "placement" ("project_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_placement_workspa_632b20" ON "placement" ("workspace_id");
|
||||||
|
COMMENT ON COLUMN "placement"."invite_link_type" IS 'PUBLIC: public\nAPPROVAL: approval';
|
||||||
|
COMMENT ON COLUMN "placement"."status" IS 'PENDING: pending\nACTIVE: active\nDELETED: deleted\nARCHIVED: archived';
|
||||||
|
COMMENT ON COLUMN "placement"."views_availability" IS 'UNKNOWN: unknown\nAVAILABLE: available\nUNAVAILABLE: unavailable\nMANUAL: manual';
|
||||||
|
CREATE TABLE IF NOT EXISTS "placement_views_history" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"views_count" INT NOT NULL,
|
||||||
|
"fetched_at" TIMESTAMPTZ NOT NULL,
|
||||||
|
"placement_id" UUID NOT NULL REFERENCES "placement" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_placement_v_fetched_4c8f51" ON "placement_views_history" ("fetched_at");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_placement_v_placeme_89e0f4" ON "placement_views_history" ("placement_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "purchase_plan" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"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_purchase_pl_project_ceb049" ON "purchase_plan" ("project_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_purchase_pl_workspa_e2459d" ON "purchase_plan" ("workspace_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_purchase_pl_project_a3c246" ON "purchase_plan" ("project_id", "status");
|
||||||
|
COMMENT ON COLUMN "purchase_plan"."status" IS 'ACTIVE: active\nARCHIVED: archived';
|
||||||
|
CREATE TABLE IF NOT EXISTS "purchase_plan_channel" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"status" VARCHAR(11) NOT NULL DEFAULT 'planned',
|
||||||
|
"planned_cost" DOUBLE PRECISION,
|
||||||
|
"comment" TEXT,
|
||||||
|
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
|
||||||
|
"purchase_plan_id" UUID NOT NULL REFERENCES "purchase_plan" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_purchase_pl_purchas_e3a717" UNIQUE ("purchase_plan_id", "channel_id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_purchase_pl_channel_4cfc1c" ON "purchase_plan_channel" ("channel_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_purchase_pl_purchas_d20693" ON "purchase_plan_channel" ("purchase_plan_id");
|
||||||
|
COMMENT ON COLUMN "purchase_plan_channel"."status" IS 'PLANNED: planned\nAPPROVED: approved\nREJECTED: rejected\nIN_PROGRESS: in_progress\nCOMPLETED: completed';
|
||||||
|
CREATE TABLE IF NOT EXISTS "subscription" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"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
|
||||||
|
);
|
||||||
|
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" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"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" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"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\nREMOVED: removed';
|
||||||
|
CREATE TABLE IF NOT EXISTS "workspace_user_permission" (
|
||||||
|
"id" SERIAL 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(17) 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 'MANAGE_PROJECTS: manage_projects\nMANAGE_PLACEMENTS: manage_placements\nVIEW_ANALYTICS: view_analytics';
|
||||||
|
CREATE TABLE IF NOT EXISTS "workspace_user_permission_scope" (
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"permission" VARCHAR(17) NOT NULL,
|
||||||
|
"scope_type" VARCHAR(9) NOT NULL,
|
||||||
|
"scope_id" UUID NOT NULL,
|
||||||
|
"workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_workspace_u_workspa_488ed3" UNIQUE ("workspace_user_id", "permission", "scope_type", "scope_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 'MANAGE_PROJECTS: manage_projects\nMANAGE_PLACEMENTS: manage_placements\nVIEW_ANALYTICS: view_analytics';
|
||||||
|
COMMENT ON COLUMN "workspace_user_permission_scope"."scope_type" IS 'WORKSPACE: workspace\nPROJECT: project';
|
||||||
|
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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'
|
||||||
|
)
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
from tortoise import BaseDBAsyncClient
|
|
||||||
|
|
||||||
RUN_IN_TRANSACTION = True
|
|
||||||
|
|
||||||
|
|
||||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
|
||||||
return """
|
|
||||||
DROP INDEX IF EXISTS "uid_external_ch_telegra_13cca5";
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS "uid_external_ch_user_id_289e6a" ON "external_channel" ("user_id", "telegram_id");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_external_ch_telegra_13cca5" ON "external_channel" ("telegram_id");"""
|
|
||||||
|
|
||||||
|
|
||||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
|
||||||
return """
|
|
||||||
DROP INDEX IF EXISTS "idx_external_ch_telegra_13cca5";
|
|
||||||
DROP INDEX IF EXISTS "uid_external_ch_user_id_289e6a";
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS "uid_external_ch_telegra_13cca5" ON "external_channel" ("telegram_id");"""
|
|
||||||
|
|
||||||
|
|
||||||
MODELS_STATE = (
|
|
||||||
'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'
|
|
||||||
'8q//AeAg1wo='
|
|
||||||
)
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
from tortoise import BaseDBAsyncClient
|
|
||||||
|
|
||||||
RUN_IN_TRANSACTION = True
|
|
||||||
|
|
||||||
|
|
||||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
|
||||||
return """
|
|
||||||
ALTER TABLE "target_channel" DROP CONSTRAINT IF EXISTS "fk_target_c_user_f3145593";
|
|
||||||
ALTER TABLE "placement" DROP CONSTRAINT IF EXISTS "fk_placemen_user_004a0a62";
|
|
||||||
DROP INDEX IF EXISTS "uid_external_ch_user_id_289e6a";
|
|
||||||
ALTER TABLE "external_channel" DROP CONSTRAINT IF EXISTS "fk_external_user_23b1e9dc";
|
|
||||||
ALTER TABLE "creative" DROP CONSTRAINT IF EXISTS "fk_creative_user_7868b532";
|
|
||||||
CREATE TABLE IF NOT EXISTS "workspace" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"name" VARCHAR(255) NOT NULL
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS "workspace_user" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"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");
|
|
||||||
ALTER TABLE "creative" RENAME COLUMN "user_id" TO "workspace_id";
|
|
||||||
ALTER TABLE "external_channel" RENAME COLUMN "user_id" TO "workspace_id";
|
|
||||||
ALTER TABLE "placement" RENAME COLUMN "user_id" TO "workspace_id";
|
|
||||||
ALTER TABLE "target_channel" RENAME COLUMN "user_id" TO "workspace_id";
|
|
||||||
ALTER TABLE "telegram_state" ALTER COLUMN "state" TYPE VARCHAR(26) USING "state"::VARCHAR(26);
|
|
||||||
COMMENT ON COLUMN "telegram_state"."state" IS 'CREATIVE_WAITING_WORKSPACE: creative_waiting_workspace
|
|
||||||
CREATIVE_WAITING_CHANNEL: creative_waiting_channel
|
|
||||||
CREATIVE_WAITING_NAME: creative_waiting_name
|
|
||||||
CREATIVE_WAITING_TEXT: creative_waiting_text';
|
|
||||||
ALTER TABLE "creative" ADD CONSTRAINT "fk_creative_workspac_ac3ef81f" FOREIGN KEY ("workspace_id") REFERENCES "workspace" ("id") ON DELETE CASCADE;
|
|
||||||
ALTER TABLE "external_channel" ADD CONSTRAINT "fk_external_workspac_5266308f" FOREIGN KEY ("workspace_id") REFERENCES "workspace" ("id") ON DELETE CASCADE;
|
|
||||||
ALTER TABLE "placement" ADD CONSTRAINT "fk_placemen_workspac_c3bc4808" FOREIGN KEY ("workspace_id") REFERENCES "workspace" ("id") ON DELETE CASCADE;
|
|
||||||
ALTER TABLE "target_channel" ADD CONSTRAINT "fk_target_c_workspac_8a2fd2e3" FOREIGN KEY ("workspace_id") REFERENCES "workspace" ("id") ON DELETE CASCADE;"""
|
|
||||||
|
|
||||||
|
|
||||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
|
||||||
return """
|
|
||||||
ALTER TABLE "external_channel" DROP CONSTRAINT IF EXISTS "fk_external_workspac_5266308f";
|
|
||||||
ALTER TABLE "target_channel" DROP CONSTRAINT IF EXISTS "fk_target_c_workspac_8a2fd2e3";
|
|
||||||
ALTER TABLE "placement" DROP CONSTRAINT IF EXISTS "fk_placemen_workspac_c3bc4808";
|
|
||||||
ALTER TABLE "creative" DROP CONSTRAINT IF EXISTS "fk_creative_workspac_ac3ef81f";
|
|
||||||
ALTER TABLE "creative" RENAME COLUMN "workspace_id" TO "user_id";
|
|
||||||
ALTER TABLE "placement" RENAME COLUMN "workspace_id" TO "user_id";
|
|
||||||
ALTER TABLE "target_channel" RENAME COLUMN "workspace_id" TO "user_id";
|
|
||||||
COMMENT ON COLUMN "telegram_state"."state" IS 'CREATIVE_WAITING_CHANNEL: creative_waiting_channel
|
|
||||||
CREATIVE_WAITING_NAME: creative_waiting_name
|
|
||||||
CREATIVE_WAITING_TEXT: creative_waiting_text';
|
|
||||||
ALTER TABLE "telegram_state" ALTER COLUMN "state" TYPE VARCHAR(24) USING "state"::VARCHAR(24);
|
|
||||||
ALTER TABLE "external_channel" RENAME COLUMN "workspace_id" TO "user_id";
|
|
||||||
DROP TABLE IF EXISTS "workspace_user";
|
|
||||||
DROP TABLE IF EXISTS "workspace";
|
|
||||||
ALTER TABLE "creative" ADD CONSTRAINT "fk_creative_user_7868b532" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE;
|
|
||||||
ALTER TABLE "placement" ADD CONSTRAINT "fk_placemen_user_004a0a62" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE;
|
|
||||||
ALTER TABLE "target_channel" ADD CONSTRAINT "fk_target_c_user_f3145593" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE;
|
|
||||||
ALTER TABLE "external_channel" ADD CONSTRAINT "fk_external_user_23b1e9dc" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE;
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS "uid_external_ch_user_id_289e6a" ON "external_channel" ("user_id", "telegram_id");"""
|
|
||||||
|
|
||||||
|
|
||||||
MODELS_STATE = (
|
|
||||||
"eJztXWtv4jgU/SuIT7NSd9TS51arlVLKzLBDoSrQrnZmFLngQtTgsHn0odH897VDno4DJC"
|
|
||||||
"SQhPtlHo5vHufa5p7ja/tnfaaNsWp8bOoYmcoLrl/WftYJmrF/RK4d1OpoPvevsAITPap2"
|
|
||||||
"5VGw1qNh6mhk0vInpBqYFo2xMdKVualohJYSS1VZoTaiFRUy8YssovxnYdnUJticYp1e+P"
|
|
||||||
"aDFitkjN+w4f53/iw/KVgdh15XGbNn2+Wy+T63y4bD9vUnuyZ73KM80lRrRvza83dzqhGv"
|
|
||||||
"umUp44/Mhl2bYIJ1ZOJx4DPYWzpf7BYt3pgWmLqFvVcd+wVj/IQslYFR//PJIiOGQc1+Ev"
|
|
||||||
"vj5K96AnhGGmHQKsRkWPz8tfgq/5vt0jp7VPOLdPfh+Ow3+ys1w5zo9kUbkfov2xCZaGFq"
|
|
||||||
"4+oDabsSj2VkRgG9pldMZYbFoIYtOXDHjulH9x9pQHYLfJT9FubC7MKXDtM6/YZxj6jvjg"
|
|
||||||
"eXYDxo37T6A+nmln3JzDD+U22IpEGLXWnYpe9c6YeFSzTaPxYdx7tJ7aE9+FJj/6392+u2"
|
|
||||||
"eMd59Qb/1tk7IcvUZKK9ymgcaGxuqQsMrek71pqPUzo2bAmO3aljnZf3/UoHY5zOr2HLDP"
|
|
||||||
"zqvO0W3VoSN7qfvbSD2n9HXNicIl3sPrc+5zgKT0G74Ay9ySomE3NK/9s4PV3ivHvpzv4N"
|
|
||||||
"o7U4j3SdS43FtV8hCE38JugFA1oqhtCtXxYIlzX31j+DUEt3gfpwI/3zW6i1d3rdz271AL"
|
|
||||||
"DNTu+Kw9MwkWkZ4kbZItbMRrVNXxGREY6g61tvD986jTydQDSMcV1qDtr3rcvaosJ3wtCh"
|
|
||||||
"Bde0RB9NadGieSRsxhdrNOKL2CZ8wTfguYpGeIbpoyiOFhE05jaJacsiUw53ZVGaB+6HG4"
|
|
||||||
"wLE/aQ3xtHJ+cnF8dnJxe0iv0iXsn5EpDb3QE/CCCd8gd5NEWEYFVOxgyExlkShSiYEaaQ"
|
|
||||||
"2xixkhb4IL5q+rMxpy0qIX683Z5Ax3jp07OQUHmIRGH8pOlYmZCv+D0ykHK4OWz8IXivos"
|
|
||||||
"Lnl/qP0NGrR9ojbYR+5yIUtYGW+k3pulVf0qUzAHJg37Dp36+0YAoHLDGirJE+otHzK9LH"
|
|
||||||
"ckxr9X9FoihfObafvt5hFdkfFAvwrXufQgdSEXRtjLSGFsAmhFr00qwx40sQQRP7rdmz2Z"
|
|
||||||
"McTFpvJtYJUt1mJ5Db+CoHy1Q37FQO9gtQ30B9i4ILIg2ob+BYUN9AfYvtoCZ1x0RHMyHf"
|
|
||||||
"uVImsbybM8yLcmccXjqk+49G4/j4vHF4fHZxenJ+fnpx6LHv6KVlNPyq/Zkx8ZD7BNRcMd"
|
|
||||||
"VEGqdnUBaFbgsip2WwqC+ZVhy0SQXl6sEl6waaP5DBN4tgGS8ac2Y5wVl66dh6ZO/5iPXk"
|
|
||||||
"UqbQNtXAun2cs1YzQYgDIW63QhzIRrnLRnFKpwDNG0TeBxr7c1tS52pxJbfWan+CzGlkkQ"
|
|
||||||
"/SWbOiTMqtx+tiCxQ13XbFM36P4uy0f89fTiX+Rk41c6pr1mQauI1IiRN2JlouB/H3Gk+s"
|
|
||||||
"aNjRJgoZaM+Y1AV6YeDqwTKpUGX1ZNOrCCohqIQgJoFKCI4FlRBUwgQqofsLuraA5RpkI2"
|
|
||||||
"DlHYttQXXBb3OFeiRFXwhblnOMq1JnsIx0P1UGjGcFcqGeUFgKmOSrKW3Rb+lFJYZGBnrS"
|
|
||||||
"0LlNYVFbqSUFmkVSGSnPnBtfUBKw55DaFE+e56FqQJ2BOpc1+qgMwwLqXFHHAnWuaqjp/Y"
|
|
||||||
"rKDPKkzoxal7OjVsmjI3o/QeyraihmXt814Fz3xCzK1v+ue8OrTqt2e9dqtvvtXjfsHvsi"
|
|
||||||
"K6IFyiIQvmtJHU4KGWkzN6xcN/kkYAKJJ8LEEzSWWUOXLV2wUCReqOPMSgJuWK87PWqsod"
|
|
||||||
"fRWrF6nX0tjKdCXmgLllWFPC/gEIK6ejWo6D47zuqr39Ju2m5e1ubWo6qMvhPp9vaudy91"
|
|
||||||
"LmuUDeraC7Lbwo7XhAaAS9KgObNyJlDm0qIj0/ozbBhoIs6wik1SW3GXPU1Xq9KK8dtW97"
|
|
||||||
"rd/UyHB0zGFEQ6PnBryK9bndaALSF3uEZBV5U7+ZT2h6XOxuSt93Jt+YuCX5MjyFnt6dCw"
|
|
||||||
"QAG9IIV+k0LD4ve0w4T4TlscMizyTEkREYwZw+7Xbu+he1lzqtAR4V5qdyRKB+iQsHhjlY"
|
|
||||||
"4cw26g3CKBKzdSd8gikBkiVrr44+hojeHj6Ch2/GCXwq5TEY2MF6g/YXM0TaHGxNwCZJld"
|
|
||||||
"k3hn676Es4Cc2Z5kly+NIpMBGGO+h0DCfi2wX0uBZvRhr5Es9hpZNlJmgKlgn4zSohrzS7"
|
|
||||||
"Aa1+CuuxviGdzmt7RAcjHJagBhRRgPYXYrwhah/lQxTE1XcFbLwu7ZXb/YN30vpn4Zu0Qs"
|
|
||||||
"VpDZDJp+4FYlQ2QreV+hFrMsB4xvWmvkg8nBRr4wguwwyA4ra25CZZKIIDusoo6F7LCqyp"
|
|
||||||
"C7m1jZRX/MeGbFFtRT9YKw5XZGt+2JQKXrBX5kmSyk4+1AxQut2diQ2me8KcmOqD3fRoq0"
|
|
||||||
"SqfvbfhUF1C0wNWDZbzMCNcDKgZUDCJ2oGLgWKBiQMXKuhNu7jte7GQj3ILu4ZpvMncu24"
|
|
||||||
"k8KbphykmxDFsBmqFEv6Rghoz2G8sII11nqhRmA/OeDQzBEs8wPdhWcky/JrBMYJlARoBl"
|
|
||||||
"gmOBZQLLXJ9lFntVa67bKuayqLVKCy/5ZZbDbn941W/eta/YwkqLeEJ/qsWVa6G/BPwo9s"
|
|
||||||
"E3SvOLEzWH4QlmYss0Eys++SUhdhHDPQQPFkNBLsDG8GWUCyDu1BmAGJ7QLy2KkfEKDrLe"
|
|
||||||
"zuKy3SSphKEWqIgRX8TLiNF2AEIiCInZBP6gN4GQCI4FIXFPmDqkq8C5zYXNByhBzk/5Th"
|
|
||||||
"CqstTd7rolCnHLCrmnIGw4k5U8BbtQHOS2C4W7JciGaVXZbo+ytZQq4YTGnp7QDNtv5HZm"
|
|
||||||
"tfi85RCkiU+tzmCDp6KdWy34JP7katHp3+Fzq2M2i+JPrhaKqRmeW72gYcvlUode9Wm8he"
|
|
||||||
"siuTRU4WCpXOpSNcOrC3IpyKWgqoFcCo4FuRTkUpBLiySXelFKWn2qCNJpvXnXkpgUJT9I"
|
|
||||||
"7UG7+1l+6N197d9KzdZlzdtq8xUpJnWA7FH27yRiR+OEbrfVEVg5YafApivdiB7DEBbUZi"
|
|
||||||
"dDCWqbNL5NI5U1ztYRLfleHNAsz3i1jD7OfZlwm/i73+vGBDq+CdcShoR66NtYGZkHNVUx"
|
|
||||||
"zB+5yZZ+MPloKSrF1PjIHptTPMmwCA2jkePA+JO/uPGR3eAq2Sq8PBNG7EOdBcTHPew5nu"
|
|
||||||
"+4p0oDywGWA8HwroNhYDkVdSywHGA5wHLSspyC5jOUJDMk1U4RqjZRKGLaM9503qrD7jRg"
|
|
||||||
"Nyrmb8xa83j+PDFrVxsi4k2du+F5iUDJk8X4KQUCKhPKN4jnM6EUByA1QGog9t117Aukpq"
|
|
||||||
"KOBVJTVVKTNNjeKNCuTg52qkgbEgcTZHclQSSXk/sgo3LHWIgS11IDksOaZqCnlaencbNt"
|
|
||||||
"EeDWoKlyPhNw3yKZ7ew57J8/gMamCeGAxu472wEaW1HHAo2tKo11f/MS/I4FTPZkvSAstU"
|
|
||||||
"wNHSy1zHypJd99M4AvOxqzI+QCY1KRdqmSsK6MpnUBEXKuHCxjQMivU5hZuthcjXVTNBzv"
|
|
||||||
"FSBDY8MTP+PZzAvWDUc+WFcdDpjsuUAcHN1Y10gAolO9nAAeHR6uASCtFb8VMrsmyHQXbb"
|
|
||||||
"W5ItNdeILv1jLdc4tVqpHT/ut/5sP+/g=="
|
|
||||||
)
|
|
||||||
@@ -19,6 +19,9 @@ class Postgres(DatabaseBase):
|
|||||||
async def create_user(self, user: domain.User) -> None:
|
async def create_user(self, user: domain.User) -> None:
|
||||||
await user.save()
|
await user.save()
|
||||||
|
|
||||||
|
async def update_user(self, user: domain.User) -> None:
|
||||||
|
await user.save()
|
||||||
|
|
||||||
async def create_workspace(self, workspace: domain.Workspace) -> None:
|
async def create_workspace(self, workspace: domain.Workspace) -> None:
|
||||||
await workspace.save()
|
await workspace.save()
|
||||||
|
|
||||||
@@ -28,10 +31,12 @@ class Postgres(DatabaseBase):
|
|||||||
async def delete_workspace(self, workspace_id: uuid.UUID) -> None:
|
async def delete_workspace(self, workspace_id: uuid.UUID) -> None:
|
||||||
await domain.Workspace.filter(id=workspace_id).delete()
|
await domain.Workspace.filter(id=workspace_id).delete()
|
||||||
|
|
||||||
async def add_user_to_workspace(
|
async def add_user_to_workspace(self, workspace_id: uuid.UUID, user_id: uuid.UUID) -> None:
|
||||||
self, workspace_id: uuid.UUID, user_id: uuid.UUID, *, is_owner: bool = False
|
await domain.WorkspaceUser.create(
|
||||||
) -> None:
|
workspace_id=workspace_id,
|
||||||
await domain.WorkspaceUser.create(workspace_id=workspace_id, user_id=user_id, is_owner=is_owner)
|
user_id=user_id,
|
||||||
|
status=domain.WorkspaceUserStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
async def get_user_workspaces(self, user_id: uuid.UUID) -> list[domain.WorkspaceUser]:
|
async def get_user_workspaces(self, user_id: uuid.UUID) -> list[domain.WorkspaceUser]:
|
||||||
return (
|
return (
|
||||||
@@ -71,9 +76,6 @@ class Postgres(DatabaseBase):
|
|||||||
async def create_login_token(self, login_token: domain.LoginToken) -> None:
|
async def create_login_token(self, login_token: domain.LoginToken) -> None:
|
||||||
await login_token.save()
|
await login_token.save()
|
||||||
|
|
||||||
async def create_external_channel(self, channel: domain.ExternalChannel) -> None:
|
|
||||||
await channel.save()
|
|
||||||
|
|
||||||
async def create_creative(self, creative: domain.Creative) -> None:
|
async def create_creative(self, creative: domain.Creative) -> None:
|
||||||
await creative.save()
|
await creative.save()
|
||||||
|
|
||||||
@@ -94,120 +96,165 @@ class Postgres(DatabaseBase):
|
|||||||
if updated == 0:
|
if updated == 0:
|
||||||
raise domain.LoginTokenAlreadyUsed() # либо нет токена, либо он уже использован
|
raise domain.LoginTokenAlreadyUsed() # либо нет токена, либо он уже использован
|
||||||
|
|
||||||
async def get_target_channel(
|
async def get_channel(
|
||||||
self, workspace_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
self,
|
||||||
) -> domain.TargetChannel | None:
|
workspace_id: uuid.UUID,
|
||||||
if not channel_id and not telegram_id:
|
channel_id: uuid.UUID | None = None,
|
||||||
raise ValueError('Either channel_id or telegram_id must be provided')
|
telegram_id: int | None = None,
|
||||||
|
username: str | None = None,
|
||||||
q = domain.TargetChannel.filter(workspace_id=workspace_id)
|
) -> domain.Channel | None:
|
||||||
if channel_id:
|
query = domain.Channel.filter(workspace_id=workspace_id)
|
||||||
q = q.filter(id=channel_id)
|
|
||||||
if telegram_id:
|
|
||||||
q = q.filter(telegram_id=telegram_id)
|
|
||||||
|
|
||||||
return await q.first()
|
|
||||||
|
|
||||||
async def get_target_channel_for_user_by_telegram(
|
|
||||||
self, user_id: uuid.UUID, telegram_id: int
|
|
||||||
) -> domain.TargetChannel | None:
|
|
||||||
return (
|
|
||||||
await domain.TargetChannel.filter(telegram_id=telegram_id, workspace__workspace_users__user_id=user_id)
|
|
||||||
.prefetch_related('workspace')
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
async def upsert_target_channel(self, channel: domain.TargetChannel) -> None:
|
|
||||||
existing = await self.get_target_channel(workspace_id=channel.workspace_id, telegram_id=channel.telegram_id)
|
|
||||||
|
|
||||||
if not existing:
|
|
||||||
await channel.save()
|
|
||||||
return
|
|
||||||
|
|
||||||
existing.title = channel.title
|
|
||||||
existing.username = channel.username
|
|
||||||
existing.workspace_id = channel.workspace_id
|
|
||||||
existing.status = channel.status
|
|
||||||
existing.deleted_at = None
|
|
||||||
existing.updated_at = timezone.now()
|
|
||||||
await existing.save()
|
|
||||||
|
|
||||||
async def get_workspace_target_channels(self, workspace_id: uuid.UUID) -> list[domain.TargetChannel]:
|
|
||||||
return await domain.TargetChannel.filter(
|
|
||||||
workspace_id=workspace_id, status=domain.TargetChannelStatus.ACTIVE
|
|
||||||
).all()
|
|
||||||
|
|
||||||
async def update_target_channel(self, channel: domain.TargetChannel) -> None:
|
|
||||||
await channel.save()
|
|
||||||
|
|
||||||
async def get_external_channel(
|
|
||||||
self, workspace_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
|
||||||
) -> domain.ExternalChannel | None:
|
|
||||||
query = domain.ExternalChannel.filter(workspace_id=workspace_id)
|
|
||||||
if channel_id:
|
if channel_id:
|
||||||
query = query.filter(id=channel_id)
|
query = query.filter(id=channel_id)
|
||||||
if telegram_id:
|
if telegram_id:
|
||||||
query = query.filter(telegram_id=telegram_id)
|
query = query.filter(telegram_id=telegram_id)
|
||||||
|
if username:
|
||||||
|
query = query.filter(username=username)
|
||||||
return await query.first()
|
return await query.first()
|
||||||
|
|
||||||
async def get_external_channels_for_target(self, target_channel_id: uuid.UUID) -> list[domain.ExternalChannel]:
|
async def get_workspace_channels(self, workspace_id: uuid.UUID) -> list[domain.Channel]:
|
||||||
return await domain.ExternalChannel.filter(target_channels__id=target_channel_id).all()
|
return await domain.Channel.filter(workspace_id=workspace_id).all()
|
||||||
|
|
||||||
async def get_workspace_external_channels(self, workspace_id: uuid.UUID) -> list[domain.ExternalChannel]:
|
async def create_channel(self, channel: domain.Channel) -> None:
|
||||||
return await domain.ExternalChannel.filter(workspace_id=workspace_id).all()
|
|
||||||
|
|
||||||
async def add_external_channel_to_targets(
|
|
||||||
self, external_channel: domain.ExternalChannel, target_channel_ids: list[uuid.UUID]
|
|
||||||
) -> None:
|
|
||||||
target_channels = await domain.TargetChannel.filter(id__in=target_channel_ids).all()
|
|
||||||
await external_channel.target_channels.add(*target_channels)
|
|
||||||
|
|
||||||
async def remove_external_channel_from_target(
|
|
||||||
self, external_channel_id: uuid.UUID, target_channel_id: uuid.UUID
|
|
||||||
) -> None:
|
|
||||||
external_channel = await domain.ExternalChannel.get_or_none(id=external_channel_id)
|
|
||||||
|
|
||||||
if not external_channel:
|
|
||||||
raise ValueError(f'ExternalChannel {external_channel_id} not found')
|
|
||||||
|
|
||||||
target_channel = await domain.TargetChannel.get_or_none(id=target_channel_id)
|
|
||||||
if target_channel:
|
|
||||||
await external_channel.target_channels.remove(target_channel)
|
|
||||||
|
|
||||||
async def delete_external_channel(self, channel_id: uuid.UUID) -> None:
|
|
||||||
await domain.ExternalChannel.filter(id=channel_id).delete()
|
|
||||||
|
|
||||||
async def update_external_channel(self, channel: domain.ExternalChannel) -> None:
|
|
||||||
await channel.save()
|
await channel.save()
|
||||||
|
|
||||||
|
async def update_channel(self, channel: domain.Channel) -> None:
|
||||||
|
await channel.save()
|
||||||
|
|
||||||
|
async def delete_channel(self, channel_id: uuid.UUID) -> None:
|
||||||
|
await domain.Channel.filter(id=channel_id).delete()
|
||||||
|
|
||||||
|
async def search_channels(self, username_query: str | None = None) -> list[domain.Channel]:
|
||||||
|
query = domain.Channel.all()
|
||||||
|
|
||||||
|
if username_query:
|
||||||
|
# Частичный поиск (case-insensitive)
|
||||||
|
query = query.filter(username__icontains=username_query)
|
||||||
|
|
||||||
|
return await query.all()
|
||||||
|
|
||||||
|
async def get_project(
|
||||||
|
self, workspace_id: uuid.UUID, project_id: uuid.UUID | None = None, channel_id: uuid.UUID | None = None
|
||||||
|
) -> domain.Project | None:
|
||||||
|
query = domain.Project.filter(workspace_id=workspace_id).prefetch_related('channel')
|
||||||
|
if project_id:
|
||||||
|
query = query.filter(id=project_id)
|
||||||
|
if channel_id:
|
||||||
|
query = query.filter(channel_id=channel_id)
|
||||||
|
return await query.first()
|
||||||
|
|
||||||
|
async def get_project_for_user_by_telegram(
|
||||||
|
self, user_id: uuid.UUID, channel_telegram_id: int
|
||||||
|
) -> domain.Project | None:
|
||||||
|
return (
|
||||||
|
await domain.Project.filter(
|
||||||
|
channel__telegram_id=channel_telegram_id, workspace__workspace_users__user_id=user_id
|
||||||
|
)
|
||||||
|
.prefetch_related('channel')
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_project_by_channel_telegram(self, channel_telegram_id: int) -> domain.Project | None:
|
||||||
|
return await domain.Project.filter(channel__telegram_id=channel_telegram_id).prefetch_related('channel').first()
|
||||||
|
|
||||||
|
async def create_project(self, project: domain.Project) -> None:
|
||||||
|
await project.save()
|
||||||
|
|
||||||
|
async def update_project(self, project: domain.Project) -> None:
|
||||||
|
await project.save()
|
||||||
|
|
||||||
|
async def get_workspace_projects(self, workspace_id: uuid.UUID) -> list[domain.Project]:
|
||||||
|
return (
|
||||||
|
await domain.Project.filter(workspace_id=workspace_id)
|
||||||
|
.prefetch_related('channel')
|
||||||
|
.order_by('created_at')
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_active_purchase_plan(self, project_id: uuid.UUID) -> domain.PurchasePlan | None:
|
||||||
|
return await domain.PurchasePlan.get_or_none(project_id=project_id, status=domain.PurchasePlanStatus.ACTIVE)
|
||||||
|
|
||||||
|
async def get_purchase_plan(self, workspace_id: uuid.UUID, plan_id: uuid.UUID) -> domain.PurchasePlan | None:
|
||||||
|
return await domain.PurchasePlan.get_or_none(id=plan_id, workspace_id=workspace_id)
|
||||||
|
|
||||||
|
async def create_purchase_plan(self, plan: domain.PurchasePlan) -> None:
|
||||||
|
await plan.save()
|
||||||
|
|
||||||
|
async def get_purchase_plan_channels(self, plan_id: uuid.UUID) -> list[domain.PurchasePlanChannel]:
|
||||||
|
return await domain.PurchasePlanChannel.filter(purchase_plan_id=plan_id).prefetch_related('channel').all()
|
||||||
|
|
||||||
|
async def get_purchase_plan_channel(
|
||||||
|
self, plan_channel_id: uuid.UUID, plan_id: uuid.UUID
|
||||||
|
) -> domain.PurchasePlanChannel | None:
|
||||||
|
return (
|
||||||
|
await domain.PurchasePlanChannel.filter(id=plan_channel_id, purchase_plan_id=plan_id)
|
||||||
|
.prefetch_related('channel')
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def add_channel_to_purchase_plan(
|
||||||
|
self,
|
||||||
|
plan_id: uuid.UUID,
|
||||||
|
channel_id: uuid.UUID,
|
||||||
|
*,
|
||||||
|
status: domain.PurchasePlanChannelStatus | None = None,
|
||||||
|
planned_cost: float | None = None,
|
||||||
|
comment: str | None = None,
|
||||||
|
) -> domain.PurchasePlanChannel:
|
||||||
|
defaults: dict[str, object | None] = {}
|
||||||
|
if status is not None:
|
||||||
|
defaults['status'] = status
|
||||||
|
if planned_cost is not None:
|
||||||
|
defaults['planned_cost'] = planned_cost
|
||||||
|
if comment is not None:
|
||||||
|
defaults['comment'] = comment
|
||||||
|
|
||||||
|
plan_channel, created = await domain.PurchasePlanChannel.get_or_create(
|
||||||
|
purchase_plan_id=plan_id,
|
||||||
|
channel_id=channel_id,
|
||||||
|
defaults=defaults,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not created and defaults:
|
||||||
|
for field, value in defaults.items():
|
||||||
|
setattr(plan_channel, field, value)
|
||||||
|
await plan_channel.save()
|
||||||
|
|
||||||
|
return plan_channel
|
||||||
|
|
||||||
|
async def update_purchase_plan_channel(self, plan_channel: domain.PurchasePlanChannel) -> None:
|
||||||
|
await plan_channel.save()
|
||||||
|
|
||||||
|
async def remove_channel_from_purchase_plan(self, plan_id: uuid.UUID, channel_id: uuid.UUID) -> None:
|
||||||
|
await domain.PurchasePlanChannel.filter(purchase_plan_id=plan_id, channel_id=channel_id).delete()
|
||||||
|
|
||||||
async def get_creative(self, workspace_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None:
|
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(
|
return await domain.Creative.get_or_none(id=creative_id, workspace_id=workspace_id).prefetch_related(
|
||||||
'target_channel'
|
'project', 'project__channel'
|
||||||
)
|
)
|
||||||
|
|
||||||
async def update_creative(self, creative: domain.Creative) -> None:
|
async def update_creative(self, creative: domain.Creative) -> None:
|
||||||
await creative.save()
|
await creative.save()
|
||||||
|
|
||||||
async def get_workspace_creatives(
|
async def get_workspace_creatives(
|
||||||
self, workspace_id: uuid.UUID, target_channel_id: uuid.UUID | None = None, include_archived: bool = False
|
self, workspace_id: uuid.UUID, project_id: uuid.UUID | None = None, include_archived: bool = False
|
||||||
) -> list[domain.Creative]:
|
) -> list[domain.Creative]:
|
||||||
query = domain.Creative.filter(workspace_id=workspace_id)
|
query = domain.Creative.filter(workspace_id=workspace_id)
|
||||||
|
|
||||||
if target_channel_id:
|
if project_id:
|
||||||
query = query.filter(target_channel_id=target_channel_id)
|
query = query.filter(project_id=project_id)
|
||||||
|
|
||||||
if not include_archived:
|
if not include_archived:
|
||||||
query = query.filter(status=domain.CreativeStatus.ACTIVE)
|
query = query.filter(status=domain.CreativeStatus.ACTIVE)
|
||||||
|
|
||||||
return await query.prefetch_related('target_channel').order_by('-created_at').all()
|
return await query.prefetch_related('project', 'project__channel').order_by('-created_at').all()
|
||||||
|
|
||||||
async def delete_creative(self, creative_id: uuid.UUID) -> None:
|
async def delete_creative(self, creative_id: uuid.UUID) -> None:
|
||||||
await domain.Creative.filter(id=creative_id).delete()
|
await domain.Creative.filter(id=creative_id).delete()
|
||||||
|
|
||||||
async def get_placement(self, workspace_id: uuid.UUID, placement_id: uuid.UUID) -> domain.Placement | None:
|
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(
|
return await domain.Placement.get_or_none(id=placement_id, workspace_id=workspace_id).prefetch_related(
|
||||||
'target_channel', 'external_channel', 'creative'
|
'project', 'project__channel', 'placement_channel', 'creative'
|
||||||
)
|
)
|
||||||
|
|
||||||
async def create_placement(self, placement: domain.Placement) -> None:
|
async def create_placement(self, placement: domain.Placement) -> None:
|
||||||
@@ -219,17 +266,17 @@ class Postgres(DatabaseBase):
|
|||||||
async def get_workspace_placements(
|
async def get_workspace_placements(
|
||||||
self,
|
self,
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
target_channel_id: uuid.UUID | None = None,
|
project_id: uuid.UUID | None = None,
|
||||||
external_channel_id: uuid.UUID | None = None,
|
placement_channel_id: uuid.UUID | None = None,
|
||||||
creative_id: uuid.UUID | None = None,
|
creative_id: uuid.UUID | None = None,
|
||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
) -> list[domain.Placement]:
|
) -> list[domain.Placement]:
|
||||||
query = domain.Placement.filter(workspace_id=workspace_id)
|
query = domain.Placement.filter(workspace_id=workspace_id)
|
||||||
|
|
||||||
if target_channel_id:
|
if project_id:
|
||||||
query = query.filter(target_channel_id=target_channel_id)
|
query = query.filter(project_id=project_id)
|
||||||
if external_channel_id:
|
if placement_channel_id:
|
||||||
query = query.filter(external_channel_id=external_channel_id)
|
query = query.filter(placement_channel_id=placement_channel_id)
|
||||||
if creative_id:
|
if creative_id:
|
||||||
query = query.filter(creative_id=creative_id)
|
query = query.filter(creative_id=creative_id)
|
||||||
|
|
||||||
@@ -237,7 +284,7 @@ class Postgres(DatabaseBase):
|
|||||||
query = query.filter(status=domain.PlacementStatus.ACTIVE)
|
query = query.filter(status=domain.PlacementStatus.ACTIVE)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
await query.prefetch_related('target_channel', 'external_channel', 'creative')
|
await query.prefetch_related('project', 'project__channel', 'placement_channel', 'creative')
|
||||||
.order_by('-placement_date')
|
.order_by('-placement_date')
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
@@ -247,23 +294,9 @@ class Postgres(DatabaseBase):
|
|||||||
|
|
||||||
async def get_placement_by_invite_link(self, invite_link: str) -> domain.Placement | None:
|
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(
|
return await domain.Placement.get_or_none(invite_link=invite_link).prefetch_related(
|
||||||
'target_channel', 'external_channel', 'creative'
|
'project', 'project__channel', 'placement_channel', 'creative'
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_subscriber(self, telegram_id: int) -> domain.Subscriber | None:
|
|
||||||
return await domain.Subscriber.get_or_none(telegram_id=telegram_id)
|
|
||||||
|
|
||||||
async def upsert_subscriber(self, subscriber: domain.Subscriber) -> None:
|
|
||||||
existing = await self.get_subscriber(subscriber.telegram_id)
|
|
||||||
if not existing:
|
|
||||||
await subscriber.save()
|
|
||||||
return
|
|
||||||
|
|
||||||
existing.username = subscriber.username
|
|
||||||
existing.first_name = subscriber.first_name
|
|
||||||
existing.last_name = subscriber.last_name
|
|
||||||
await existing.save()
|
|
||||||
|
|
||||||
async def create_subscription(self, subscription: domain.Subscription) -> None:
|
async def create_subscription(self, subscription: domain.Subscription) -> None:
|
||||||
await subscription.save()
|
await subscription.save()
|
||||||
|
|
||||||
@@ -275,26 +308,26 @@ class Postgres(DatabaseBase):
|
|||||||
async def update_subscription(self, subscription: domain.Subscription) -> None:
|
async def update_subscription(self, subscription: domain.Subscription) -> None:
|
||||||
await subscription.save()
|
await subscription.save()
|
||||||
|
|
||||||
async def get_active_subscriptions_by_subscriber_and_channel(
|
async def get_active_subscriptions_by_subscriber_and_project(
|
||||||
self, subscriber_id: uuid.UUID, channel_telegram_id: int
|
self, subscriber_id: uuid.UUID, project_id: uuid.UUID
|
||||||
) -> list[domain.Subscription]:
|
) -> list[domain.Subscription]:
|
||||||
return (
|
return (
|
||||||
await domain.Subscription.filter(
|
await domain.Subscription.filter(
|
||||||
subscriber_id=subscriber_id,
|
subscriber_id=subscriber_id,
|
||||||
target_channel__telegram_id=channel_telegram_id,
|
placement__project_id=project_id,
|
||||||
status=domain.SubscriptionStatus.ACTIVE,
|
status=domain.SubscriptionStatus.ACTIVE,
|
||||||
)
|
)
|
||||||
.prefetch_related('placement', 'subscriber')
|
.prefetch_related('placement', 'subscriber')
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_active_subscription_by_subscriber_and_channel(
|
async def get_active_subscription_by_subscriber_and_project(
|
||||||
self, subscriber_id: uuid.UUID, channel_telegram_id: int
|
self, subscriber_id: uuid.UUID, project_id: uuid.UUID
|
||||||
) -> domain.Subscription | None:
|
) -> domain.Subscription | None:
|
||||||
return (
|
return (
|
||||||
await domain.Subscription.filter(
|
await domain.Subscription.filter(
|
||||||
subscriber_id=subscriber_id,
|
subscriber_id=subscriber_id,
|
||||||
target_channel__telegram_id=channel_telegram_id,
|
placement__project_id=project_id,
|
||||||
status=domain.SubscriptionStatus.ACTIVE,
|
status=domain.SubscriptionStatus.ACTIVE,
|
||||||
)
|
)
|
||||||
.prefetch_related('placement', 'subscriber')
|
.prefetch_related('placement', 'subscriber')
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ from fastapi import APIRouter
|
|||||||
|
|
||||||
from src.controller.http.analytics import analytics_router
|
from src.controller.http.analytics import analytics_router
|
||||||
from src.controller.http.auth import auth_router
|
from src.controller.http.auth import auth_router
|
||||||
|
from src.controller.http.channels import channels_router
|
||||||
from src.controller.http.creatives import creatives_router
|
from src.controller.http.creatives import creatives_router
|
||||||
from src.controller.http.external_channels import external_channels_router
|
|
||||||
from src.controller.http.placements import placements_router
|
from src.controller.http.placements import placements_router
|
||||||
from src.controller.http.target_channels import target_channels_router
|
from src.controller.http.projects import projects_router
|
||||||
|
from src.controller.http.purchase_plans import purchase_plan_channels_router
|
||||||
from src.controller.http.views import views_router
|
from src.controller.http.views import views_router
|
||||||
from src.controller.http.workspaces import workspaces_router
|
from src.controller.http.workspaces import workspaces_router
|
||||||
|
|
||||||
@@ -14,8 +15,9 @@ api_router = APIRouter()
|
|||||||
# API v1 endpoints
|
# API v1 endpoints
|
||||||
api_v1_router = APIRouter(prefix='/api/v1')
|
api_v1_router = APIRouter(prefix='/api/v1')
|
||||||
api_v1_router.include_router(auth_router)
|
api_v1_router.include_router(auth_router)
|
||||||
api_v1_router.include_router(target_channels_router)
|
api_v1_router.include_router(channels_router)
|
||||||
api_v1_router.include_router(external_channels_router)
|
api_v1_router.include_router(projects_router)
|
||||||
|
api_v1_router.include_router(purchase_plan_channels_router)
|
||||||
api_v1_router.include_router(creatives_router)
|
api_v1_router.include_router(creatives_router)
|
||||||
api_v1_router.include_router(placements_router)
|
api_v1_router.include_router(placements_router)
|
||||||
api_v1_router.include_router(views_router)
|
api_v1_router.include_router(views_router)
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ analytics_router = APIRouter(prefix='/workspaces/{workspace_id}/analytics', tags
|
|||||||
async def get_placements_analytics(
|
async def get_placements_analytics(
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
project_id: uuid.UUID | None = None,
|
||||||
) -> dto.GetPlacementsAnalyticsOutput:
|
) -> dto.GetPlacementsAnalyticsOutput:
|
||||||
input = dto.GetPlacementsAnalyticsInput(
|
input = dto.GetPlacementsAnalyticsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
return await deps.get_usecase().get_placements_analytics(input)
|
return await deps.get_usecase().get_placements_analytics(input)
|
||||||
|
|
||||||
@@ -29,35 +29,35 @@ async def get_placements_analytics(
|
|||||||
async def get_creatives_analytics(
|
async def get_creatives_analytics(
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
project_id: uuid.UUID | None = None,
|
||||||
) -> dto.GetCreativesAnalyticsOutput:
|
) -> dto.GetCreativesAnalyticsOutput:
|
||||||
input = dto.GetCreativesAnalyticsInput(
|
input = dto.GetCreativesAnalyticsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
return await deps.get_usecase().get_creatives_analytics(input)
|
return await deps.get_usecase().get_creatives_analytics(input)
|
||||||
|
|
||||||
|
|
||||||
@analytics_router.get('/external-channels')
|
@analytics_router.get('/channels')
|
||||||
async def get_external_channels_analytics(
|
async def get_channel_analytics(
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
project_id: uuid.UUID | None = None,
|
||||||
) -> dto.GetExternalChannelsAnalyticsOutput:
|
) -> dto.GetChannelAnalyticsOutput:
|
||||||
input = dto.GetExternalChannelsAnalyticsInput(
|
input = dto.GetChannelAnalyticsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
return await deps.get_usecase().get_external_channels_analytics(input)
|
return await deps.get_usecase().get_channel_analytics(input)
|
||||||
|
|
||||||
|
|
||||||
@analytics_router.get('/spending')
|
@analytics_router.get('/spending')
|
||||||
async def get_spending_analytics(
|
async def get_spending_analytics(
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
project_id: uuid.UUID | None = None,
|
||||||
date_from: datetime.datetime | None = None,
|
date_from: datetime.datetime | None = None,
|
||||||
date_to: datetime.datetime | None = None,
|
date_to: datetime.datetime | None = None,
|
||||||
grouping: dto.DateGrouping = dto.DateGrouping.DAY,
|
grouping: dto.DateGrouping = dto.DateGrouping.DAY,
|
||||||
@@ -65,7 +65,7 @@ async def get_spending_analytics(
|
|||||||
input = dto.GetSpendingAnalyticsInput(
|
input = dto.GetSpendingAnalyticsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
project_id=project_id,
|
||||||
date_from=date_from,
|
date_from=date_from,
|
||||||
date_to=date_to,
|
date_to=date_to,
|
||||||
grouping=grouping,
|
grouping=grouping,
|
||||||
|
|||||||
18
src/controller/http/channels.py
Normal file
18
src/controller/http/channels.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from fastapi.routing import APIRouter
|
||||||
|
|
||||||
|
from src import deps, dto
|
||||||
|
from src.adapter.jwt import JWTPayload
|
||||||
|
|
||||||
|
channels_router = APIRouter(prefix='/channels', tags=['channels'])
|
||||||
|
|
||||||
|
|
||||||
|
@channels_router.get('')
|
||||||
|
async def get_channels(
|
||||||
|
_: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
username: str | None = None,
|
||||||
|
) -> dto.GetChannelsOutput:
|
||||||
|
input = dto.GetChannelsInput(username=username)
|
||||||
|
return await deps.get_usecase().get_channels(input=input)
|
||||||
@@ -14,13 +14,13 @@ creatives_router = APIRouter(prefix='/workspaces/{workspace_id}/creatives', tags
|
|||||||
async def list_creatives(
|
async def list_creatives(
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
project_id: uuid.UUID | None = None,
|
||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
) -> dto.GetCreativesOutput:
|
) -> dto.GetCreativesOutput:
|
||||||
input = dto.GetCreativesInput(
|
input = dto.GetCreativesInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
project_id=project_id,
|
||||||
include_archived=include_archived,
|
include_archived=include_archived,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import uuid
|
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import Depends
|
|
||||||
from fastapi.routing import APIRouter
|
|
||||||
|
|
||||||
from src import deps, dto
|
|
||||||
from src.adapter.jwt import JWTPayload
|
|
||||||
|
|
||||||
external_channels_router = APIRouter(prefix='/workspaces/{workspace_id}/external_channels', tags=['external_channels'])
|
|
||||||
|
|
||||||
|
|
||||||
@external_channels_router.get('/target/{target_channel_id}')
|
|
||||||
async def get_external_channels(
|
|
||||||
target_channel_id: uuid.UUID,
|
|
||||||
workspace_id: uuid.UUID,
|
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
|
||||||
) -> dto.GetExternalChannelsOutput:
|
|
||||||
"""Get all external channels for a specific target channel."""
|
|
||||||
input = dto.GetExternalChannelsInput(
|
|
||||||
target_channel_id=target_channel_id,
|
|
||||||
user_id=current_user.user_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
return await deps.get_usecase().get_external_channels(input=input)
|
|
||||||
|
|
||||||
|
|
||||||
@external_channels_router.post('')
|
|
||||||
async def create_external_channel(
|
|
||||||
request: dto.CreateExternalChannelInput,
|
|
||||||
workspace_id: uuid.UUID,
|
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
|
||||||
) -> dto.ExternalChannelOutput:
|
|
||||||
"""Create a new external channel and link it to target channels."""
|
|
||||||
return await deps.get_usecase().create_external_channel(
|
|
||||||
input=request,
|
|
||||||
user_id=current_user.user_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@external_channels_router.patch('/{channel_id}')
|
|
||||||
async def update_external_channel(
|
|
||||||
channel_id: uuid.UUID,
|
|
||||||
request: dto.UpdateExternalChannelInput,
|
|
||||||
workspace_id: uuid.UUID,
|
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
|
||||||
) -> dto.ExternalChannelOutput:
|
|
||||||
"""Update external channel fields (title, username, description, subscribers_count)."""
|
|
||||||
return await deps.get_usecase().update_external_channel(
|
|
||||||
channel_id=channel_id,
|
|
||||||
input=request,
|
|
||||||
user_id=current_user.user_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@external_channels_router.patch('/{channel_id}/links')
|
|
||||||
async def update_external_channel_links(
|
|
||||||
channel_id: uuid.UUID,
|
|
||||||
request: dto.UpdateExternalChannelLinksInput,
|
|
||||||
workspace_id: uuid.UUID,
|
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
|
||||||
) -> dto.ExternalChannelOutput:
|
|
||||||
"""
|
|
||||||
Update target channel links for an existing external channel.
|
|
||||||
|
|
||||||
You can add new target channels, remove existing ones, or both in a single request.
|
|
||||||
"""
|
|
||||||
return await deps.get_usecase().update_external_channel_links(
|
|
||||||
channel_id=channel_id,
|
|
||||||
input=request,
|
|
||||||
user_id=current_user.user_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@external_channels_router.delete('/{channel_id}')
|
|
||||||
async def delete_external_channel(
|
|
||||||
channel_id: uuid.UUID,
|
|
||||||
workspace_id: uuid.UUID,
|
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
|
||||||
) -> None:
|
|
||||||
"""Delete an external channel."""
|
|
||||||
input = dto.DeleteExternalChannelInput(
|
|
||||||
channel_id=channel_id,
|
|
||||||
user_id=current_user.user_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
await deps.get_usecase().delete_external_channel(input=input)
|
|
||||||
@@ -14,16 +14,16 @@ placements_router = APIRouter(prefix='/workspaces/{workspace_id}/placements', ta
|
|||||||
async def list_placements(
|
async def list_placements(
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
project_id: uuid.UUID | None = None,
|
||||||
external_channel_id: uuid.UUID | None = None,
|
placement_channel_id: uuid.UUID | None = None,
|
||||||
creative_id: uuid.UUID | None = None,
|
creative_id: uuid.UUID | None = None,
|
||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
) -> dto.GetPlacementsOutput:
|
) -> dto.GetPlacementsOutput:
|
||||||
input = dto.GetPlacementsInput(
|
input = dto.GetPlacementsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
project_id=project_id,
|
||||||
external_channel_id=external_channel_id,
|
placement_channel_id=placement_channel_id,
|
||||||
creative_id=creative_id,
|
creative_id=creative_id,
|
||||||
include_archived=include_archived,
|
include_archived=include_archived,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ from fastapi.routing import APIRouter
|
|||||||
from src import deps, dto
|
from src import deps, dto
|
||||||
from src.adapter.jwt import JWTPayload
|
from src.adapter.jwt import JWTPayload
|
||||||
|
|
||||||
target_channels_router = APIRouter(prefix='/workspaces/{workspace_id}/target_channels', tags=['target_channels'])
|
projects_router = APIRouter(prefix='/workspaces/{workspace_id}/projects', tags=['projects'])
|
||||||
|
|
||||||
|
|
||||||
@target_channels_router.get('')
|
@projects_router.get('')
|
||||||
async def get_user_target_chans(
|
async def get_workspace_projects(
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
) -> dto.GetUserTargetChansOutput:
|
) -> dto.GetWorkspaceProjectsOutput:
|
||||||
input = dto.GetUserTargetChansInput(
|
input = dto.GetWorkspaceProjectsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await deps.get_usecase().get_user_target_chans(input=input)
|
return await deps.get_usecase().get_workspace_projects(input=input)
|
||||||
74
src/controller/http/purchase_plans.py
Normal file
74
src/controller/http/purchase_plans.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from fastapi.routing import APIRouter
|
||||||
|
|
||||||
|
from src import deps, dto
|
||||||
|
from src.adapter.jwt import JWTPayload
|
||||||
|
|
||||||
|
purchase_plan_channels_router = APIRouter(
|
||||||
|
prefix='/workspaces/{workspace_id}/projects/{project_id}/purchase-plan/channels',
|
||||||
|
tags=['purchase plan'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@purchase_plan_channels_router.get('')
|
||||||
|
async def get_purchase_plan_channels(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> dto.GetPurchasePlanChannelsOutput:
|
||||||
|
input = dto.GetPurchasePlanChannelsInput(
|
||||||
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
return await deps.get_usecase().get_purchase_plan_channels(input=input)
|
||||||
|
|
||||||
|
|
||||||
|
@purchase_plan_channels_router.post('')
|
||||||
|
async def attach_channel_to_purchase_plan(
|
||||||
|
request: dto.AttachChannelToPurchasePlanInput,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> dto.PurchasePlanChannelOutput:
|
||||||
|
return await deps.get_usecase().attach_channel_to_purchase_plan(
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
user_id=current_user.user_id,
|
||||||
|
input=request,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@purchase_plan_channels_router.patch('/{channel_id}')
|
||||||
|
async def update_purchase_plan_channel(
|
||||||
|
channel_id: uuid.UUID,
|
||||||
|
request: dto.UpdatePurchasePlanChannelInput,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> dto.PurchasePlanChannelOutput:
|
||||||
|
return await deps.get_usecase().update_purchase_plan_channel(
|
||||||
|
channel_id=channel_id,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
user_id=current_user.user_id,
|
||||||
|
input=request,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@purchase_plan_channels_router.delete('/{channel_id}')
|
||||||
|
async def remove_purchase_plan_channel(
|
||||||
|
channel_id: uuid.UUID,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> None:
|
||||||
|
await deps.get_usecase().remove_purchase_plan_channel(
|
||||||
|
channel_id=channel_id,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
user_id=current_user.user_id,
|
||||||
|
)
|
||||||
@@ -8,6 +8,6 @@ from . import ( # noqa: E402, F401
|
|||||||
chat_member_updated,
|
chat_member_updated,
|
||||||
creative_commands,
|
creative_commands,
|
||||||
my_chat_member,
|
my_chat_member,
|
||||||
|
project_commands,
|
||||||
start_with_login,
|
start_with_login,
|
||||||
target_channel_commands,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ async def on_bot_added_to_chat(event: ChatMemberUpdated) -> None:
|
|||||||
bot_removed = was_member and is_now_not_member
|
bot_removed = was_member and is_now_not_member
|
||||||
|
|
||||||
if bot_removed:
|
if bot_removed:
|
||||||
disconnect_input = dto.DisconnectTargetChanByTgIdInput(
|
disconnect_input = dto.DisconnectProjectByTgIdInput(
|
||||||
telegram_id=event.chat.id,
|
telegram_id=event.chat.id,
|
||||||
user_telegram_id=event.from_user.id,
|
user_telegram_id=event.from_user.id,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await usecase.disconnect_target_chan_by_tg_id(input=disconnect_input)
|
await usecase.disconnect_project_by_tg_id(input=disconnect_input)
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
log.error(e)
|
log.error(e)
|
||||||
|
|
||||||
@@ -53,14 +53,14 @@ async def on_bot_added_to_chat(event: ChatMemberUpdated) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if permissions_changed:
|
if permissions_changed:
|
||||||
permissions_input = dto.UpdateTargetChanPermissionsInput(
|
permissions_input = dto.UpdateProjectPermissionsInput(
|
||||||
telegram_id=event.chat.id,
|
telegram_id=event.chat.id,
|
||||||
permissions=bot_permissions,
|
permissions=bot_permissions,
|
||||||
chat_title=event.chat.title or f'Channel {event.chat.id}',
|
chat_title=event.chat.title or f'Channel {event.chat.id}',
|
||||||
user_telegram_id=event.from_user.id,
|
user_telegram_id=event.from_user.id,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await usecase.update_target_chan_permissions(input=permissions_input)
|
await usecase.update_project_permissions(input=permissions_input)
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
log.error(e)
|
log.error(e)
|
||||||
return
|
return
|
||||||
@@ -71,7 +71,7 @@ async def on_bot_added_to_chat(event: ChatMemberUpdated) -> None:
|
|||||||
bot_added = was_not_member and is_now_member
|
bot_added = was_not_member and is_now_member
|
||||||
|
|
||||||
if bot_added:
|
if bot_added:
|
||||||
connect_input = dto.ConnectTargetChanInput(
|
connect_input = dto.ConnectProjectInput(
|
||||||
telegram_id=event.chat.id,
|
telegram_id=event.chat.id,
|
||||||
title=event.chat.title or f'Channel {event.chat.id}',
|
title=event.chat.title or f'Channel {event.chat.id}',
|
||||||
username=event.chat.username,
|
username=event.chat.username,
|
||||||
@@ -79,6 +79,6 @@ async def on_bot_added_to_chat(event: ChatMemberUpdated) -> None:
|
|||||||
bot_permissions=bot_permissions,
|
bot_permissions=bot_permissions,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await usecase.tg_add_target_chan_1(input=connect_input)
|
await usecase.tg_add_project_1(input=connect_input)
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
log.error(e)
|
log.error(e)
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ from src.controller.telegram_callback import telegram_callback_router
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@telegram_callback_router.callback_query(F.data.startswith('tg_add_target_chan_2:'))
|
@telegram_callback_router.callback_query(F.data.startswith('tg_add_project_2:'))
|
||||||
async def callback_target_channel_workspace(callback: CallbackQuery) -> None:
|
async def callback_project_workspace(callback: CallbackQuery) -> None:
|
||||||
if not callback.from_user or not callback.message or not callback.data:
|
if not callback.from_user or not callback.message or not callback.data:
|
||||||
log.error('Failed to get required data from callback query')
|
log.error('Failed to get required data from callback query')
|
||||||
return
|
return
|
||||||
|
|
||||||
usecase = deps.get_usecase()
|
usecase = deps.get_usecase()
|
||||||
await usecase.tg_add_target_chan_2(
|
await usecase.tg_add_project_2(
|
||||||
telegram_id=callback.from_user.id,
|
telegram_id=callback.from_user.id,
|
||||||
chat_id=callback.message.chat.id,
|
chat_id=callback.message.chat.id,
|
||||||
callback_data=callback.data,
|
callback_data=callback.data,
|
||||||
@@ -2,16 +2,29 @@ __all__ = (
|
|||||||
'User',
|
'User',
|
||||||
'Workspace',
|
'Workspace',
|
||||||
'WorkspaceUser',
|
'WorkspaceUser',
|
||||||
'TargetChannel',
|
'WorkspaceInvite',
|
||||||
'ExternalChannel',
|
'WorkspaceInviteStatus',
|
||||||
|
'WorkspaceUserStatus',
|
||||||
|
'WorkspaceUserPermission',
|
||||||
|
'WorkspaceUserPermissionScope',
|
||||||
|
'PermissionKey',
|
||||||
|
'PermissionScopeType',
|
||||||
|
'Channel',
|
||||||
|
'ChannelVerificationStatus',
|
||||||
|
'Project',
|
||||||
|
'ProjectStatus',
|
||||||
|
'PurchasePlan',
|
||||||
|
'PurchasePlanChannel',
|
||||||
|
'PurchasePlanStatus',
|
||||||
|
'PurchasePlanChannelStatus',
|
||||||
'Creative',
|
'Creative',
|
||||||
'Placement',
|
'Placement',
|
||||||
'Subscription',
|
'Subscription',
|
||||||
'Subscriber',
|
|
||||||
'PlacementViewsHistory',
|
'PlacementViewsHistory',
|
||||||
'TelegramState',
|
'TelegramState',
|
||||||
'TelegramStateEnum',
|
'TelegramStateEnum',
|
||||||
'TargetChannelStatus',
|
'ChannelNotFound',
|
||||||
|
'ProjectNotFound',
|
||||||
'CreativeStatus',
|
'CreativeStatus',
|
||||||
'PlacementStatus',
|
'PlacementStatus',
|
||||||
'SubscriptionStatus',
|
'SubscriptionStatus',
|
||||||
@@ -24,40 +37,57 @@ __all__ = (
|
|||||||
'LoginTokenNotFound',
|
'LoginTokenNotFound',
|
||||||
'LoginTokenExpired',
|
'LoginTokenExpired',
|
||||||
'LoginTokenAlreadyUsed',
|
'LoginTokenAlreadyUsed',
|
||||||
'TargetChannelNotFound',
|
'ProjectNotFound',
|
||||||
|
'PurchasePlanNotFound',
|
||||||
|
'PurchasePlanChannelNotFound',
|
||||||
|
'ChannelNotFound',
|
||||||
'ChannelAlreadyExists',
|
'ChannelAlreadyExists',
|
||||||
'ChannelNoAdminRights',
|
'ChannelNoAdminRights',
|
||||||
'ExternalChannelNotFound',
|
|
||||||
'ExternalChannelAlreadyExists',
|
|
||||||
'CreativeNotFound',
|
'CreativeNotFound',
|
||||||
'CreativeInUse',
|
'CreativeInUse',
|
||||||
'PlacementNotFound',
|
'PlacementNotFound',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .channel import Channel, ChannelVerificationStatus
|
||||||
from .creative import Creative, CreativeStatus
|
from .creative import Creative, CreativeStatus
|
||||||
from .error import (
|
from .error import (
|
||||||
ChannelAlreadyExists,
|
ChannelAlreadyExists,
|
||||||
ChannelNoAdminRights,
|
ChannelNoAdminRights,
|
||||||
|
ChannelNotFound,
|
||||||
CreativeInUse,
|
CreativeInUse,
|
||||||
CreativeNotFound,
|
CreativeNotFound,
|
||||||
ExternalChannelAlreadyExists,
|
|
||||||
ExternalChannelNotFound,
|
|
||||||
LoginTokenAlreadyUsed,
|
LoginTokenAlreadyUsed,
|
||||||
LoginTokenExpired,
|
LoginTokenExpired,
|
||||||
LoginTokenNotFound,
|
LoginTokenNotFound,
|
||||||
PlacementNotFound,
|
PlacementNotFound,
|
||||||
TargetChannelNotFound,
|
ProjectNotFound,
|
||||||
|
PurchasePlanChannelNotFound,
|
||||||
|
PurchasePlanNotFound,
|
||||||
UserNotFound,
|
UserNotFound,
|
||||||
WorkspaceAccessDenied,
|
WorkspaceAccessDenied,
|
||||||
WorkspaceNotFound,
|
WorkspaceNotFound,
|
||||||
)
|
)
|
||||||
from .external_channel import ExternalChannel
|
|
||||||
from .login_token import LoginToken
|
from .login_token import LoginToken
|
||||||
from .placement import InviteLinkType, Placement, PlacementStatus, PostViewsAvailability
|
from .placement import InviteLinkType, Placement, PlacementStatus, PostViewsAvailability
|
||||||
from .placement_views_history import PlacementViewsHistory
|
from .placement_views_history import PlacementViewsHistory
|
||||||
from .subscriber import Subscriber
|
from .project import Project, ProjectStatus
|
||||||
|
from .purchase_plan import (
|
||||||
|
PurchasePlan,
|
||||||
|
PurchasePlanChannel,
|
||||||
|
PurchasePlanChannelStatus,
|
||||||
|
PurchasePlanStatus,
|
||||||
|
)
|
||||||
from .subscription import Subscription, SubscriptionStatus
|
from .subscription import Subscription, SubscriptionStatus
|
||||||
from .target_channel import TargetChannel, TargetChannelStatus
|
|
||||||
from .telegram_state import TelegramState, TelegramStateEnum
|
from .telegram_state import TelegramState, TelegramStateEnum
|
||||||
from .user import User
|
from .user import User
|
||||||
from .workspace import Workspace, WorkspaceUser
|
from .workspace import (
|
||||||
|
PermissionKey,
|
||||||
|
PermissionScopeType,
|
||||||
|
Workspace,
|
||||||
|
WorkspaceInvite,
|
||||||
|
WorkspaceInviteStatus,
|
||||||
|
WorkspaceUser,
|
||||||
|
WorkspaceUserPermission,
|
||||||
|
WorkspaceUserPermissionScope,
|
||||||
|
WorkspaceUserStatus,
|
||||||
|
)
|
||||||
|
|||||||
34
src/domain/channel.py
Normal file
34
src/domain/channel.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import enum
|
||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from tortoise import fields
|
||||||
|
|
||||||
|
from .base import TimestampedModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .workspace import Workspace
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelVerificationStatus(str, enum.Enum):
|
||||||
|
UNVERIFIED = 'unverified'
|
||||||
|
VERIFIED = 'verified'
|
||||||
|
FAILED = 'failed'
|
||||||
|
|
||||||
|
|
||||||
|
class Channel(TimestampedModel):
|
||||||
|
id = fields.UUIDField(pk=True)
|
||||||
|
telegram_id = fields.BigIntField(null=True, unique=True, index=True)
|
||||||
|
username = fields.CharField(max_length=255, null=True, index=True)
|
||||||
|
title = fields.CharField(max_length=255, null=True)
|
||||||
|
verification_status = fields.CharEnumField(ChannelVerificationStatus, default=ChannelVerificationStatus.UNVERIFIED)
|
||||||
|
|
||||||
|
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||||
|
'models.Workspace', related_name='channels', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'channel'
|
||||||
@@ -7,7 +7,7 @@ from tortoise import fields
|
|||||||
from .base import TimestampedModel
|
from .base import TimestampedModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .target_channel import TargetChannel
|
from .project import Project
|
||||||
from .workspace import Workspace
|
from .workspace import Workspace
|
||||||
|
|
||||||
|
|
||||||
@@ -26,13 +26,13 @@ class Creative(TimestampedModel):
|
|||||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||||
'models.Workspace', related_name='creatives', on_delete=fields.CASCADE, index=True
|
'models.Workspace', related_name='creatives', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField(
|
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
||||||
'models.TargetChannel', related_name='creatives', on_delete=fields.CASCADE, index=True
|
'models.Project', related_name='creatives', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
workspace_id: UUID
|
workspace_id: UUID
|
||||||
target_channel_id: UUID
|
project_id: UUID
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'creative'
|
table = 'creative'
|
||||||
|
|||||||
@@ -21,10 +21,28 @@ def LoginTokenAlreadyUsed() -> HTTPException:
|
|||||||
return HTTPException(status.HTTP_400_BAD_REQUEST, 'Login token has already been used')
|
return HTTPException(status.HTTP_400_BAD_REQUEST, 'Login token has already been used')
|
||||||
|
|
||||||
|
|
||||||
def TargetChannelNotFound(channel_id: uuid.UUID | None = None) -> HTTPException:
|
def ChannelNotFound(channel_id: uuid.UUID | None = None) -> HTTPException:
|
||||||
if channel_id is None:
|
if channel_id is None:
|
||||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Target channel not found')
|
return HTTPException(status.HTTP_404_NOT_FOUND, 'Channel not found')
|
||||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Target channel {channel_id} not found')
|
return HTTPException(status.HTTP_404_NOT_FOUND, f'Channel {channel_id} not found')
|
||||||
|
|
||||||
|
|
||||||
|
def ProjectNotFound(project_id: uuid.UUID | None = None) -> HTTPException:
|
||||||
|
if project_id is None:
|
||||||
|
return HTTPException(status.HTTP_404_NOT_FOUND, 'Project not found')
|
||||||
|
return HTTPException(status.HTTP_404_NOT_FOUND, f'Project {project_id} not found')
|
||||||
|
|
||||||
|
|
||||||
|
def PurchasePlanNotFound(plan_id: uuid.UUID | None = None) -> HTTPException:
|
||||||
|
if plan_id is None:
|
||||||
|
return HTTPException(status.HTTP_404_NOT_FOUND, 'Purchase plan not found')
|
||||||
|
return HTTPException(status.HTTP_404_NOT_FOUND, f'Purchase plan {plan_id} not found')
|
||||||
|
|
||||||
|
|
||||||
|
def PurchasePlanChannelNotFound(plan_channel_id: uuid.UUID | None = None) -> HTTPException:
|
||||||
|
if plan_channel_id is None:
|
||||||
|
return HTTPException(status.HTTP_404_NOT_FOUND, 'Purchase plan channel not found')
|
||||||
|
return HTTPException(status.HTTP_404_NOT_FOUND, f'Purchase plan channel {plan_channel_id} not found')
|
||||||
|
|
||||||
|
|
||||||
def ChannelAlreadyExists(telegram_id: int) -> HTTPException:
|
def ChannelAlreadyExists(telegram_id: int) -> HTTPException:
|
||||||
@@ -37,16 +55,6 @@ def ChannelNoAdminRights() -> HTTPException:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def ExternalChannelNotFound(channel_id: uuid.UUID | None = None) -> HTTPException:
|
|
||||||
if channel_id is None:
|
|
||||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'External channel not found')
|
|
||||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'External channel {channel_id} not found')
|
|
||||||
|
|
||||||
|
|
||||||
def ExternalChannelAlreadyExists(telegram_id: int) -> HTTPException:
|
|
||||||
return HTTPException(status.HTTP_409_CONFLICT, f'External channel {telegram_id} already exists')
|
|
||||||
|
|
||||||
|
|
||||||
def CreativeNotFound(creative_id: uuid.UUID | None = None) -> HTTPException:
|
def CreativeNotFound(creative_id: uuid.UUID | None = None) -> HTTPException:
|
||||||
if creative_id is None:
|
if creative_id is None:
|
||||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Creative not found')
|
return HTTPException(status.HTTP_404_NOT_FOUND, 'Creative not found')
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
from typing import TYPE_CHECKING
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from tortoise import fields
|
|
||||||
|
|
||||||
from .base import TimestampedModel
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .target_channel import TargetChannel
|
|
||||||
from .workspace import Workspace
|
|
||||||
|
|
||||||
|
|
||||||
class ExternalChannel(TimestampedModel):
|
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
telegram_id = fields.BigIntField(index=True)
|
|
||||||
title = fields.CharField(max_length=255)
|
|
||||||
username = fields.CharField(max_length=255, null=True, index=True)
|
|
||||||
description = fields.TextField(null=True)
|
|
||||||
subscribers_count = fields.IntField(null=True)
|
|
||||||
|
|
||||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
|
||||||
'models.Workspace', related_name='external_channels', on_delete=fields.CASCADE, index=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
target_channels: fields.ManyToManyRelation['TargetChannel']
|
|
||||||
workspace_id: UUID
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table = 'external_channel'
|
|
||||||
@@ -7,9 +7,9 @@ from tortoise import fields
|
|||||||
from .base import TimestampedModel
|
from .base import TimestampedModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .channel import Channel
|
||||||
from .creative import Creative
|
from .creative import Creative
|
||||||
from .external_channel import ExternalChannel
|
from .project import Project
|
||||||
from .target_channel import TargetChannel
|
|
||||||
from .workspace import Workspace
|
from .workspace import Workspace
|
||||||
|
|
||||||
|
|
||||||
@@ -50,11 +50,11 @@ class Placement(TimestampedModel):
|
|||||||
views_availability = fields.CharEnumField(PostViewsAvailability, default=PostViewsAvailability.UNKNOWN)
|
views_availability = fields.CharEnumField(PostViewsAvailability, default=PostViewsAvailability.UNKNOWN)
|
||||||
last_views_fetch_at = fields.DatetimeField(null=True)
|
last_views_fetch_at = fields.DatetimeField(null=True)
|
||||||
|
|
||||||
target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField(
|
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
||||||
'models.TargetChannel', related_name='placements', on_delete=fields.CASCADE, index=True
|
'models.Project', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
external_channel: fields.ForeignKeyRelation['ExternalChannel'] = fields.ForeignKeyField(
|
placement_channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
|
||||||
'models.ExternalChannel', related_name='placements', on_delete=fields.CASCADE, index=True
|
'models.Channel', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
|
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
|
||||||
'models.Creative', related_name='placements', on_delete=fields.CASCADE, index=True
|
'models.Creative', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||||
@@ -64,8 +64,8 @@ class Placement(TimestampedModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
target_channel_id: UUID
|
project_id: UUID
|
||||||
external_channel_id: UUID
|
placement_channel_id: UUID
|
||||||
creative_id: UUID
|
creative_id: UUID
|
||||||
workspace_id: UUID
|
workspace_id: UUID
|
||||||
|
|
||||||
|
|||||||
37
src/domain/project.py
Normal file
37
src/domain/project.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import enum
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from tortoise import fields
|
||||||
|
|
||||||
|
from .base import TimestampedModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .channel import Channel
|
||||||
|
from .workspace import Workspace
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectStatus(str, enum.Enum):
|
||||||
|
ACTIVE = 'active'
|
||||||
|
INACTIVE = 'inactive'
|
||||||
|
ARCHIVED = 'archived'
|
||||||
|
|
||||||
|
|
||||||
|
class Project(TimestampedModel):
|
||||||
|
id = fields.UUIDField(pk=True)
|
||||||
|
status = fields.CharEnumField(ProjectStatus, default=ProjectStatus.ACTIVE)
|
||||||
|
|
||||||
|
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||||
|
'models.Workspace', related_name='projects', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
|
||||||
|
'models.Channel', related_name='projects', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
workspace_id: UUID
|
||||||
|
channel_id: UUID
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'project'
|
||||||
|
unique_together = (('workspace_id', 'channel_id'),)
|
||||||
63
src/domain/purchase_plan.py
Normal file
63
src/domain/purchase_plan.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import enum
|
||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from tortoise import fields
|
||||||
|
|
||||||
|
from .base import TimestampedModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .channel import Channel
|
||||||
|
from .project import Project
|
||||||
|
from .workspace import Workspace
|
||||||
|
|
||||||
|
|
||||||
|
class PurchasePlanStatus(str, enum.Enum):
|
||||||
|
ACTIVE = 'active'
|
||||||
|
ARCHIVED = 'archived'
|
||||||
|
|
||||||
|
|
||||||
|
class PurchasePlanChannelStatus(str, enum.Enum):
|
||||||
|
PLANNED = 'planned'
|
||||||
|
APPROVED = 'approved'
|
||||||
|
REJECTED = 'rejected'
|
||||||
|
IN_PROGRESS = 'in_progress'
|
||||||
|
COMPLETED = 'completed'
|
||||||
|
|
||||||
|
|
||||||
|
class PurchasePlan(TimestampedModel):
|
||||||
|
id = fields.UUIDField(pk=True)
|
||||||
|
status = fields.CharEnumField(PurchasePlanStatus, default=PurchasePlanStatus.ACTIVE)
|
||||||
|
|
||||||
|
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||||
|
'models.Workspace', related_name='purchase_plans', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
||||||
|
'models.Project', related_name='purchase_plans', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'purchase_plan'
|
||||||
|
indexes = (('project', 'status'),)
|
||||||
|
|
||||||
|
|
||||||
|
class PurchasePlanChannel(TimestampedModel):
|
||||||
|
id = fields.UUIDField(pk=True)
|
||||||
|
status = fields.CharEnumField(PurchasePlanChannelStatus, default=PurchasePlanChannelStatus.PLANNED)
|
||||||
|
planned_cost = fields.FloatField(null=True)
|
||||||
|
comment = fields.TextField(null=True)
|
||||||
|
|
||||||
|
purchase_plan: fields.ForeignKeyRelation['PurchasePlan'] = fields.ForeignKeyField(
|
||||||
|
'models.PurchasePlan', related_name='channels', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
|
||||||
|
'models.Channel', related_name='purchase_plan_channels', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
purchase_plan_id: uuid.UUID
|
||||||
|
channel_id: uuid.UUID
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'purchase_plan_channel'
|
||||||
|
unique_together = (('purchase_plan_id', 'channel_id'),)
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from tortoise import fields
|
|
||||||
|
|
||||||
from .base import TimestampedModel
|
|
||||||
|
|
||||||
|
|
||||||
class Subscriber(TimestampedModel):
|
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
telegram_id = fields.BigIntField(unique=True, index=True)
|
|
||||||
username = fields.CharField(max_length=255, null=True)
|
|
||||||
first_name = fields.CharField(max_length=255, null=True)
|
|
||||||
last_name = fields.CharField(max_length=255, null=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table = 'subscriber'
|
|
||||||
@@ -8,8 +8,7 @@ from .base import TimestampedModel
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .placement import Placement
|
from .placement import Placement
|
||||||
from .subscriber import Subscriber
|
from .user import User
|
||||||
from .target_channel import TargetChannel
|
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionStatus(str, enum.Enum):
|
class SubscriptionStatus(str, enum.Enum):
|
||||||
@@ -26,17 +25,13 @@ class Subscription(TimestampedModel):
|
|||||||
placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField(
|
placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField(
|
||||||
'models.Placement', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
'models.Placement', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
subscriber: fields.ForeignKeyRelation['Subscriber'] = fields.ForeignKeyField(
|
subscriber: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
||||||
'models.Subscriber', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
'models.User', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
||||||
)
|
|
||||||
target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField(
|
|
||||||
'models.TargetChannel', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
placement_id: UUID
|
placement_id: UUID
|
||||||
subscriber_id: UUID
|
subscriber_id: UUID
|
||||||
target_channel_id: UUID
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'subscription'
|
table = 'subscription'
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import enum
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from tortoise import fields
|
|
||||||
|
|
||||||
from .base import TimestampedModel
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .external_channel import ExternalChannel
|
|
||||||
from .workspace import Workspace
|
|
||||||
|
|
||||||
|
|
||||||
class TargetChannelStatus(str, enum.Enum):
|
|
||||||
ACTIVE = 'active'
|
|
||||||
INACTIVE = 'inactive'
|
|
||||||
ARCHIVED = 'archived'
|
|
||||||
|
|
||||||
|
|
||||||
class TargetChannel(TimestampedModel):
|
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
telegram_id = fields.BigIntField(unique=True, index=True)
|
|
||||||
title = fields.CharField(max_length=255)
|
|
||||||
username = fields.CharField(max_length=255, null=True, index=True)
|
|
||||||
status = fields.CharEnumField(TargetChannelStatus, default=TargetChannelStatus.ACTIVE)
|
|
||||||
|
|
||||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
|
||||||
'models.Workspace', related_name='target_channels', on_delete=fields.CASCADE, index=True
|
|
||||||
)
|
|
||||||
|
|
||||||
external_channels: fields.ManyToManyRelation['ExternalChannel'] = fields.ManyToManyField(
|
|
||||||
'models.ExternalChannel',
|
|
||||||
related_name='target_channels',
|
|
||||||
through='target_external_channel',
|
|
||||||
forward_key='external_channel_id',
|
|
||||||
backward_key='target_channel_id',
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
workspace_id: UUID
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table = 'target_channel'
|
|
||||||
@@ -10,7 +10,7 @@ class TelegramStateEnum(str, enum.Enum):
|
|||||||
CREATIVE_WAITING_CHANNEL = 'creative_waiting_channel'
|
CREATIVE_WAITING_CHANNEL = 'creative_waiting_channel'
|
||||||
CREATIVE_WAITING_NAME = 'creative_waiting_name'
|
CREATIVE_WAITING_NAME = 'creative_waiting_name'
|
||||||
CREATIVE_WAITING_TEXT = 'creative_waiting_text'
|
CREATIVE_WAITING_TEXT = 'creative_waiting_text'
|
||||||
TARGET_CHANNEL_WAITING_WORKSPACE = 'target_channel_waiting_workspace'
|
PROJECT_WAITING_WORKSPACE = 'project_waiting_workspace'
|
||||||
|
|
||||||
|
|
||||||
class TelegramState(TimestampedModel):
|
class TelegramState(TimestampedModel):
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ class User(TimestampedModel):
|
|||||||
id = fields.UUIDField(pk=True)
|
id = fields.UUIDField(pk=True)
|
||||||
telegram_id = fields.BigIntField(unique=True, index=True)
|
telegram_id = fields.BigIntField(unique=True, index=True)
|
||||||
username = fields.CharField(max_length=255, null=True)
|
username = fields.CharField(max_length=255, null=True)
|
||||||
|
first_name = fields.CharField(max_length=255, null=True)
|
||||||
|
last_name = fields.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'user'
|
table = 'user'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
import uuid
|
import uuid
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -19,8 +20,15 @@ class Workspace(TimestampedModel):
|
|||||||
table = 'workspace'
|
table = 'workspace'
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserStatus(str, enum.Enum):
|
||||||
|
ACTIVE = 'active'
|
||||||
|
INVITED = 'invited'
|
||||||
|
REMOVED = 'removed'
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceUser(TimestampedModel):
|
class WorkspaceUser(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
id = fields.UUIDField(pk=True)
|
||||||
|
status = fields.CharEnumField(WorkspaceUserStatus, default=WorkspaceUserStatus.ACTIVE)
|
||||||
|
|
||||||
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
|
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
|
||||||
'models.Workspace', related_name='workspace_users', on_delete=fields.CASCADE, index=True
|
'models.Workspace', related_name='workspace_users', on_delete=fields.CASCADE, index=True
|
||||||
@@ -36,3 +44,65 @@ class WorkspaceUser(TimestampedModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
table = 'workspace_user'
|
table = 'workspace_user'
|
||||||
unique_together = (('workspace_id', 'user_id'),)
|
unique_together = (('workspace_id', 'user_id'),)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceInviteStatus(str, enum.Enum):
|
||||||
|
PENDING = 'pending'
|
||||||
|
ACCEPTED = 'accepted'
|
||||||
|
REVOKED = 'revoked'
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceInvite(TimestampedModel):
|
||||||
|
id = fields.UUIDField(pk=True)
|
||||||
|
status = fields.CharEnumField(WorkspaceInviteStatus, default=WorkspaceInviteStatus.PENDING)
|
||||||
|
|
||||||
|
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
|
||||||
|
'models.Workspace', related_name='invites', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
invited_by: fields.ForeignKeyRelation[User] = fields.ForeignKeyField(
|
||||||
|
'models.User', related_name='sent_invites', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
user: fields.ForeignKeyRelation[User] = fields.ForeignKeyField(
|
||||||
|
'models.User', related_name='workspace_invites', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'workspace_invite'
|
||||||
|
unique_together = (('workspace_id', 'user_id'),)
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionKey(str, enum.Enum):
|
||||||
|
MANAGE_PROJECTS = 'manage_projects'
|
||||||
|
MANAGE_PLACEMENTS = 'manage_placements'
|
||||||
|
VIEW_ANALYTICS = 'view_analytics'
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionScopeType(str, enum.Enum):
|
||||||
|
WORKSPACE = 'workspace'
|
||||||
|
PROJECT = 'project'
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserPermission(TimestampedModel):
|
||||||
|
workspace_user: fields.ForeignKeyRelation[WorkspaceUser] = fields.ForeignKeyField(
|
||||||
|
'models.WorkspaceUser', related_name='permissions', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
permission = fields.CharEnumField(PermissionKey)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'workspace_user_permission'
|
||||||
|
unique_together = (('workspace_user_id', 'permission'),)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserPermissionScope(TimestampedModel):
|
||||||
|
id = fields.UUIDField(pk=True)
|
||||||
|
permission = fields.CharEnumField(PermissionKey)
|
||||||
|
scope_type = fields.CharEnumField(PermissionScopeType)
|
||||||
|
scope_id = fields.UUIDField()
|
||||||
|
|
||||||
|
workspace_user: fields.ForeignKeyRelation[WorkspaceUser] = fields.ForeignKeyField(
|
||||||
|
'models.WorkspaceUser', related_name='permission_scopes', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'workspace_user_permission_scope'
|
||||||
|
unique_together = (('workspace_user_id', 'permission', 'scope_type', 'scope_id'),)
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'UpdateTargetChanPermissionsInput',
|
'UpdateProjectPermissionsInput',
|
||||||
'GetUserTargetChansInput',
|
'GetWorkspaceProjectsInput',
|
||||||
'GetUserTargetChansOutput',
|
'GetWorkspaceProjectsOutput',
|
||||||
'DisconnectTargetChanByTgIdInput',
|
'DisconnectProjectByTgIdInput',
|
||||||
'ConnectTargetChanInput',
|
'ConnectProjectInput',
|
||||||
'ConnectTargetChanOutput',
|
'ProjectOutput',
|
||||||
'ValidateLoginTokenInput',
|
'ValidateLoginTokenInput',
|
||||||
'ValidateLoginTokenOutput',
|
'ValidateLoginTokenOutput',
|
||||||
'TelegramLoginInput',
|
'TelegramLoginInput',
|
||||||
'ChannelBotPermissions',
|
'ChannelBotPermissions',
|
||||||
'ExternalChannelOutput',
|
'ChannelOutput',
|
||||||
'GetExternalChannelsInput',
|
'GetChannelsInput',
|
||||||
'GetExternalChannelsOutput',
|
'GetChannelsOutput',
|
||||||
'CreateExternalChannelInput',
|
'PurchasePlanChannelOutput',
|
||||||
'DeleteExternalChannelInput',
|
'GetPurchasePlanChannelsInput',
|
||||||
'UpdateExternalChannelInput',
|
'GetPurchasePlanChannelsOutput',
|
||||||
'UpdateExternalChannelLinksInput',
|
'AttachChannelToPurchasePlanInput',
|
||||||
|
'UpdatePurchasePlanChannelInput',
|
||||||
'CreativeOutput',
|
'CreativeOutput',
|
||||||
'GetCreativesInput',
|
'GetCreativesInput',
|
||||||
'GetCreativesOutput',
|
'GetCreativesOutput',
|
||||||
@@ -37,14 +38,14 @@ __all__ = (
|
|||||||
'UserOutput',
|
'UserOutput',
|
||||||
'DateGrouping',
|
'DateGrouping',
|
||||||
'PlacementAnalyticsOutput',
|
'PlacementAnalyticsOutput',
|
||||||
|
'ChannelAnalyticsOutput',
|
||||||
'CreativeAnalyticsOutput',
|
'CreativeAnalyticsOutput',
|
||||||
'ExternalChannelAnalyticsOutput',
|
|
||||||
'GetPlacementsAnalyticsInput',
|
'GetPlacementsAnalyticsInput',
|
||||||
'GetPlacementsAnalyticsOutput',
|
'GetPlacementsAnalyticsOutput',
|
||||||
'GetCreativesAnalyticsInput',
|
'GetCreativesAnalyticsInput',
|
||||||
'GetCreativesAnalyticsOutput',
|
'GetCreativesAnalyticsOutput',
|
||||||
'GetExternalChannelsAnalyticsInput',
|
'GetChannelAnalyticsInput',
|
||||||
'GetExternalChannelsAnalyticsOutput',
|
'GetChannelAnalyticsOutput',
|
||||||
'SpendingDataPoint',
|
'SpendingDataPoint',
|
||||||
'GetSpendingAnalyticsInput',
|
'GetSpendingAnalyticsInput',
|
||||||
'GetSpendingAnalyticsOutput',
|
'GetSpendingAnalyticsOutput',
|
||||||
@@ -56,13 +57,13 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .analytics import (
|
from .analytics import (
|
||||||
|
ChannelAnalyticsOutput,
|
||||||
CreativeAnalyticsOutput,
|
CreativeAnalyticsOutput,
|
||||||
DateGrouping,
|
DateGrouping,
|
||||||
ExternalChannelAnalyticsOutput,
|
GetChannelAnalyticsInput,
|
||||||
|
GetChannelAnalyticsOutput,
|
||||||
GetCreativesAnalyticsInput,
|
GetCreativesAnalyticsInput,
|
||||||
GetCreativesAnalyticsOutput,
|
GetCreativesAnalyticsOutput,
|
||||||
GetExternalChannelsAnalyticsInput,
|
|
||||||
GetExternalChannelsAnalyticsOutput,
|
|
||||||
GetPlacementsAnalyticsInput,
|
GetPlacementsAnalyticsInput,
|
||||||
GetPlacementsAnalyticsOutput,
|
GetPlacementsAnalyticsOutput,
|
||||||
GetSpendingAnalyticsInput,
|
GetSpendingAnalyticsInput,
|
||||||
@@ -70,6 +71,7 @@ from .analytics import (
|
|||||||
PlacementAnalyticsOutput,
|
PlacementAnalyticsOutput,
|
||||||
SpendingDataPoint,
|
SpendingDataPoint,
|
||||||
)
|
)
|
||||||
|
from .channel import ChannelOutput, GetChannelsInput, GetChannelsOutput
|
||||||
from .creative import (
|
from .creative import (
|
||||||
CreateCreativeInput,
|
CreateCreativeInput,
|
||||||
CreativeOutput,
|
CreativeOutput,
|
||||||
@@ -79,15 +81,6 @@ from .creative import (
|
|||||||
GetCreativesOutput,
|
GetCreativesOutput,
|
||||||
UpdateCreativeInput,
|
UpdateCreativeInput,
|
||||||
)
|
)
|
||||||
from .external_channel import (
|
|
||||||
CreateExternalChannelInput,
|
|
||||||
DeleteExternalChannelInput,
|
|
||||||
ExternalChannelOutput,
|
|
||||||
GetExternalChannelsInput,
|
|
||||||
GetExternalChannelsOutput,
|
|
||||||
UpdateExternalChannelInput,
|
|
||||||
UpdateExternalChannelLinksInput,
|
|
||||||
)
|
|
||||||
from .placement import (
|
from .placement import (
|
||||||
CreatePlacementInput,
|
CreatePlacementInput,
|
||||||
DeletePlacementInput,
|
DeletePlacementInput,
|
||||||
@@ -97,14 +90,21 @@ from .placement import (
|
|||||||
PlacementOutput,
|
PlacementOutput,
|
||||||
UpdatePlacementInput,
|
UpdatePlacementInput,
|
||||||
)
|
)
|
||||||
from .target_channel import (
|
from .project import (
|
||||||
ChannelBotPermissions,
|
ChannelBotPermissions,
|
||||||
ConnectTargetChanInput,
|
ConnectProjectInput,
|
||||||
ConnectTargetChanOutput,
|
DisconnectProjectByTgIdInput,
|
||||||
DisconnectTargetChanByTgIdInput,
|
GetWorkspaceProjectsInput,
|
||||||
GetUserTargetChansInput,
|
GetWorkspaceProjectsOutput,
|
||||||
GetUserTargetChansOutput,
|
ProjectOutput,
|
||||||
UpdateTargetChanPermissionsInput,
|
UpdateProjectPermissionsInput,
|
||||||
|
)
|
||||||
|
from .purchase_plan import (
|
||||||
|
AttachChannelToPurchasePlanInput,
|
||||||
|
GetPurchasePlanChannelsInput,
|
||||||
|
GetPurchasePlanChannelsOutput,
|
||||||
|
PurchasePlanChannelOutput,
|
||||||
|
UpdatePurchasePlanChannelInput,
|
||||||
)
|
)
|
||||||
from .telegram_login import TelegramLoginInput
|
from .telegram_login import TelegramLoginInput
|
||||||
from .user import UserOutput
|
from .user import UserOutput
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ class DateGrouping(StrEnum):
|
|||||||
|
|
||||||
class PlacementAnalyticsOutput(pydantic.BaseModel):
|
class PlacementAnalyticsOutput(pydantic.BaseModel):
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID
|
project_id: uuid.UUID
|
||||||
target_channel_title: str
|
project_channel_title: str
|
||||||
external_channel_id: uuid.UUID
|
placement_channel_id: uuid.UUID
|
||||||
external_channel_title: str
|
placement_channel_title: str
|
||||||
creative_id: uuid.UUID
|
creative_id: uuid.UUID
|
||||||
creative_name: str
|
creative_name: str
|
||||||
placement_date: datetime.datetime
|
placement_date: datetime.datetime
|
||||||
@@ -40,7 +40,7 @@ class CreativeAnalyticsOutput(pydantic.BaseModel):
|
|||||||
avg_cpm: float | None
|
avg_cpm: float | None
|
||||||
|
|
||||||
|
|
||||||
class ExternalChannelAnalyticsOutput(pydantic.BaseModel):
|
class ChannelAnalyticsOutput(pydantic.BaseModel):
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
title: str
|
title: str
|
||||||
username: str | None
|
username: str | None
|
||||||
@@ -55,7 +55,7 @@ class ExternalChannelAnalyticsOutput(pydantic.BaseModel):
|
|||||||
class GetPlacementsAnalyticsInput(pydantic.BaseModel):
|
class GetPlacementsAnalyticsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
project_id: uuid.UUID | None = None
|
||||||
|
|
||||||
|
|
||||||
class GetPlacementsAnalyticsOutput(pydantic.BaseModel):
|
class GetPlacementsAnalyticsOutput(pydantic.BaseModel):
|
||||||
@@ -65,21 +65,21 @@ class GetPlacementsAnalyticsOutput(pydantic.BaseModel):
|
|||||||
class GetCreativesAnalyticsInput(pydantic.BaseModel):
|
class GetCreativesAnalyticsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
project_id: uuid.UUID | None = None
|
||||||
|
|
||||||
|
|
||||||
class GetCreativesAnalyticsOutput(pydantic.BaseModel):
|
class GetCreativesAnalyticsOutput(pydantic.BaseModel):
|
||||||
creatives: list[CreativeAnalyticsOutput]
|
creatives: list[CreativeAnalyticsOutput]
|
||||||
|
|
||||||
|
|
||||||
class GetExternalChannelsAnalyticsInput(pydantic.BaseModel):
|
class GetChannelAnalyticsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
project_id: uuid.UUID | None = None
|
||||||
|
|
||||||
|
|
||||||
class GetExternalChannelsAnalyticsOutput(pydantic.BaseModel):
|
class GetChannelAnalyticsOutput(pydantic.BaseModel):
|
||||||
external_channels: list[ExternalChannelAnalyticsOutput]
|
channels: list[ChannelAnalyticsOutput]
|
||||||
|
|
||||||
|
|
||||||
class SpendingDataPoint(pydantic.BaseModel):
|
class SpendingDataPoint(pydantic.BaseModel):
|
||||||
@@ -94,7 +94,7 @@ class SpendingDataPoint(pydantic.BaseModel):
|
|||||||
class GetSpendingAnalyticsInput(pydantic.BaseModel):
|
class GetSpendingAnalyticsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
project_id: uuid.UUID | None = None
|
||||||
date_from: datetime.datetime | None = None
|
date_from: datetime.datetime | None = None
|
||||||
date_to: datetime.datetime | None = None
|
date_to: datetime.datetime | None = None
|
||||||
grouping: DateGrouping = DateGrouping.DAY
|
grouping: DateGrouping = DateGrouping.DAY
|
||||||
|
|||||||
21
src/dto/channel.py
Normal file
21
src/dto/channel.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
|
||||||
|
from src.domain.channel import ChannelVerificationStatus
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelOutput(pydantic.BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
telegram_id: int | None
|
||||||
|
title: str | None
|
||||||
|
username: str | None
|
||||||
|
verification_status: ChannelVerificationStatus
|
||||||
|
|
||||||
|
|
||||||
|
class GetChannelsInput(pydantic.BaseModel):
|
||||||
|
username: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class GetChannelsOutput(pydantic.BaseModel):
|
||||||
|
channels: list[ChannelOutput]
|
||||||
@@ -10,8 +10,8 @@ class CreativeOutput(pydantic.BaseModel):
|
|||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
name: str
|
name: str
|
||||||
text: str
|
text: str
|
||||||
target_channel_id: uuid.UUID
|
project_id: uuid.UUID
|
||||||
target_channel_title: str
|
project_channel_title: str
|
||||||
created_at: datetime.datetime
|
created_at: datetime.datetime
|
||||||
status: domain.CreativeStatus
|
status: domain.CreativeStatus
|
||||||
placements_count: int
|
placements_count: int
|
||||||
@@ -20,7 +20,7 @@ class CreativeOutput(pydantic.BaseModel):
|
|||||||
class GetCreativesInput(pydantic.BaseModel):
|
class GetCreativesInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
project_id: uuid.UUID | None = None
|
||||||
include_archived: bool = False
|
include_archived: bool = False
|
||||||
|
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class GetCreativeInput(pydantic.BaseModel):
|
|||||||
class CreateCreativeInput(pydantic.BaseModel):
|
class CreateCreativeInput(pydantic.BaseModel):
|
||||||
name: str
|
name: str
|
||||||
text: str
|
text: str
|
||||||
target_channel_id: uuid.UUID
|
project_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
class UpdateCreativeInput(pydantic.BaseModel):
|
class UpdateCreativeInput(pydantic.BaseModel):
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import uuid
|
|
||||||
|
|
||||||
import pydantic
|
|
||||||
|
|
||||||
|
|
||||||
class ExternalChannelOutput(pydantic.BaseModel):
|
|
||||||
id: uuid.UUID
|
|
||||||
telegram_id: int
|
|
||||||
title: str
|
|
||||||
username: str | None
|
|
||||||
description: str | None
|
|
||||||
subscribers_count: int | None
|
|
||||||
|
|
||||||
|
|
||||||
class GetExternalChannelsInput(pydantic.BaseModel):
|
|
||||||
target_channel_id: uuid.UUID
|
|
||||||
user_id: uuid.UUID
|
|
||||||
workspace_id: uuid.UUID
|
|
||||||
|
|
||||||
|
|
||||||
class GetExternalChannelsOutput(pydantic.BaseModel):
|
|
||||||
external_channels: list[ExternalChannelOutput]
|
|
||||||
|
|
||||||
|
|
||||||
class CreateExternalChannelInput(pydantic.BaseModel):
|
|
||||||
telegram_id: int
|
|
||||||
title: str
|
|
||||||
username: str | None = None
|
|
||||||
description: str | None = None
|
|
||||||
subscribers_count: int | None = None
|
|
||||||
target_channel_ids: list[uuid.UUID]
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteExternalChannelInput(pydantic.BaseModel):
|
|
||||||
channel_id: uuid.UUID
|
|
||||||
user_id: uuid.UUID
|
|
||||||
workspace_id: uuid.UUID
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateExternalChannelInput(pydantic.BaseModel):
|
|
||||||
title: str | None = None
|
|
||||||
username: str | None = None
|
|
||||||
description: str | None = None
|
|
||||||
subscribers_count: int | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateExternalChannelLinksInput(pydantic.BaseModel):
|
|
||||||
add_target_channel_ids: list[uuid.UUID] = []
|
|
||||||
remove_target_channel_ids: list[uuid.UUID] = []
|
|
||||||
@@ -8,10 +8,10 @@ from src import domain
|
|||||||
|
|
||||||
class PlacementOutput(pydantic.BaseModel):
|
class PlacementOutput(pydantic.BaseModel):
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID
|
project_id: uuid.UUID
|
||||||
target_channel_title: str
|
project_channel_title: str
|
||||||
external_channel_id: uuid.UUID
|
placement_channel_id: uuid.UUID
|
||||||
external_channel_title: str
|
placement_channel_title: str
|
||||||
creative_id: uuid.UUID
|
creative_id: uuid.UUID
|
||||||
creative_name: str
|
creative_name: str
|
||||||
placement_date: datetime.datetime
|
placement_date: datetime.datetime
|
||||||
@@ -31,8 +31,8 @@ class PlacementOutput(pydantic.BaseModel):
|
|||||||
class GetPlacementsInput(pydantic.BaseModel):
|
class GetPlacementsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
project_id: uuid.UUID | None = None
|
||||||
external_channel_id: uuid.UUID | None = None
|
placement_channel_id: uuid.UUID | None = None
|
||||||
creative_id: uuid.UUID | None = None
|
creative_id: uuid.UUID | None = None
|
||||||
include_archived: bool = False
|
include_archived: bool = False
|
||||||
|
|
||||||
@@ -48,8 +48,8 @@ class GetPlacementInput(pydantic.BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CreatePlacementInput(pydantic.BaseModel):
|
class CreatePlacementInput(pydantic.BaseModel):
|
||||||
target_channel_id: uuid.UUID
|
project_id: uuid.UUID
|
||||||
external_channel_id: uuid.UUID
|
placement_channel_id: uuid.UUID
|
||||||
creative_id: uuid.UUID
|
creative_id: uuid.UUID
|
||||||
placement_date: datetime.datetime
|
placement_date: datetime.datetime
|
||||||
cost: float | None = None
|
cost: float | None = None
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import uuid
|
|||||||
|
|
||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
from src.domain.target_channel import TargetChannelStatus
|
from src.domain.project import ProjectStatus
|
||||||
|
|
||||||
|
|
||||||
class ChannelBotPermissions(pydantic.BaseModel):
|
class ChannelBotPermissions(pydantic.BaseModel):
|
||||||
@@ -17,7 +17,7 @@ class ChannelBotPermissions(pydantic.BaseModel):
|
|||||||
can_pin_messages: bool | None = None
|
can_pin_messages: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
class ConnectTargetChanInput(pydantic.BaseModel):
|
class ConnectProjectInput(pydantic.BaseModel):
|
||||||
telegram_id: int
|
telegram_id: int
|
||||||
title: str
|
title: str
|
||||||
username: str | None
|
username: str | None
|
||||||
@@ -25,31 +25,29 @@ class ConnectTargetChanInput(pydantic.BaseModel):
|
|||||||
bot_permissions: ChannelBotPermissions
|
bot_permissions: ChannelBotPermissions
|
||||||
|
|
||||||
|
|
||||||
class ConnectTargetChanOutput(pydantic.BaseModel):
|
class ProjectOutput(pydantic.BaseModel):
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
telegram_id: int
|
telegram_id: int
|
||||||
title: str
|
title: str
|
||||||
username: str | None
|
username: str | None
|
||||||
status: TargetChannelStatus
|
status: ProjectStatus
|
||||||
# Deprecated: используйте status. Оставлено для обратной совместимости с фронтендом
|
|
||||||
is_active: bool
|
|
||||||
|
|
||||||
|
|
||||||
class GetUserTargetChansInput(pydantic.BaseModel):
|
class GetWorkspaceProjectsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
class GetUserTargetChansOutput(pydantic.BaseModel):
|
class GetWorkspaceProjectsOutput(pydantic.BaseModel):
|
||||||
target_channels: list[ConnectTargetChanOutput]
|
projects: list[ProjectOutput]
|
||||||
|
|
||||||
|
|
||||||
class DisconnectTargetChanByTgIdInput(pydantic.BaseModel):
|
class DisconnectProjectByTgIdInput(pydantic.BaseModel):
|
||||||
telegram_id: int
|
telegram_id: int
|
||||||
user_telegram_id: int
|
user_telegram_id: int
|
||||||
|
|
||||||
|
|
||||||
class UpdateTargetChanPermissionsInput(pydantic.BaseModel):
|
class UpdateProjectPermissionsInput(pydantic.BaseModel):
|
||||||
telegram_id: int
|
telegram_id: int
|
||||||
permissions: ChannelBotPermissions
|
permissions: ChannelBotPermissions
|
||||||
chat_title: str
|
chat_title: str
|
||||||
38
src/dto/purchase_plan.py
Normal file
38
src/dto/purchase_plan.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
|
||||||
|
from src.domain.purchase_plan import PurchasePlanChannelStatus
|
||||||
|
|
||||||
|
from .channel import ChannelOutput
|
||||||
|
|
||||||
|
|
||||||
|
class PurchasePlanChannelOutput(pydantic.BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
status: PurchasePlanChannelStatus
|
||||||
|
planned_cost: float | None = None
|
||||||
|
comment: str | None = None
|
||||||
|
channel: ChannelOutput
|
||||||
|
|
||||||
|
|
||||||
|
class GetPurchasePlanChannelsInput(pydantic.BaseModel):
|
||||||
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
project_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
|
class GetPurchasePlanChannelsOutput(pydantic.BaseModel):
|
||||||
|
channels: list[PurchasePlanChannelOutput]
|
||||||
|
|
||||||
|
|
||||||
|
class AttachChannelToPurchasePlanInput(pydantic.BaseModel):
|
||||||
|
username: str
|
||||||
|
status: PurchasePlanChannelStatus | None = None
|
||||||
|
planned_cost: float | None = None
|
||||||
|
comment: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePurchasePlanChannelInput(pydantic.BaseModel):
|
||||||
|
status: PurchasePlanChannelStatus | None = None
|
||||||
|
planned_cost: float | None = None
|
||||||
|
comment: str | None = None
|
||||||
@@ -8,14 +8,15 @@ if typing.TYPE_CHECKING:
|
|||||||
|
|
||||||
from src import domain
|
from src import domain
|
||||||
|
|
||||||
|
from .analytics.get_channel_analytics import get_channel_analytics
|
||||||
from .analytics.get_creatives_analytics import get_creatives_analytics
|
from .analytics.get_creatives_analytics import get_creatives_analytics
|
||||||
from .analytics.get_external_channels_analytics import get_external_channels_analytics
|
|
||||||
from .analytics.get_placements_analytics import get_placements_analytics
|
from .analytics.get_placements_analytics import get_placements_analytics
|
||||||
from .analytics.get_spending_analytics import get_spending_analytics
|
from .analytics.get_spending_analytics import get_spending_analytics
|
||||||
from .auth.get_me import get_me
|
from .auth.get_me import get_me
|
||||||
from .auth.telegram_login import telegram_login
|
from .auth.telegram_login import telegram_login
|
||||||
from .auth.telegram_start import telegram_start
|
from .auth.telegram_start import telegram_start
|
||||||
from .auth.validate_login_token import validate_login_token
|
from .auth.validate_login_token import validate_login_token
|
||||||
|
from .channel.get_channels import get_channels
|
||||||
from .creative.create_creative import create_creative
|
from .creative.create_creative import create_creative
|
||||||
from .creative.delete_creative import delete_creative
|
from .creative.delete_creative import delete_creative
|
||||||
from .creative.get_creative import get_creative
|
from .creative.get_creative import get_creative
|
||||||
@@ -25,23 +26,22 @@ from .creative.tg_create_creative_2 import tg_create_creative_2
|
|||||||
from .creative.tg_create_creative_3 import tg_create_creative_3
|
from .creative.tg_create_creative_3 import tg_create_creative_3
|
||||||
from .creative.tg_create_creative_4 import tg_create_creative_4
|
from .creative.tg_create_creative_4 import tg_create_creative_4
|
||||||
from .creative.update_creative import update_creative
|
from .creative.update_creative import update_creative
|
||||||
from .external_channel.create_external_channel import create_external_channel
|
|
||||||
from .external_channel.delete_external_channel import delete_external_channel
|
|
||||||
from .external_channel.get_external_channels import get_external_channels
|
|
||||||
from .external_channel.update_external_channel import update_external_channel
|
|
||||||
from .external_channel.update_external_channel_links import update_external_channel_links
|
|
||||||
from .placement.create_placement import create_placement
|
from .placement.create_placement import create_placement
|
||||||
from .placement.delete_placement import delete_placement
|
from .placement.delete_placement import delete_placement
|
||||||
from .placement.get_placement import get_placement
|
from .placement.get_placement import get_placement
|
||||||
from .placement.get_placements import get_placements
|
from .placement.get_placements import get_placements
|
||||||
from .placement.update_placement import update_placement
|
from .placement.update_placement import update_placement
|
||||||
|
from .project.disconnect_project_by_tg_id import disconnect_project_by_tg_id
|
||||||
|
from .project.get_workspace_projects import get_workspace_projects
|
||||||
|
from .project.tg_add_project_1 import tg_add_project_1
|
||||||
|
from .project.tg_add_project_2 import tg_add_project_2
|
||||||
|
from .project.update_project_permissions import update_project_permissions
|
||||||
|
from .purchase_plan.attach_channel_to_purchase_plan import attach_channel_to_purchase_plan
|
||||||
|
from .purchase_plan.get_purchase_plan_channels import get_purchase_plan_channels
|
||||||
|
from .purchase_plan.remove_purchase_plan_channel import remove_purchase_plan_channel
|
||||||
|
from .purchase_plan.update_purchase_plan_channel import update_purchase_plan_channel
|
||||||
from .subscription.handle_subscription import handle_subscription
|
from .subscription.handle_subscription import handle_subscription
|
||||||
from .subscription.handle_unsubscription import handle_unsubscription
|
from .subscription.handle_unsubscription import handle_unsubscription
|
||||||
from .target_channel.disconnect_target_chan_by_tg_id import disconnect_target_chan_by_tg_id
|
|
||||||
from .target_channel.get_user_target_chans import get_user_target_chans
|
|
||||||
from .target_channel.tg_add_target_chan_1 import tg_add_target_chan_1
|
|
||||||
from .target_channel.tg_add_target_chan_2 import tg_add_target_chan_2
|
|
||||||
from .target_channel.update_target_chan_permissions import update_target_chan_permissions
|
|
||||||
from .views.get_views_history import get_views_history
|
from .views.get_views_history import get_views_history
|
||||||
from .workspace.create_workspace import create_workspace
|
from .workspace.create_workspace import create_workspace
|
||||||
from .workspace.delete_workspace import delete_workspace
|
from .workspace.delete_workspace import delete_workspace
|
||||||
@@ -82,46 +82,74 @@ class Database(typing.Protocol):
|
|||||||
|
|
||||||
async def mark_token_as_used(self, token: str) -> None: ...
|
async def mark_token_as_used(self, token: str) -> None: ...
|
||||||
|
|
||||||
async def get_target_channel(
|
async def update_user(self, user: domain.User) -> None: ...
|
||||||
self, workspace_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
|
|
||||||
) -> domain.TargetChannel | None: ...
|
|
||||||
|
|
||||||
async def get_target_channel_for_user_by_telegram(
|
async def get_channel(
|
||||||
self, user_id: UUID, telegram_id: int
|
self,
|
||||||
) -> domain.TargetChannel | None: ...
|
workspace_id: UUID,
|
||||||
|
channel_id: UUID | None = None,
|
||||||
|
telegram_id: int | None = None,
|
||||||
|
username: str | None = None,
|
||||||
|
) -> domain.Channel | None: ...
|
||||||
|
|
||||||
async def upsert_target_channel(self, channel: domain.TargetChannel) -> None: ...
|
async def get_workspace_channels(self, workspace_id: UUID) -> list[domain.Channel]: ...
|
||||||
|
|
||||||
async def get_workspace_target_channels(self, workspace_id: UUID) -> list[domain.TargetChannel]: ...
|
async def create_channel(self, channel: domain.Channel) -> None: ...
|
||||||
|
|
||||||
async def update_target_channel(self, channel: domain.TargetChannel) -> None: ...
|
async def update_channel(self, channel: domain.Channel) -> None: ...
|
||||||
|
|
||||||
async def get_external_channel(
|
async def delete_channel(self, channel_id: UUID) -> None: ...
|
||||||
self, workspace_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
|
|
||||||
) -> domain.ExternalChannel | None: ...
|
|
||||||
|
|
||||||
async def create_external_channel(self, channel: domain.ExternalChannel) -> None: ...
|
async def search_channels(self, username_query: str | None = None) -> list[domain.Channel]: ...
|
||||||
|
|
||||||
async def get_external_channels_for_target(self, target_channel_id: UUID) -> list[domain.ExternalChannel]: ...
|
async def get_project(
|
||||||
|
self, workspace_id: UUID, project_id: UUID | None = None, channel_id: UUID | None = None
|
||||||
|
) -> domain.Project | None: ...
|
||||||
|
|
||||||
async def get_workspace_external_channels(self, workspace_id: UUID) -> list[domain.ExternalChannel]: ...
|
async def get_project_for_user_by_telegram(
|
||||||
|
self, user_id: UUID, channel_telegram_id: int
|
||||||
|
) -> domain.Project | None: ...
|
||||||
|
|
||||||
async def add_external_channel_to_targets(
|
async def get_project_by_channel_telegram(self, channel_telegram_id: int) -> domain.Project | None: ...
|
||||||
self, external_channel: domain.ExternalChannel, target_channel_ids: list[UUID]
|
|
||||||
) -> None: ...
|
|
||||||
|
|
||||||
async def remove_external_channel_from_target(self, external_channel_id: UUID, target_channel_id: UUID) -> None: ...
|
async def create_project(self, project: domain.Project) -> None: ...
|
||||||
|
|
||||||
async def delete_external_channel(self, channel_id: UUID) -> None: ...
|
async def update_project(self, project: domain.Project) -> None: ...
|
||||||
|
|
||||||
async def update_external_channel(self, channel: domain.ExternalChannel) -> None: ...
|
async def get_workspace_projects(self, workspace_id: UUID) -> list[domain.Project]: ...
|
||||||
|
|
||||||
|
async def get_active_purchase_plan(self, project_id: UUID) -> domain.PurchasePlan | None: ...
|
||||||
|
|
||||||
|
async def get_purchase_plan(self, workspace_id: UUID, plan_id: UUID) -> domain.PurchasePlan | None: ...
|
||||||
|
|
||||||
|
async def create_purchase_plan(self, plan: domain.PurchasePlan) -> None: ...
|
||||||
|
|
||||||
|
async def get_purchase_plan_channels(self, plan_id: UUID) -> list[domain.PurchasePlanChannel]: ...
|
||||||
|
|
||||||
|
async def get_purchase_plan_channel(
|
||||||
|
self, plan_channel_id: UUID, plan_id: UUID
|
||||||
|
) -> domain.PurchasePlanChannel | None: ...
|
||||||
|
|
||||||
|
async def add_channel_to_purchase_plan(
|
||||||
|
self,
|
||||||
|
plan_id: UUID,
|
||||||
|
channel_id: UUID,
|
||||||
|
*,
|
||||||
|
status: domain.PurchasePlanChannelStatus | None = None,
|
||||||
|
planned_cost: float | None = None,
|
||||||
|
comment: str | None = None,
|
||||||
|
) -> domain.PurchasePlanChannel: ...
|
||||||
|
|
||||||
|
async def update_purchase_plan_channel(self, plan_channel: domain.PurchasePlanChannel) -> None: ...
|
||||||
|
|
||||||
|
async def remove_channel_from_purchase_plan(self, plan_id: UUID, channel_id: UUID) -> None: ...
|
||||||
|
|
||||||
async def get_creative(self, workspace_id: UUID, creative_id: UUID) -> domain.Creative | None: ...
|
async def get_creative(self, workspace_id: UUID, creative_id: UUID) -> domain.Creative | None: ...
|
||||||
|
|
||||||
async def update_creative(self, creative: domain.Creative) -> None: ...
|
async def update_creative(self, creative: domain.Creative) -> None: ...
|
||||||
|
|
||||||
async def get_workspace_creatives(
|
async def get_workspace_creatives(
|
||||||
self, workspace_id: UUID, target_channel_id: UUID | None = None, include_archived: bool = False
|
self, workspace_id: UUID, project_id: UUID | None = None, include_archived: bool = False
|
||||||
) -> list[domain.Creative]: ...
|
) -> list[domain.Creative]: ...
|
||||||
|
|
||||||
async def delete_creative(self, creative_id: UUID) -> None: ...
|
async def delete_creative(self, creative_id: UUID) -> None: ...
|
||||||
@@ -135,8 +163,8 @@ class Database(typing.Protocol):
|
|||||||
async def get_workspace_placements(
|
async def get_workspace_placements(
|
||||||
self,
|
self,
|
||||||
workspace_id: UUID,
|
workspace_id: UUID,
|
||||||
target_channel_id: UUID | None = None,
|
project_id: UUID | None = None,
|
||||||
external_channel_id: UUID | None = None,
|
placement_channel_id: UUID | None = None,
|
||||||
creative_id: UUID | None = None,
|
creative_id: UUID | None = None,
|
||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
) -> list[domain.Placement]: ...
|
) -> list[domain.Placement]: ...
|
||||||
@@ -145,10 +173,6 @@ class Database(typing.Protocol):
|
|||||||
|
|
||||||
async def get_placement_by_invite_link(self, invite_link: str) -> domain.Placement | None: ...
|
async def get_placement_by_invite_link(self, invite_link: str) -> domain.Placement | None: ...
|
||||||
|
|
||||||
async def get_subscriber(self, telegram_id: int) -> domain.Subscriber | None: ...
|
|
||||||
|
|
||||||
async def upsert_subscriber(self, subscriber: domain.Subscriber) -> None: ...
|
|
||||||
|
|
||||||
async def create_subscription(self, subscription: domain.Subscription) -> None: ...
|
async def create_subscription(self, subscription: domain.Subscription) -> None: ...
|
||||||
|
|
||||||
async def get_subscription_by_subscriber_and_placement(
|
async def get_subscription_by_subscriber_and_placement(
|
||||||
@@ -157,12 +181,12 @@ class Database(typing.Protocol):
|
|||||||
|
|
||||||
async def update_subscription(self, subscription: domain.Subscription) -> None: ...
|
async def update_subscription(self, subscription: domain.Subscription) -> None: ...
|
||||||
|
|
||||||
async def get_active_subscriptions_by_subscriber_and_channel(
|
async def get_active_subscriptions_by_subscriber_and_project(
|
||||||
self, subscriber_id: UUID, channel_telegram_id: int
|
self, subscriber_id: UUID, project_id: UUID
|
||||||
) -> list[domain.Subscription]: ...
|
) -> list[domain.Subscription]: ...
|
||||||
|
|
||||||
async def get_active_subscription_by_subscriber_and_channel(
|
async def get_active_subscription_by_subscriber_and_project(
|
||||||
self, subscriber_id: UUID, channel_telegram_id: int
|
self, subscriber_id: UUID, project_id: UUID
|
||||||
) -> domain.Subscription | None: ...
|
) -> domain.Subscription | None: ...
|
||||||
|
|
||||||
async def create_placement_views_history(self, history: domain.PlacementViewsHistory) -> None: ...
|
async def create_placement_views_history(self, history: domain.PlacementViewsHistory) -> None: ...
|
||||||
@@ -229,20 +253,29 @@ class Usecase:
|
|||||||
|
|
||||||
return workspace
|
return workspace
|
||||||
|
|
||||||
|
async def ensure_active_purchase_plan(self, project: domain.Project) -> domain.PurchasePlan:
|
||||||
|
plan = await self.database.get_active_purchase_plan(project.id)
|
||||||
|
if plan:
|
||||||
|
return plan
|
||||||
|
|
||||||
|
plan = domain.PurchasePlan(workspace_id=project.workspace_id, project_id=project.id)
|
||||||
|
await self.database.create_purchase_plan(plan)
|
||||||
|
return plan
|
||||||
|
|
||||||
validate_login_token = validate_login_token
|
validate_login_token = validate_login_token
|
||||||
telegram_login = telegram_login
|
telegram_login = telegram_login
|
||||||
telegram_start = telegram_start
|
telegram_start = telegram_start
|
||||||
get_me = get_me
|
get_me = get_me
|
||||||
tg_add_target_chan_1 = tg_add_target_chan_1
|
tg_add_project_1 = tg_add_project_1
|
||||||
tg_add_target_chan_2 = tg_add_target_chan_2
|
tg_add_project_2 = tg_add_project_2
|
||||||
get_user_target_chans = get_user_target_chans
|
get_workspace_projects = get_workspace_projects
|
||||||
disconnect_target_chan_by_tg_id = disconnect_target_chan_by_tg_id
|
disconnect_project_by_tg_id = disconnect_project_by_tg_id
|
||||||
update_target_chan_permissions = update_target_chan_permissions
|
update_project_permissions = update_project_permissions
|
||||||
get_external_channels = get_external_channels
|
get_channels = get_channels
|
||||||
create_external_channel = create_external_channel
|
attach_channel_to_purchase_plan = attach_channel_to_purchase_plan
|
||||||
delete_external_channel = delete_external_channel
|
get_purchase_plan_channels = get_purchase_plan_channels
|
||||||
update_external_channel = update_external_channel
|
update_purchase_plan_channel = update_purchase_plan_channel
|
||||||
update_external_channel_links = update_external_channel_links
|
remove_purchase_plan_channel = remove_purchase_plan_channel
|
||||||
get_creatives = get_creatives
|
get_creatives = get_creatives
|
||||||
get_creative = get_creative
|
get_creative = get_creative
|
||||||
create_creative = create_creative
|
create_creative = create_creative
|
||||||
@@ -262,7 +295,7 @@ class Usecase:
|
|||||||
get_views_history = get_views_history
|
get_views_history = get_views_history
|
||||||
get_placements_analytics = get_placements_analytics
|
get_placements_analytics = get_placements_analytics
|
||||||
get_creatives_analytics = get_creatives_analytics
|
get_creatives_analytics = get_creatives_analytics
|
||||||
get_external_channels_analytics = get_external_channels_analytics
|
get_channel_analytics = get_channel_analytics
|
||||||
get_spending_analytics = get_spending_analytics
|
get_spending_analytics = get_spending_analytics
|
||||||
get_workspaces = get_workspaces
|
get_workspaces = get_workspaces
|
||||||
create_workspace = create_workspace
|
create_workspace = create_workspace
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
from .get_creatives_analytics import get_creatives_analytics
|
|
||||||
from .get_external_channels_analytics import get_external_channels_analytics
|
|
||||||
from .get_placements_analytics import get_placements_analytics
|
|
||||||
from .get_spending_analytics import get_spending_analytics
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'get_placements_analytics',
|
|
||||||
'get_creatives_analytics',
|
|
||||||
'get_external_channels_analytics',
|
|
||||||
'get_spending_analytics',
|
|
||||||
)
|
|
||||||
@@ -11,21 +11,21 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def get_external_channels_analytics(
|
async def get_channel_analytics(self: 'Usecase', input: dto.GetChannelAnalyticsInput) -> dto.GetChannelAnalyticsOutput:
|
||||||
self: 'Usecase', input: dto.GetExternalChannelsAnalyticsInput
|
|
||||||
) -> dto.GetExternalChannelsAnalyticsOutput:
|
|
||||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
if input.target_channel_id:
|
if input.project_id:
|
||||||
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
project = await self.database.get_project(input.workspace_id, input.project_id)
|
||||||
if not target_channel:
|
if not project:
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.ProjectNotFound(input.project_id)
|
||||||
external_channels = await self.database.get_external_channels_for_target(input.target_channel_id)
|
plan = await self.ensure_active_purchase_plan(project)
|
||||||
|
plan_channels = await self.database.get_purchase_plan_channels(plan.id)
|
||||||
|
channels = [pc.channel for pc in plan_channels]
|
||||||
else:
|
else:
|
||||||
external_channels = await self.database.get_workspace_external_channels(input.workspace_id)
|
channels = await self.database.get_workspace_channels(input.workspace_id)
|
||||||
|
|
||||||
placements = await self.database.get_workspace_placements(
|
placements = await self.database.get_workspace_placements(
|
||||||
input.workspace_id, input.target_channel_id, include_archived=False
|
input.workspace_id, input.project_id, include_archived=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -35,10 +35,10 @@ async def get_external_channels_analytics(
|
|||||||
total_views: int = 0
|
total_views: int = 0
|
||||||
placements_count: int = 0
|
placements_count: int = 0
|
||||||
|
|
||||||
channel_stats: dict[UUID, ChannelStats] = {ch.id: ChannelStats() for ch in external_channels}
|
channel_stats: dict[UUID, ChannelStats] = {ch.id: ChannelStats() for ch in channels}
|
||||||
|
|
||||||
for p in placements:
|
for p in placements:
|
||||||
stats = channel_stats.get(p.external_channel_id)
|
stats = channel_stats.get(p.placement_channel_id)
|
||||||
if stats is None:
|
if stats is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ async def get_external_channels_analytics(
|
|||||||
stats.placements_count += 1
|
stats.placements_count += 1
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for ch in external_channels:
|
for ch in channels:
|
||||||
stats = channel_stats[ch.id]
|
stats = channel_stats[ch.id]
|
||||||
|
|
||||||
avg_cpf = (
|
avg_cpf = (
|
||||||
@@ -61,7 +61,7 @@ async def get_external_channels_analytics(
|
|||||||
)
|
)
|
||||||
|
|
||||||
result.append(
|
result.append(
|
||||||
dto.ExternalChannelAnalyticsOutput(
|
dto.ChannelAnalyticsOutput(
|
||||||
id=ch.id,
|
id=ch.id,
|
||||||
title=ch.title,
|
title=ch.title,
|
||||||
username=ch.username,
|
username=ch.username,
|
||||||
@@ -74,4 +74,4 @@ async def get_external_channels_analytics(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return dto.GetExternalChannelsAnalyticsOutput(external_channels=result)
|
return dto.GetChannelAnalyticsOutput(channels=result)
|
||||||
@@ -16,16 +16,16 @@ async def get_creatives_analytics(
|
|||||||
) -> dto.GetCreativesAnalyticsOutput:
|
) -> dto.GetCreativesAnalyticsOutput:
|
||||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
if input.target_channel_id:
|
if input.project_id:
|
||||||
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
project = await self.database.get_project(input.workspace_id, input.project_id)
|
||||||
if not target_channel:
|
if not project:
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.ProjectNotFound(input.project_id)
|
||||||
|
|
||||||
creatives = await self.database.get_workspace_creatives(
|
creatives = await self.database.get_workspace_creatives(
|
||||||
input.workspace_id, input.target_channel_id, include_archived=False
|
input.workspace_id, input.project_id, include_archived=False
|
||||||
)
|
)
|
||||||
placements = await self.database.get_workspace_placements(
|
placements = await self.database.get_workspace_placements(
|
||||||
input.workspace_id, input.target_channel_id, include_archived=False
|
input.workspace_id, input.project_id, include_archived=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -26,22 +26,22 @@ async def get_placements_analytics(
|
|||||||
) -> dto.GetPlacementsAnalyticsOutput:
|
) -> dto.GetPlacementsAnalyticsOutput:
|
||||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
if input.target_channel_id:
|
if input.project_id:
|
||||||
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
project = await self.database.get_project(input.workspace_id, input.project_id)
|
||||||
if not target_channel:
|
if not project:
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.ProjectNotFound(input.project_id)
|
||||||
|
|
||||||
placements = await self.database.get_workspace_placements(
|
placements = await self.database.get_workspace_placements(
|
||||||
input.workspace_id, input.target_channel_id, include_archived=False
|
input.workspace_id, input.project_id, include_archived=False
|
||||||
)
|
)
|
||||||
|
|
||||||
result = [
|
result = [
|
||||||
dto.PlacementAnalyticsOutput(
|
dto.PlacementAnalyticsOutput(
|
||||||
id=p.id,
|
id=p.id,
|
||||||
target_channel_id=p.target_channel_id,
|
project_id=p.project_id,
|
||||||
target_channel_title=p.target_channel.title,
|
project_channel_title=p.project.channel.title,
|
||||||
external_channel_id=p.external_channel_id,
|
placement_channel_id=p.placement_channel_id,
|
||||||
external_channel_title=p.external_channel.title,
|
placement_channel_title=p.placement_channel.title,
|
||||||
creative_id=p.creative_id,
|
creative_id=p.creative_id,
|
||||||
creative_name=p.creative.name,
|
creative_name=p.creative.name,
|
||||||
placement_date=p.placement_date,
|
placement_date=p.placement_date,
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ async def get_spending_analytics(
|
|||||||
) -> dto.GetSpendingAnalyticsOutput:
|
) -> dto.GetSpendingAnalyticsOutput:
|
||||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
if input.target_channel_id:
|
if input.project_id:
|
||||||
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
project = await self.database.get_project(input.workspace_id, input.project_id)
|
||||||
if not target_channel:
|
if not project:
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.ProjectNotFound(input.project_id)
|
||||||
|
|
||||||
placements = await self.database.get_workspace_placements(
|
placements = await self.database.get_workspace_placements(
|
||||||
input.workspace_id, input.target_channel_id, include_archived=False
|
input.workspace_id, input.project_id, include_archived=False
|
||||||
)
|
)
|
||||||
|
|
||||||
filtered = [
|
filtered = [
|
||||||
|
|||||||
28
src/usecase/channel/get_channels.py
Normal file
28
src/usecase/channel/get_channels.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import dto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_channels(self: 'Usecase', input: dto.GetChannelsInput) -> dto.GetChannelsOutput:
|
||||||
|
channels = await self.database.search_channels(username_query=input.username)
|
||||||
|
|
||||||
|
log.debug('Found %s channels for username query: %s', len(channels), input.username)
|
||||||
|
|
||||||
|
return dto.GetChannelsOutput(
|
||||||
|
channels=[
|
||||||
|
dto.ChannelOutput(
|
||||||
|
id=channel.id,
|
||||||
|
telegram_id=channel.telegram_id,
|
||||||
|
title=channel.title,
|
||||||
|
username=channel.username,
|
||||||
|
verification_status=channel.verification_status,
|
||||||
|
)
|
||||||
|
for channel in channels
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -15,16 +15,16 @@ async def create_creative(
|
|||||||
) -> dto.CreativeOutput:
|
) -> dto.CreativeOutput:
|
||||||
await self.ensure_workspace_access(workspace_id, user_id)
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
target_channel = await self.database.get_target_channel(workspace_id, channel_id=input.target_channel_id)
|
project = await self.database.get_project(workspace_id, project_id=input.project_id)
|
||||||
if not target_channel:
|
if not project:
|
||||||
log.warning('User %s attempted to create creative for unavailable target %s', user_id, input.target_channel_id)
|
log.warning('User %s attempted to create creative for unavailable project %s', user_id, input.project_id)
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.ProjectNotFound(input.project_id)
|
||||||
|
|
||||||
creative = domain.Creative(
|
creative = domain.Creative(
|
||||||
name=input.name,
|
name=input.name,
|
||||||
text=input.text,
|
text=input.text,
|
||||||
status=domain.CreativeStatus.ACTIVE,
|
status=domain.CreativeStatus.ACTIVE,
|
||||||
target_channel_id=target_channel.id,
|
project_id=project.id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
await self.database.create_creative(creative)
|
await self.database.create_creative(creative)
|
||||||
@@ -33,8 +33,8 @@ async def create_creative(
|
|||||||
id=creative.id,
|
id=creative.id,
|
||||||
name=creative.name,
|
name=creative.name,
|
||||||
text=creative.text,
|
text=creative.text,
|
||||||
target_channel_id=target_channel.id,
|
project_id=project.id,
|
||||||
target_channel_title=target_channel.title,
|
project_channel_title=project.channel.title,
|
||||||
created_at=creative.created_at,
|
created_at=creative.created_at,
|
||||||
status=creative.status,
|
status=creative.status,
|
||||||
placements_count=creative.placements_count,
|
placements_count=creative.placements_count,
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.Crea
|
|||||||
id=creative.id,
|
id=creative.id,
|
||||||
name=creative.name,
|
name=creative.name,
|
||||||
text=creative.text,
|
text=creative.text,
|
||||||
target_channel_id=creative.target_channel_id,
|
project_id=creative.project_id,
|
||||||
target_channel_title=creative.target_channel.title,
|
project_channel_title=creative.project.channel.title,
|
||||||
created_at=creative.created_at,
|
created_at=creative.created_at,
|
||||||
status=creative.status,
|
status=creative.status,
|
||||||
placements_count=creative.placements_count,
|
placements_count=creative.placements_count,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> dto.Ge
|
|||||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
creatives = await self.database.get_workspace_creatives(
|
creatives = await self.database.get_workspace_creatives(
|
||||||
input.workspace_id, input.target_channel_id, input.include_archived
|
input.workspace_id, input.project_id, input.include_archived
|
||||||
)
|
)
|
||||||
|
|
||||||
return dto.GetCreativesOutput(
|
return dto.GetCreativesOutput(
|
||||||
@@ -19,8 +19,8 @@ async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> dto.Ge
|
|||||||
id=creative.id,
|
id=creative.id,
|
||||||
name=creative.name,
|
name=creative.name,
|
||||||
text=creative.text,
|
text=creative.text,
|
||||||
target_channel_id=creative.target_channel_id,
|
project_id=creative.project_id,
|
||||||
target_channel_title=creative.target_channel.title,
|
project_channel_title=creative.project.channel.title,
|
||||||
created_at=creative.created_at,
|
created_at=creative.created_at,
|
||||||
status=creative.status,
|
status=creative.status,
|
||||||
placements_count=creative.placements_count,
|
placements_count=creative.placements_count,
|
||||||
|
|||||||
@@ -36,18 +36,23 @@ async def tg_create_creative_1(self: 'Usecase', telegram_id: int, chat_id: int)
|
|||||||
await self.database.clear_telegram_state(telegram_id)
|
await self.database.clear_telegram_state(telegram_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
channels = await self.database.get_workspace_target_channels(workspace.id)
|
projects = await self.database.get_workspace_projects(workspace.id)
|
||||||
if not channels:
|
if not projects:
|
||||||
await self.telegram_bot.send_message(
|
await self.telegram_bot.send_message(
|
||||||
'❌ В выбранном рабочем пространстве нет подключенных каналов. Добавьте канал и попробуйте снова.',
|
'❌ В выбранном рабочем пространстве нет подключенных проектов. Добавьте канал и попробуйте снова.',
|
||||||
chat_id,
|
chat_id,
|
||||||
)
|
)
|
||||||
await self.database.clear_telegram_state(telegram_id)
|
await self.database.clear_telegram_state(telegram_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton(text=channel.title, callback_data=f'tg_create_creative_3:{channel.id}')]
|
[
|
||||||
for channel in channels
|
InlineKeyboardButton(
|
||||||
|
text=project.channel.title or project.channel.username or 'Unnamed Channel',
|
||||||
|
callback_data=f'tg_create_creative_3:{project.id}',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for project in projects
|
||||||
]
|
]
|
||||||
|
|
||||||
await self.telegram_bot.send_message_with_inline_keyboard(
|
await self.telegram_bot.send_message_with_inline_keyboard(
|
||||||
|
|||||||
@@ -47,18 +47,23 @@ async def tg_create_creative_2(
|
|||||||
|
|
||||||
await self.telegram_bot.edit_message_reply_markup(chat_id, message_id)
|
await self.telegram_bot.edit_message_reply_markup(chat_id, message_id)
|
||||||
|
|
||||||
channels = await self.database.get_workspace_target_channels(workspace.id)
|
projects = await self.database.get_workspace_projects(workspace.id)
|
||||||
if not channels:
|
if not projects:
|
||||||
await self.telegram_bot.send_message(
|
await self.telegram_bot.send_message(
|
||||||
'❌ В выбранном рабочем пространстве нет подключенных каналов. Добавьте канал и попробуйте снова.',
|
'❌ В выбранном рабочем пространстве нет подключенных проектов. Добавьте канал и попробуйте снова.',
|
||||||
chat_id,
|
chat_id,
|
||||||
)
|
)
|
||||||
await self.database.clear_telegram_state(telegram_id)
|
await self.database.clear_telegram_state(telegram_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton(text=channel.title, callback_data=f'tg_create_creative_3:{channel.id}')]
|
[
|
||||||
for channel in channels
|
InlineKeyboardButton(
|
||||||
|
text=project.channel.title or project.channel.username or 'Unnamed Channel',
|
||||||
|
callback_data=f'tg_create_creative_3:{project.id}',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for project in projects
|
||||||
]
|
]
|
||||||
|
|
||||||
await self.telegram_bot.send_message_with_inline_keyboard(
|
await self.telegram_bot.send_message_with_inline_keyboard(
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ async def tg_create_creative_3(
|
|||||||
log.warning('Invalid callback data format: %s', callback_data)
|
log.warning('Invalid callback data format: %s', callback_data)
|
||||||
return
|
return
|
||||||
|
|
||||||
channel_id_str = callback_data.split(':', 1)[1]
|
project_id_str = callback_data.split(':', 1)[1]
|
||||||
try:
|
try:
|
||||||
channel_id = uuid.UUID(channel_id_str)
|
project_id = uuid.UUID(project_id_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
log.error('Invalid channel UUID in callback data: %s', channel_id_str)
|
log.error('Invalid project UUID in callback data: %s', project_id_str)
|
||||||
return
|
return
|
||||||
|
|
||||||
user = await self.database.get_user(telegram_id=telegram_id)
|
user = await self.database.get_user(telegram_id=telegram_id)
|
||||||
@@ -53,13 +53,13 @@ async def tg_create_creative_3(
|
|||||||
workspace_id = workspace.id
|
workspace_id = workspace.id
|
||||||
|
|
||||||
if not workspace_id:
|
if not workspace_id:
|
||||||
log.error('Workspace ID missing for user %s when selecting channel %s', telegram_id, channel_id)
|
log.error('Workspace ID missing for user %s when selecting project %s', telegram_id, project_id)
|
||||||
await self.telegram_bot.send_message('❌ Ошибка: рабочее пространство не найдено. Начните заново.', chat_id)
|
await self.telegram_bot.send_message('❌ Ошибка: рабочее пространство не найдено. Начните заново.', chat_id)
|
||||||
await self.database.clear_telegram_state(telegram_id)
|
await self.database.clear_telegram_state(telegram_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
channel = await self.database.get_target_channel(workspace_id, channel_id=channel_id)
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
if not channel:
|
if not project:
|
||||||
await self.telegram_bot.send_message('❌ Канал не найден или не принадлежит вам.', chat_id)
|
await self.telegram_bot.send_message('❌ Канал не найден или не принадлежит вам.', chat_id)
|
||||||
await self.database.clear_telegram_state(telegram_id)
|
await self.database.clear_telegram_state(telegram_id)
|
||||||
return
|
return
|
||||||
@@ -68,12 +68,14 @@ async def tg_create_creative_3(
|
|||||||
telegram_id,
|
telegram_id,
|
||||||
domain.TelegramStateEnum.CREATIVE_WAITING_NAME,
|
domain.TelegramStateEnum.CREATIVE_WAITING_NAME,
|
||||||
{
|
{
|
||||||
'target_channel_id': str(channel_id),
|
'project_id': str(project_id),
|
||||||
'channel_title': channel.title,
|
'channel_title': project.channel.title,
|
||||||
'workspace_id': str(workspace_id),
|
'workspace_id': str(workspace_id),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.telegram_bot.edit_message_reply_markup(chat_id, message_id)
|
await self.telegram_bot.edit_message_reply_markup(chat_id, message_id)
|
||||||
|
|
||||||
await self.telegram_bot.send_message(f'✅ Канал выбран: {channel.title}\n\n📝 Введите название креатива:', chat_id)
|
await self.telegram_bot.send_message(
|
||||||
|
f'✅ Канал выбран: {project.channel.title}\n\n📝 Введите название креатива:', chat_id
|
||||||
|
)
|
||||||
|
|||||||
@@ -38,20 +38,20 @@ async def tg_create_creative_4(
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif state.state == domain.TelegramStateEnum.CREATIVE_WAITING_TEXT:
|
elif state.state == domain.TelegramStateEnum.CREATIVE_WAITING_TEXT:
|
||||||
target_channel_id_str = state.context.get('target_channel_id')
|
project_id_str = state.context.get('project_id')
|
||||||
creative_name = state.context.get('creative_name')
|
creative_name = state.context.get('creative_name')
|
||||||
workspace_id_str = state.context.get('workspace_id')
|
workspace_id_str = state.context.get('workspace_id')
|
||||||
|
|
||||||
if not target_channel_id_str or not creative_name:
|
if not project_id_str or not creative_name:
|
||||||
log.error('Missing context data for creative creation: %s', state.context)
|
log.error('Missing context data for creative creation: %s', state.context)
|
||||||
await self.telegram_bot.send_message('❌ Ошибка: данные не найдены. Начните заново.', chat_id)
|
await self.telegram_bot.send_message('❌ Ошибка: данные не найдены. Начните заново.', chat_id)
|
||||||
await self.database.clear_telegram_state(telegram_id)
|
await self.database.clear_telegram_state(telegram_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
target_channel_id = uuid.UUID(target_channel_id_str)
|
project_id = uuid.UUID(project_id_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
log.error('Invalid target_channel_id in context: %s', target_channel_id_str)
|
log.error('Invalid project_id in context: %s', project_id_str)
|
||||||
await self.telegram_bot.send_message('❌ Ошибка: неверный формат данных.', chat_id)
|
await self.telegram_bot.send_message('❌ Ошибка: неверный формат данных.', chat_id)
|
||||||
await self.database.clear_telegram_state(telegram_id)
|
await self.database.clear_telegram_state(telegram_id)
|
||||||
return
|
return
|
||||||
@@ -78,8 +78,8 @@ async def tg_create_creative_4(
|
|||||||
await self.database.clear_telegram_state(telegram_id)
|
await self.database.clear_telegram_state(telegram_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
channel = await self.database.get_target_channel(workspace_id, channel_id=target_channel_id)
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
if not channel:
|
if not project:
|
||||||
await self.telegram_bot.send_message('❌ Канал не найден.', chat_id)
|
await self.telegram_bot.send_message('❌ Канал не найден.', chat_id)
|
||||||
await self.database.clear_telegram_state(telegram_id)
|
await self.database.clear_telegram_state(telegram_id)
|
||||||
return
|
return
|
||||||
@@ -92,7 +92,7 @@ async def tg_create_creative_4(
|
|||||||
text=creative_text,
|
text=creative_text,
|
||||||
status=domain.CreativeStatus.ACTIVE,
|
status=domain.CreativeStatus.ACTIVE,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
async with self.database.transaction():
|
async with self.database.transaction():
|
||||||
@@ -104,7 +104,7 @@ async def tg_create_creative_4(
|
|||||||
await self.telegram_bot.send_message(
|
await self.telegram_bot.send_message(
|
||||||
f'✅ Креатив успешно создан!\n\n'
|
f'✅ Креатив успешно создан!\n\n'
|
||||||
f'📝 Название: {creative.name}\n'
|
f'📝 Название: {creative.name}\n'
|
||||||
f'📢 Канал: {channel.title}\n\n'
|
f'📢 Канал: {project.channel.title}\n\n'
|
||||||
f'💬 Текст:\n{text_preview}',
|
f'💬 Текст:\n{text_preview}',
|
||||||
chat_id,
|
chat_id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ async def update_creative(
|
|||||||
id=creative.id,
|
id=creative.id,
|
||||||
name=creative.name,
|
name=creative.name,
|
||||||
text=creative.text,
|
text=creative.text,
|
||||||
target_channel_id=creative.target_channel_id,
|
project_id=creative.project_id,
|
||||||
target_channel_title=creative.target_channel.title,
|
project_channel_title=creative.project.channel.title,
|
||||||
created_at=creative.created_at,
|
created_at=creative.created_at,
|
||||||
status=creative.status,
|
status=creative.status,
|
||||||
placements_count=creative.placements_count,
|
placements_count=creative.placements_count,
|
||||||
|
|||||||
@@ -1,53 +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 create_external_channel(
|
|
||||||
self: 'Usecase', input: dto.CreateExternalChannelInput, user_id: uuid.UUID, workspace_id: uuid.UUID
|
|
||||||
) -> dto.ExternalChannelOutput:
|
|
||||||
await self.ensure_workspace_access(workspace_id, user_id)
|
|
||||||
|
|
||||||
existing_channel = await self.database.get_external_channel(workspace_id, telegram_id=input.telegram_id)
|
|
||||||
if existing_channel:
|
|
||||||
raise domain.ExternalChannelAlreadyExists(input.telegram_id)
|
|
||||||
|
|
||||||
for target_id in input.target_channel_ids:
|
|
||||||
target_channel = await self.database.get_target_channel(workspace_id, channel_id=target_id)
|
|
||||||
|
|
||||||
if not target_channel:
|
|
||||||
raise domain.TargetChannelNotFound(target_id)
|
|
||||||
|
|
||||||
async with self.database.transaction():
|
|
||||||
channel = domain.ExternalChannel(
|
|
||||||
telegram_id=input.telegram_id,
|
|
||||||
title=input.title,
|
|
||||||
username=input.username,
|
|
||||||
description=input.description,
|
|
||||||
subscribers_count=input.subscribers_count,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
)
|
|
||||||
await self.database.create_external_channel(channel)
|
|
||||||
|
|
||||||
if input.target_channel_ids:
|
|
||||||
await self.database.add_external_channel_to_targets(channel, input.target_channel_ids)
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'External channel %s created and linked to %s target channels', input.telegram_id, len(input.target_channel_ids)
|
|
||||||
)
|
|
||||||
|
|
||||||
return dto.ExternalChannelOutput(
|
|
||||||
id=channel.id,
|
|
||||||
telegram_id=channel.telegram_id,
|
|
||||||
title=channel.title,
|
|
||||||
username=channel.username,
|
|
||||||
description=channel.description,
|
|
||||||
subscribers_count=channel.subscribers_count,
|
|
||||||
)
|
|
||||||
@@ -1,20 +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_external_channel(self: 'Usecase', input: dto.DeleteExternalChannelInput) -> None:
|
|
||||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
|
||||||
|
|
||||||
channel = await self.database.get_external_channel(input.workspace_id, channel_id=input.channel_id)
|
|
||||||
if not channel:
|
|
||||||
raise domain.ExternalChannelNotFound(input.channel_id)
|
|
||||||
|
|
||||||
await self.database.delete_external_channel(input.channel_id)
|
|
||||||
log.info('User %s deleted external channel %s', input.user_id, input.channel_id)
|
|
||||||
@@ -1,34 +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 get_external_channels(self: 'Usecase', input: dto.GetExternalChannelsInput) -> dto.GetExternalChannelsOutput:
|
|
||||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
|
||||||
|
|
||||||
target_channel = await self.database.get_target_channel(input.workspace_id, channel_id=input.target_channel_id)
|
|
||||||
if not target_channel:
|
|
||||||
log.warning('Target channel %s not found or not accessible for user %s', input.target_channel_id, input.user_id)
|
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
|
||||||
|
|
||||||
channels = await self.database.get_external_channels_for_target(input.target_channel_id)
|
|
||||||
|
|
||||||
return dto.GetExternalChannelsOutput(
|
|
||||||
external_channels=[
|
|
||||||
dto.ExternalChannelOutput(
|
|
||||||
id=channel.id,
|
|
||||||
telegram_id=channel.telegram_id,
|
|
||||||
title=channel.title,
|
|
||||||
username=channel.username,
|
|
||||||
description=channel.description,
|
|
||||||
subscribers_count=channel.subscribers_count,
|
|
||||||
)
|
|
||||||
for channel in channels
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@@ -1,47 +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_external_channel(
|
|
||||||
self: 'Usecase',
|
|
||||||
channel_id: uuid.UUID,
|
|
||||||
input: dto.UpdateExternalChannelInput,
|
|
||||||
user_id: uuid.UUID,
|
|
||||||
workspace_id: uuid.UUID,
|
|
||||||
) -> dto.ExternalChannelOutput:
|
|
||||||
await self.ensure_workspace_access(workspace_id, user_id)
|
|
||||||
|
|
||||||
external_channel = await self.database.get_external_channel(workspace_id, channel_id=channel_id)
|
|
||||||
if not external_channel:
|
|
||||||
log.warning('External channel %s not found', channel_id)
|
|
||||||
raise domain.ExternalChannelNotFound(channel_id)
|
|
||||||
|
|
||||||
if input.title is not None:
|
|
||||||
external_channel.title = input.title
|
|
||||||
if input.username is not None:
|
|
||||||
external_channel.username = input.username
|
|
||||||
if input.description is not None:
|
|
||||||
external_channel.description = input.description
|
|
||||||
if input.subscribers_count is not None:
|
|
||||||
external_channel.subscribers_count = input.subscribers_count
|
|
||||||
|
|
||||||
await self.database.update_external_channel(external_channel)
|
|
||||||
|
|
||||||
log.info('External channel %s updated', channel_id)
|
|
||||||
|
|
||||||
return dto.ExternalChannelOutput(
|
|
||||||
id=external_channel.id,
|
|
||||||
telegram_id=external_channel.telegram_id,
|
|
||||||
title=external_channel.title,
|
|
||||||
username=external_channel.username,
|
|
||||||
description=external_channel.description,
|
|
||||||
subscribers_count=external_channel.subscribers_count,
|
|
||||||
)
|
|
||||||
@@ -1,54 +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_external_channel_links(
|
|
||||||
self: 'Usecase',
|
|
||||||
channel_id: uuid.UUID,
|
|
||||||
input: dto.UpdateExternalChannelLinksInput,
|
|
||||||
user_id: uuid.UUID,
|
|
||||||
workspace_id: uuid.UUID,
|
|
||||||
) -> dto.ExternalChannelOutput:
|
|
||||||
await self.ensure_workspace_access(workspace_id, user_id)
|
|
||||||
|
|
||||||
external_channel = await self.database.get_external_channel(workspace_id, channel_id=channel_id)
|
|
||||||
if not external_channel:
|
|
||||||
log.warning('External channel %s not found', channel_id)
|
|
||||||
raise domain.ExternalChannelNotFound(channel_id)
|
|
||||||
|
|
||||||
for target_id in input.add_target_channel_ids:
|
|
||||||
target_channel = await self.database.get_target_channel(workspace_id, channel_id=target_id)
|
|
||||||
if not target_channel:
|
|
||||||
log.warning('User %s attempted to link external channel to target %s they do not own', user_id, target_id)
|
|
||||||
raise domain.TargetChannelNotFound(target_id)
|
|
||||||
|
|
||||||
async with self.database.transaction():
|
|
||||||
if input.add_target_channel_ids:
|
|
||||||
await self.database.add_external_channel_to_targets(external_channel, input.add_target_channel_ids)
|
|
||||||
|
|
||||||
for target_id in input.remove_target_channel_ids:
|
|
||||||
await self.database.remove_external_channel_from_target(channel_id, target_id)
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'External channel %s links updated: %s added, %s removed',
|
|
||||||
channel_id,
|
|
||||||
len(input.add_target_channel_ids),
|
|
||||||
len(input.remove_target_channel_ids),
|
|
||||||
)
|
|
||||||
|
|
||||||
return dto.ExternalChannelOutput(
|
|
||||||
id=external_channel.id,
|
|
||||||
telegram_id=external_channel.telegram_id,
|
|
||||||
title=external_channel.title,
|
|
||||||
username=external_channel.username,
|
|
||||||
description=external_channel.description,
|
|
||||||
subscribers_count=external_channel.subscribers_count,
|
|
||||||
)
|
|
||||||
@@ -15,24 +15,30 @@ async def create_placement(
|
|||||||
) -> dto.PlacementOutput:
|
) -> dto.PlacementOutput:
|
||||||
await self.ensure_workspace_access(workspace_id, user_id)
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
target_channel = await self.database.get_target_channel(workspace_id, channel_id=input.target_channel_id)
|
project = await self.database.get_project(workspace_id, project_id=input.project_id)
|
||||||
if not target_channel:
|
if not project:
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.ProjectNotFound(input.project_id)
|
||||||
|
|
||||||
external_channel = await self.database.get_external_channel(workspace_id, channel_id=input.external_channel_id)
|
placement_channel = await self.database.get_channel(workspace_id, channel_id=input.placement_channel_id)
|
||||||
if not external_channel:
|
if not placement_channel:
|
||||||
raise domain.ExternalChannelNotFound(input.external_channel_id)
|
raise domain.ChannelNotFound(input.placement_channel_id)
|
||||||
|
|
||||||
creative = await self.database.get_creative(workspace_id, input.creative_id)
|
creative = await self.database.get_creative(workspace_id, input.creative_id)
|
||||||
if not creative:
|
if not creative:
|
||||||
raise domain.CreativeNotFound(input.creative_id)
|
raise domain.CreativeNotFound(input.creative_id)
|
||||||
|
if creative.project_id != project.id:
|
||||||
|
raise domain.CreativeNotFound(input.creative_id)
|
||||||
|
|
||||||
requires_approval = input.invite_link_type == domain.InviteLinkType.APPROVAL
|
requires_approval = input.invite_link_type == domain.InviteLinkType.APPROVAL
|
||||||
invite_link = await self.telegram_bot.create_chat_invite_link(target_channel.telegram_id, requires_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)
|
||||||
|
|
||||||
placement = domain.Placement(
|
placement = domain.Placement(
|
||||||
target_channel_id=input.target_channel_id,
|
project_id=input.project_id,
|
||||||
external_channel_id=input.external_channel_id,
|
placement_channel_id=input.placement_channel_id,
|
||||||
creative_id=input.creative_id,
|
creative_id=input.creative_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
placement_date=input.placement_date,
|
placement_date=input.placement_date,
|
||||||
@@ -51,10 +57,10 @@ async def create_placement(
|
|||||||
|
|
||||||
return dto.PlacementOutput(
|
return dto.PlacementOutput(
|
||||||
id=placement.id,
|
id=placement.id,
|
||||||
target_channel_id=placement.target_channel_id,
|
project_id=placement.project_id,
|
||||||
target_channel_title=target_channel.title,
|
project_channel_title=project.channel.title,
|
||||||
external_channel_id=placement.external_channel_id,
|
placement_channel_id=placement.placement_channel_id,
|
||||||
external_channel_title=external_channel.title,
|
placement_channel_title=placement_channel.title,
|
||||||
creative_id=placement.creative_id,
|
creative_id=placement.creative_id,
|
||||||
creative_name=creative.name,
|
creative_name=creative.name,
|
||||||
placement_date=placement.placement_date,
|
placement_date=placement.placement_date,
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl
|
|||||||
|
|
||||||
return dto.PlacementOutput(
|
return dto.PlacementOutput(
|
||||||
id=placement.id,
|
id=placement.id,
|
||||||
target_channel_id=placement.target_channel_id,
|
project_id=placement.project_id,
|
||||||
target_channel_title=placement.target_channel.title,
|
project_channel_title=placement.project.channel.title,
|
||||||
external_channel_id=placement.external_channel_id,
|
placement_channel_id=placement.placement_channel_id,
|
||||||
external_channel_title=placement.external_channel.title,
|
placement_channel_title=placement.placement_channel.title,
|
||||||
creative_id=placement.creative_id,
|
creative_id=placement.creative_id,
|
||||||
creative_name=placement.creative.name,
|
creative_name=placement.creative.name,
|
||||||
placement_date=placement.placement_date,
|
placement_date=placement.placement_date,
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
|
|||||||
|
|
||||||
placements = await self.database.get_workspace_placements(
|
placements = await self.database.get_workspace_placements(
|
||||||
input.workspace_id,
|
input.workspace_id,
|
||||||
target_channel_id=input.target_channel_id,
|
project_id=input.project_id,
|
||||||
external_channel_id=input.external_channel_id,
|
placement_channel_id=input.placement_channel_id,
|
||||||
creative_id=input.creative_id,
|
creative_id=input.creative_id,
|
||||||
include_archived=input.include_archived,
|
include_archived=input.include_archived,
|
||||||
)
|
)
|
||||||
@@ -21,10 +21,10 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
|
|||||||
placements=[
|
placements=[
|
||||||
dto.PlacementOutput(
|
dto.PlacementOutput(
|
||||||
id=placement.id,
|
id=placement.id,
|
||||||
target_channel_id=placement.target_channel_id,
|
project_id=placement.project_id,
|
||||||
target_channel_title=placement.target_channel.title,
|
project_channel_title=placement.project.channel.title,
|
||||||
external_channel_id=placement.external_channel_id,
|
placement_channel_id=placement.placement_channel_id,
|
||||||
external_channel_title=placement.external_channel.title,
|
placement_channel_title=placement.placement_channel.title,
|
||||||
creative_id=placement.creative_id,
|
creative_id=placement.creative_id,
|
||||||
creative_name=placement.creative.name,
|
creative_name=placement.creative.name,
|
||||||
placement_date=placement.placement_date,
|
placement_date=placement.placement_date,
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ async def update_placement(
|
|||||||
|
|
||||||
return dto.PlacementOutput(
|
return dto.PlacementOutput(
|
||||||
id=placement.id,
|
id=placement.id,
|
||||||
target_channel_id=placement.target_channel_id,
|
project_id=placement.project_id,
|
||||||
target_channel_title=placement.target_channel.title,
|
project_channel_title=placement.project.channel.title,
|
||||||
external_channel_id=placement.external_channel_id,
|
placement_channel_id=placement.placement_channel_id,
|
||||||
external_channel_title=placement.external_channel.title,
|
placement_channel_title=placement.placement_channel.title,
|
||||||
creative_id=placement.creative_id,
|
creative_id=placement.creative_id,
|
||||||
creative_name=placement.creative.name,
|
creative_name=placement.creative.name,
|
||||||
placement_date=placement.placement_date,
|
placement_date=placement.placement_date,
|
||||||
|
|||||||
26
src/usecase/project/disconnect_project_by_tg_id.py
Normal file
26
src/usecase/project/disconnect_project_by_tg_id.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import domain, dto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def disconnect_project_by_tg_id(self: 'Usecase', input: dto.DisconnectProjectByTgIdInput) -> None:
|
||||||
|
user = await self.database.get_user(telegram_id=input.user_telegram_id)
|
||||||
|
if not user:
|
||||||
|
log.warning(f'User with telegram_id {input.user_telegram_id} not found when disconnecting channel')
|
||||||
|
return
|
||||||
|
|
||||||
|
project = await self.database.get_project_for_user_by_telegram(user.id, input.telegram_id)
|
||||||
|
if not project:
|
||||||
|
log.warning(f'Project channel {input.telegram_id} not found')
|
||||||
|
raise domain.ProjectNotFound()
|
||||||
|
|
||||||
|
project.status = domain.ProjectStatus.ARCHIVED
|
||||||
|
await self.database.update_project(project)
|
||||||
|
|
||||||
|
log.info('Project %s archived for channel %s', project.id, input.telegram_id)
|
||||||
27
src/usecase/project/get_workspace_projects.py
Normal file
27
src/usecase/project/get_workspace_projects.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import dto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
|
||||||
|
async def get_workspace_projects(
|
||||||
|
self: 'Usecase', input: dto.GetWorkspaceProjectsInput
|
||||||
|
) -> dto.GetWorkspaceProjectsOutput:
|
||||||
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
|
projects = await self.database.get_workspace_projects(input.workspace_id)
|
||||||
|
|
||||||
|
return dto.GetWorkspaceProjectsOutput(
|
||||||
|
projects=[
|
||||||
|
dto.ProjectOutput(
|
||||||
|
id=project.id,
|
||||||
|
telegram_id=project.channel.telegram_id,
|
||||||
|
title=project.channel.title,
|
||||||
|
username=project.channel.username,
|
||||||
|
status=project.status,
|
||||||
|
)
|
||||||
|
for project in projects
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -11,9 +11,7 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def tg_add_target_chan_1(
|
async def tg_add_project_1(self: 'Usecase', input: dto.ConnectProjectInput) -> dto.ProjectOutput | None:
|
||||||
self: 'Usecase', input: dto.ConnectTargetChanInput
|
|
||||||
) -> dto.ConnectTargetChanOutput | None:
|
|
||||||
permissions = input.bot_permissions
|
permissions = input.bot_permissions
|
||||||
|
|
||||||
if not permissions.is_admin:
|
if not permissions.is_admin:
|
||||||
@@ -71,17 +69,37 @@ async def tg_add_target_chan_1(
|
|||||||
|
|
||||||
if len(workspaces) == 1:
|
if len(workspaces) == 1:
|
||||||
workspace = workspaces[0]
|
workspace = workspaces[0]
|
||||||
channel = domain.TargetChannel(
|
|
||||||
|
channel = await self.database.get_channel(workspace.id, telegram_id=input.telegram_id)
|
||||||
|
if channel:
|
||||||
|
channel.title = input.title
|
||||||
|
channel.username = input.username
|
||||||
|
await self.database.update_channel(channel)
|
||||||
|
else:
|
||||||
|
channel = domain.Channel(
|
||||||
telegram_id=input.telegram_id,
|
telegram_id=input.telegram_id,
|
||||||
title=input.title,
|
title=input.title,
|
||||||
username=input.username,
|
username=input.username,
|
||||||
workspace_id=workspace.id,
|
workspace_id=workspace.id,
|
||||||
status=domain.TargetChannelStatus.ACTIVE,
|
|
||||||
)
|
)
|
||||||
|
await self.database.create_channel(channel)
|
||||||
|
|
||||||
|
project = await self.database.get_project(workspace.id, channel_id=channel.id)
|
||||||
|
if project:
|
||||||
|
project.status = domain.ProjectStatus.ACTIVE
|
||||||
|
await self.database.update_project(project)
|
||||||
|
else:
|
||||||
|
project = domain.Project(
|
||||||
|
workspace_id=workspace.id,
|
||||||
|
channel_id=channel.id,
|
||||||
|
status=domain.ProjectStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
await self.database.create_project(project)
|
||||||
|
|
||||||
|
await self.ensure_active_purchase_plan(project)
|
||||||
|
|
||||||
await self.database.upsert_target_channel(channel)
|
|
||||||
log.info(
|
log.info(
|
||||||
'Channel %s connected/updated successfully in workspace %s by user %s',
|
'Project for channel %s connected/updated successfully in workspace %s by user %s',
|
||||||
input.telegram_id,
|
input.telegram_id,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
input.user_telegram_id,
|
input.user_telegram_id,
|
||||||
@@ -91,17 +109,16 @@ async def tg_add_target_chan_1(
|
|||||||
f'✅ Канал "{input.title}" добавлен в рабочее пространство "{workspace.name}".', input.user_telegram_id
|
f'✅ Канал "{input.title}" добавлен в рабочее пространство "{workspace.name}".', input.user_telegram_id
|
||||||
)
|
)
|
||||||
|
|
||||||
return dto.ConnectTargetChanOutput(
|
return dto.ProjectOutput(
|
||||||
id=channel.id,
|
id=project.id,
|
||||||
telegram_id=channel.telegram_id,
|
telegram_id=channel.telegram_id,
|
||||||
title=channel.title,
|
title=channel.title,
|
||||||
username=channel.username,
|
username=channel.username,
|
||||||
status=channel.status,
|
status=project.status,
|
||||||
is_active=channel.status == domain.TargetChannelStatus.ACTIVE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
current_state = await self.database.get_telegram_state(user.telegram_id)
|
current_state = await self.database.get_telegram_state(user.telegram_id)
|
||||||
if current_state and current_state.state == domain.TelegramStateEnum.TARGET_CHANNEL_WAITING_WORKSPACE:
|
if current_state and current_state.state == domain.TelegramStateEnum.PROJECT_WAITING_WORKSPACE:
|
||||||
await self.telegram_bot.send_message(
|
await self.telegram_bot.send_message(
|
||||||
'⚠️ Сначала завершите выбор рабочего пространства для предыдущего канала.', user.telegram_id
|
'⚠️ Сначала завершите выбор рабочего пространства для предыдущего канала.', user.telegram_id
|
||||||
)
|
)
|
||||||
@@ -122,20 +139,17 @@ async def tg_add_target_chan_1(
|
|||||||
|
|
||||||
await self.database.set_telegram_state(
|
await self.database.set_telegram_state(
|
||||||
user.telegram_id,
|
user.telegram_id,
|
||||||
domain.TelegramStateEnum.TARGET_CHANNEL_WAITING_WORKSPACE,
|
domain.TelegramStateEnum.PROJECT_WAITING_WORKSPACE,
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
|
|
||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton(text=workspace.name, callback_data=f'tg_add_target_chan_2:{workspace.id}')]
|
[InlineKeyboardButton(text=workspace.name, callback_data=f'tg_add_project_2:{workspace.id}')]
|
||||||
for workspace in workspaces
|
for workspace in workspaces
|
||||||
]
|
]
|
||||||
|
|
||||||
await self.telegram_bot.send_message_with_inline_keyboard(
|
await self.telegram_bot.send_message_with_inline_keyboard(
|
||||||
(
|
(f'✨ Подключение канала\n\nВыберите рабочее пространство, в которое добавить канал "{input.title}":'),
|
||||||
'✨ Подключение канала\n\n'
|
|
||||||
f'Выберите рабочее пространство, в которое добавить канал "{input.title}":'
|
|
||||||
),
|
|
||||||
user.telegram_id,
|
user.telegram_id,
|
||||||
buttons,
|
buttons,
|
||||||
)
|
)
|
||||||
@@ -10,12 +10,12 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def tg_add_target_chan_2(
|
async def tg_add_project_2(
|
||||||
self: 'Usecase', telegram_id: int, chat_id: int, callback_data: str, message_id: int
|
self: 'Usecase', telegram_id: int, chat_id: int, callback_data: str, message_id: int
|
||||||
) -> None:
|
) -> None:
|
||||||
state = await self.database.get_telegram_state(telegram_id)
|
state = await self.database.get_telegram_state(telegram_id)
|
||||||
if not state or state.state != domain.TelegramStateEnum.TARGET_CHANNEL_WAITING_WORKSPACE:
|
if not state or state.state != domain.TelegramStateEnum.PROJECT_WAITING_WORKSPACE:
|
||||||
log.debug('User %s has no active target channel workspace selection flow', telegram_id)
|
log.debug('User %s has no active project workspace selection flow', telegram_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
context = state.context or {}
|
context = state.context or {}
|
||||||
@@ -28,8 +28,8 @@ async def tg_add_target_chan_2(
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
previous_state = None
|
previous_state = None
|
||||||
|
|
||||||
if not callback_data.startswith('tg_add_target_chan_2:'):
|
if not callback_data.startswith('tg_add_project_2:'):
|
||||||
log.warning('Invalid target channel workspace callback data: %s', callback_data)
|
log.warning('Invalid project workspace callback data: %s', callback_data)
|
||||||
return
|
return
|
||||||
|
|
||||||
workspace_id_str = callback_data.split(':', 1)[1]
|
workspace_id_str = callback_data.split(':', 1)[1]
|
||||||
@@ -44,9 +44,9 @@ async def tg_add_target_chan_2(
|
|||||||
await self.database.clear_telegram_state(telegram_id)
|
await self.database.clear_telegram_state(telegram_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
channel_data = state.context.get('channel_data') if state.context else None
|
channel_data = context.get('channel_data')
|
||||||
if not channel_data:
|
if not channel_data:
|
||||||
log.error('Missing channel data for user %s in target channel flow', telegram_id)
|
log.error('Missing channel data for user %s in project flow', telegram_id)
|
||||||
await self.telegram_bot.send_message('❌ Не удалось подключить канал. Попробуйте снова.', chat_id)
|
await self.telegram_bot.send_message('❌ Не удалось подключить канал. Попробуйте снова.', chat_id)
|
||||||
if previous_state:
|
if previous_state:
|
||||||
await self.database.set_telegram_state(telegram_id, previous_state, previous_context)
|
await self.database.set_telegram_state(telegram_id, previous_state, previous_context)
|
||||||
@@ -65,15 +65,31 @@ async def tg_add_target_chan_2(
|
|||||||
await self.telegram_bot.send_message('❌ У вас нет доступа к выбранному рабочему пространству.', chat_id)
|
await self.telegram_bot.send_message('❌ У вас нет доступа к выбранному рабочему пространству.', chat_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
channel = domain.TargetChannel(
|
telegram_channel_id = int(channel_data['telegram_id'])
|
||||||
telegram_id=int(channel_data['telegram_id']),
|
channel = await self.database.get_channel(workspace.id, telegram_id=telegram_channel_id)
|
||||||
|
if channel:
|
||||||
|
channel.title = str(channel_data['title'])
|
||||||
|
channel.username = channel_data.get('username')
|
||||||
|
await self.database.update_channel(channel)
|
||||||
|
else:
|
||||||
|
channel = domain.Channel(
|
||||||
|
telegram_id=telegram_channel_id,
|
||||||
title=str(channel_data['title']),
|
title=str(channel_data['title']),
|
||||||
username=channel_data.get('username'),
|
username=channel_data.get('username'),
|
||||||
workspace_id=workspace.id,
|
workspace_id=workspace.id,
|
||||||
status=domain.TargetChannelStatus.ACTIVE,
|
|
||||||
)
|
)
|
||||||
|
await self.database.create_channel(channel)
|
||||||
|
|
||||||
|
project = await self.database.get_project(workspace.id, channel_id=channel.id)
|
||||||
|
if project:
|
||||||
|
project.status = domain.ProjectStatus.ACTIVE
|
||||||
|
await self.database.update_project(project)
|
||||||
|
else:
|
||||||
|
project = domain.Project(workspace_id=workspace.id, channel_id=channel.id, status=domain.ProjectStatus.ACTIVE)
|
||||||
|
await self.database.create_project(project)
|
||||||
|
|
||||||
|
await self.ensure_active_purchase_plan(project)
|
||||||
|
|
||||||
await self.database.upsert_target_channel(channel)
|
|
||||||
log.info(
|
log.info(
|
||||||
'Channel %s connected/updated successfully in workspace %s by user %s',
|
'Channel %s connected/updated successfully in workspace %s by user %s',
|
||||||
channel.telegram_id,
|
channel.telegram_id,
|
||||||
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def update_target_chan_permissions(self: 'Usecase', input: dto.UpdateTargetChanPermissionsInput) -> None:
|
async def update_project_permissions(self: 'Usecase', input: dto.UpdateProjectPermissionsInput) -> None:
|
||||||
missing_permissions = []
|
missing_permissions = []
|
||||||
if not input.permissions.can_invite_users:
|
if not input.permissions.can_invite_users:
|
||||||
missing_permissions.append('Создание инвайт-ссылок')
|
missing_permissions.append('Создание инвайт-ссылок')
|
||||||
@@ -22,31 +22,37 @@ async def update_target_chan_permissions(self: 'Usecase', input: dto.UpdateTarge
|
|||||||
log.warning(f'User with telegram_id {input.user_telegram_id} not found when updating channel permissions')
|
log.warning(f'User with telegram_id {input.user_telegram_id} not found when updating channel permissions')
|
||||||
return
|
return
|
||||||
|
|
||||||
channel = await self.database.get_target_channel_for_user_by_telegram(user.id, input.telegram_id)
|
project = await self.database.get_project_for_user_by_telegram(user.id, input.telegram_id)
|
||||||
if not channel:
|
if not project:
|
||||||
log.warning(f'Target channel {input.telegram_id} not found when permissions changed')
|
log.warning(f'Project channel {input.telegram_id} not found when permissions changed')
|
||||||
raise domain.TargetChannelNotFound()
|
raise domain.ProjectNotFound()
|
||||||
|
|
||||||
if not missing_permissions:
|
if not missing_permissions:
|
||||||
if channel.status != domain.TargetChannelStatus.ACTIVE:
|
if project.status != domain.ProjectStatus.ACTIVE:
|
||||||
channel.status = domain.TargetChannelStatus.ACTIVE
|
project.status = domain.ProjectStatus.ACTIVE
|
||||||
await self.database.update_target_channel(channel)
|
await self.database.update_project(project)
|
||||||
|
|
||||||
await self.telegram_bot.send_message(
|
await self.telegram_bot.send_message(
|
||||||
f'✅ Канал "{input.chat_title}" был активирован.\n\nВсе необходимые права боту предоставлены!',
|
f'✅ Канал "{input.chat_title}" был активирован.\n\nВсе необходимые права боту предоставлены!',
|
||||||
input.user_telegram_id,
|
input.user_telegram_id,
|
||||||
)
|
)
|
||||||
log.info(f'Target channel {input.telegram_id} reactivated - all permissions granted')
|
log.info(f'Project channel {input.telegram_id} reactivated - all permissions granted')
|
||||||
return
|
return
|
||||||
|
|
||||||
if channel.status == domain.TargetChannelStatus.ACTIVE:
|
if project.status == domain.ProjectStatus.ACTIVE:
|
||||||
channel.status = domain.TargetChannelStatus.INACTIVE
|
project.status = domain.ProjectStatus.INACTIVE
|
||||||
await self.database.update_target_channel(channel)
|
await self.database.update_project(project)
|
||||||
|
|
||||||
missed_permissions = '\n'.join(f'• {p}' for p in missing_permissions)
|
missed_permissions = '\n'.join(f'• {p}' for p in missing_permissions)
|
||||||
await self.telegram_bot.send_message(
|
await self.telegram_bot.send_message(
|
||||||
f'⚠️ Канал "{input.chat_title}" был деактивирован.\
|
(
|
||||||
\n\nБоту убрали необходимые права:\n{missed_permissions}',
|
f'⚠️ Канал "{input.chat_title}" был деактивирован.\n\n'
|
||||||
|
f'Боту убрали необходимые права:\n{missed_permissions}'
|
||||||
|
),
|
||||||
input.user_telegram_id,
|
input.user_telegram_id,
|
||||||
)
|
)
|
||||||
log.warning(f'Target channel {input.telegram_id} deactivated due to missing permissions: {missing_permissions}')
|
log.warning(
|
||||||
|
'Project channel %s deactivated due to missing permissions: %s',
|
||||||
|
input.telegram_id,
|
||||||
|
missing_permissions,
|
||||||
|
)
|
||||||
67
src/usecase/purchase_plan/attach_channel_to_purchase_plan.py
Normal file
67
src/usecase/purchase_plan/attach_channel_to_purchase_plan.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
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 attach_channel_to_purchase_plan(
|
||||||
|
self: 'Usecase',
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
input: dto.AttachChannelToPurchasePlanInput,
|
||||||
|
) -> dto.PurchasePlanChannelOutput:
|
||||||
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
|
if not project:
|
||||||
|
raise domain.ProjectNotFound(project_id)
|
||||||
|
|
||||||
|
plan = await self.ensure_active_purchase_plan(project)
|
||||||
|
|
||||||
|
# Поиск канала по username
|
||||||
|
channel = await self.database.get_channel(workspace_id, username=input.username)
|
||||||
|
|
||||||
|
if not channel:
|
||||||
|
# Создать неподтвержденный канал
|
||||||
|
channel = domain.Channel(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
username=input.username,
|
||||||
|
telegram_id=None,
|
||||||
|
title=None,
|
||||||
|
verification_status=domain.ChannelVerificationStatus.UNVERIFIED,
|
||||||
|
)
|
||||||
|
await self.database.create_channel(channel)
|
||||||
|
log.info('Created unverified channel with username %s', input.username)
|
||||||
|
else:
|
||||||
|
log.info('Found existing channel %s for username %s', channel.id, input.username)
|
||||||
|
|
||||||
|
plan_channel = await self.database.add_channel_to_purchase_plan(
|
||||||
|
plan.id,
|
||||||
|
channel.id,
|
||||||
|
status=input.status,
|
||||||
|
planned_cost=input.planned_cost,
|
||||||
|
comment=input.comment,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info('Channel %s attached to purchase plan %s (project %s)', channel.id, plan.id, project.id)
|
||||||
|
|
||||||
|
return dto.PurchasePlanChannelOutput(
|
||||||
|
id=plan_channel.id,
|
||||||
|
status=plan_channel.status,
|
||||||
|
planned_cost=plan_channel.planned_cost,
|
||||||
|
comment=plan_channel.comment,
|
||||||
|
channel=dto.ChannelOutput(
|
||||||
|
id=channel.id,
|
||||||
|
telegram_id=channel.telegram_id,
|
||||||
|
title=channel.title,
|
||||||
|
username=channel.username,
|
||||||
|
verification_status=channel.verification_status,
|
||||||
|
),
|
||||||
|
)
|
||||||
43
src/usecase/purchase_plan/get_purchase_plan_channels.py
Normal file
43
src/usecase/purchase_plan/get_purchase_plan_channels.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import domain, dto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_purchase_plan_channels(
|
||||||
|
self: 'Usecase', input: dto.GetPurchasePlanChannelsInput
|
||||||
|
) -> dto.GetPurchasePlanChannelsOutput:
|
||||||
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
|
project = await self.database.get_project(input.workspace_id, project_id=input.project_id)
|
||||||
|
if not project:
|
||||||
|
raise domain.ProjectNotFound(input.project_id)
|
||||||
|
|
||||||
|
plan = await self.ensure_active_purchase_plan(project)
|
||||||
|
|
||||||
|
plan_channels = await self.database.get_purchase_plan_channels(plan.id)
|
||||||
|
log.debug('Fetched %s channels for purchase plan %s (project %s)', len(plan_channels), plan.id, project.id)
|
||||||
|
|
||||||
|
return dto.GetPurchasePlanChannelsOutput(
|
||||||
|
channels=[
|
||||||
|
dto.PurchasePlanChannelOutput(
|
||||||
|
id=plan_channel.id,
|
||||||
|
status=plan_channel.status,
|
||||||
|
planned_cost=plan_channel.planned_cost,
|
||||||
|
comment=plan_channel.comment,
|
||||||
|
channel=dto.ChannelOutput(
|
||||||
|
id=plan_channel.channel.id,
|
||||||
|
telegram_id=plan_channel.channel.telegram_id,
|
||||||
|
title=plan_channel.channel.title,
|
||||||
|
username=plan_channel.channel.username,
|
||||||
|
verification_status=plan_channel.channel.verification_status,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for plan_channel in plan_channels
|
||||||
|
]
|
||||||
|
)
|
||||||
33
src/usecase/purchase_plan/remove_purchase_plan_channel.py
Normal file
33
src/usecase/purchase_plan/remove_purchase_plan_channel.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import domain
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_purchase_plan_channel(
|
||||||
|
self: 'Usecase',
|
||||||
|
channel_id: uuid.UUID,
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
) -> None:
|
||||||
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
|
if not project:
|
||||||
|
raise domain.ProjectNotFound(project_id)
|
||||||
|
|
||||||
|
plan = await self.ensure_active_purchase_plan(project)
|
||||||
|
|
||||||
|
plan_channel = await self.database.get_purchase_plan_channel(channel_id, plan.id)
|
||||||
|
if not plan_channel:
|
||||||
|
raise domain.PurchasePlanChannelNotFound(channel_id)
|
||||||
|
|
||||||
|
await self.database.remove_channel_from_purchase_plan(plan.id, plan_channel.channel_id)
|
||||||
|
log.info('Removed channel %s from purchase plan %s (project %s)', plan_channel.channel_id, plan.id, project.id)
|
||||||
71
src/usecase/purchase_plan/update_purchase_plan_channel.py
Normal file
71
src/usecase/purchase_plan/update_purchase_plan_channel.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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_purchase_plan_channel(
|
||||||
|
self: 'Usecase',
|
||||||
|
channel_id: uuid.UUID,
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
input: dto.UpdatePurchasePlanChannelInput,
|
||||||
|
) -> dto.PurchasePlanChannelOutput:
|
||||||
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
|
if not project:
|
||||||
|
raise domain.ProjectNotFound(project_id)
|
||||||
|
|
||||||
|
plan = await self.ensure_active_purchase_plan(project)
|
||||||
|
|
||||||
|
plan_channel = await self.database.get_purchase_plan_channel(channel_id, plan.id)
|
||||||
|
if not plan_channel:
|
||||||
|
raise domain.PurchasePlanChannelNotFound(channel_id)
|
||||||
|
|
||||||
|
updated_fields: dict[str, object | None] = {}
|
||||||
|
if input.status is not None:
|
||||||
|
plan_channel.status = input.status
|
||||||
|
updated_fields['status'] = input.status
|
||||||
|
if input.planned_cost is not None:
|
||||||
|
plan_channel.planned_cost = input.planned_cost
|
||||||
|
updated_fields['planned_cost'] = input.planned_cost
|
||||||
|
if input.comment is not None:
|
||||||
|
plan_channel.comment = input.comment
|
||||||
|
updated_fields['comment'] = input.comment
|
||||||
|
|
||||||
|
if updated_fields:
|
||||||
|
await self.database.update_purchase_plan_channel(plan_channel)
|
||||||
|
log.info(
|
||||||
|
'Updated purchase plan channel %s for project %s fields %s',
|
||||||
|
channel_id,
|
||||||
|
project.id,
|
||||||
|
', '.join(updated_fields.keys()),
|
||||||
|
)
|
||||||
|
|
||||||
|
channel: domain.Channel | None = plan_channel.channel
|
||||||
|
if channel is None:
|
||||||
|
channel = await self.database.get_channel(workspace_id, channel_id=plan_channel.channel_id)
|
||||||
|
if channel is None:
|
||||||
|
raise domain.ChannelNotFound(plan_channel.channel_id)
|
||||||
|
|
||||||
|
return dto.PurchasePlanChannelOutput(
|
||||||
|
id=plan_channel.id,
|
||||||
|
status=plan_channel.status,
|
||||||
|
planned_cost=plan_channel.planned_cost,
|
||||||
|
comment=plan_channel.comment,
|
||||||
|
channel=dto.ChannelOutput(
|
||||||
|
id=channel.id,
|
||||||
|
telegram_id=channel.telegram_id,
|
||||||
|
title=channel.title,
|
||||||
|
username=channel.username,
|
||||||
|
verification_status=channel.verification_status,
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -22,16 +22,23 @@ async def handle_subscription(
|
|||||||
log.warning('Placement not found for invite_link: %s', invite_link)
|
log.warning('Placement not found for invite_link: %s', invite_link)
|
||||||
return
|
return
|
||||||
|
|
||||||
subscriber = domain.Subscriber(
|
subscriber = await self.database.get_user(telegram_id=user_telegram_id)
|
||||||
|
if not subscriber:
|
||||||
|
subscriber = domain.User(
|
||||||
telegram_id=user_telegram_id,
|
telegram_id=user_telegram_id,
|
||||||
username=username,
|
username=username,
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
)
|
)
|
||||||
await self.database.upsert_subscriber(subscriber)
|
await self.database.create_user(subscriber)
|
||||||
|
else:
|
||||||
|
subscriber.username = username or subscriber.username
|
||||||
|
subscriber.first_name = first_name or subscriber.first_name
|
||||||
|
subscriber.last_name = last_name or subscriber.last_name
|
||||||
|
await self.database.update_user(subscriber)
|
||||||
|
|
||||||
active_subscription = await self.database.get_active_subscription_by_subscriber_and_channel(
|
active_subscription = await self.database.get_active_subscription_by_subscriber_and_project(
|
||||||
subscriber.id, placement.target_channel.telegram_id
|
subscriber.id, placement.project_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if active_subscription:
|
if active_subscription:
|
||||||
@@ -43,7 +50,7 @@ async def handle_subscription(
|
|||||||
'ignoring new subscription attempt via placement %s',
|
'ignoring new subscription attempt via placement %s',
|
||||||
subscriber.id,
|
subscriber.id,
|
||||||
user_telegram_id,
|
user_telegram_id,
|
||||||
placement.target_channel_id,
|
placement.project_id,
|
||||||
active_subscription.placement_id,
|
active_subscription.placement_id,
|
||||||
placement.id,
|
placement.id,
|
||||||
)
|
)
|
||||||
@@ -72,7 +79,6 @@ async def handle_subscription(
|
|||||||
subscription = domain.Subscription(
|
subscription = domain.Subscription(
|
||||||
placement_id=placement.id,
|
placement_id=placement.id,
|
||||||
subscriber_id=subscriber.id,
|
subscriber_id=subscriber.id,
|
||||||
target_channel_id=placement.target_channel_id,
|
|
||||||
invite_link=invite_link,
|
invite_link=invite_link,
|
||||||
)
|
)
|
||||||
async with self.database.transaction():
|
async with self.database.transaction():
|
||||||
|
|||||||
@@ -12,14 +12,17 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def handle_unsubscription(self: 'Usecase', user_telegram_id: int, channel_telegram_id: int) -> None:
|
async def handle_unsubscription(self: 'Usecase', user_telegram_id: int, channel_telegram_id: int) -> None:
|
||||||
subscriber = await self.database.get_subscriber(user_telegram_id)
|
subscriber = await self.database.get_user(telegram_id=user_telegram_id)
|
||||||
if not subscriber:
|
if not subscriber:
|
||||||
log.warning('Subscriber not found for telegram_id: %s', user_telegram_id)
|
log.warning('Subscriber not found for telegram_id: %s', user_telegram_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
subscriptions = await self.database.get_active_subscriptions_by_subscriber_and_channel(
|
project = await self.database.get_project_by_channel_telegram(channel_telegram_id)
|
||||||
subscriber.id, channel_telegram_id
|
if not project:
|
||||||
)
|
log.warning('Project not found for channel telegram_id: %s', channel_telegram_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
subscriptions = await self.database.get_active_subscriptions_by_subscriber_and_project(subscriber.id, project.id)
|
||||||
|
|
||||||
if not subscriptions:
|
if not subscriptions:
|
||||||
log.info(
|
log.info(
|
||||||
|
|||||||
@@ -1,26 +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 disconnect_target_chan_by_tg_id(self: 'Usecase', input: dto.DisconnectTargetChanByTgIdInput) -> None:
|
|
||||||
user = await self.database.get_user(telegram_id=input.user_telegram_id)
|
|
||||||
if not user:
|
|
||||||
log.warning(f'User with telegram_id {input.user_telegram_id} not found when disconnecting channel')
|
|
||||||
return
|
|
||||||
|
|
||||||
channel = await self.database.get_target_channel_for_user_by_telegram(user.id, input.telegram_id)
|
|
||||||
if not channel:
|
|
||||||
log.warning(f'Target channel {input.telegram_id} not found')
|
|
||||||
raise domain.TargetChannelNotFound()
|
|
||||||
|
|
||||||
channel.status = domain.TargetChannelStatus.INACTIVE
|
|
||||||
await self.database.update_target_channel(channel)
|
|
||||||
|
|
||||||
log.info(f'Target channel {input.telegram_id} deactivated')
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from src import domain, dto
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .. import Usecase
|
|
||||||
|
|
||||||
|
|
||||||
async def get_user_target_chans(self: 'Usecase', input: dto.GetUserTargetChansInput) -> dto.GetUserTargetChansOutput:
|
|
||||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
|
||||||
|
|
||||||
channels = await self.database.get_workspace_target_channels(input.workspace_id)
|
|
||||||
|
|
||||||
return dto.GetUserTargetChansOutput(
|
|
||||||
target_channels=[
|
|
||||||
dto.ConnectTargetChanOutput(
|
|
||||||
id=channel.id,
|
|
||||||
telegram_id=channel.telegram_id,
|
|
||||||
title=channel.title,
|
|
||||||
username=channel.username,
|
|
||||||
status=channel.status,
|
|
||||||
is_active=channel.status == domain.TargetChannelStatus.ACTIVE,
|
|
||||||
)
|
|
||||||
for channel in channels
|
|
||||||
]
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user