домен обновлен

This commit is contained in:
Artem Tsyrulnikov
2026-01-07 15:11:28 +03:00
parent f6669c3f5a
commit e4199fc9c9
30 changed files with 696 additions and 1201 deletions

View File

@@ -1,335 +0,0 @@
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
CREATE TABLE IF NOT EXISTS "channel"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"telegram_id" BIGINT NOT NULL UNIQUE,
"title" VARCHAR(255) NOT NULL,
"username" VARCHAR(255) NOT NULL UNIQUE,
"access_hash" BIGINT,
"pts" INT
);
CREATE INDEX IF NOT EXISTS "idx_channel_telegra_414c3b" ON "channel" ("telegram_id");
CREATE INDEX IF NOT EXISTS "idx_channel_usernam_7fd87a" ON "channel" ("username");
CREATE TABLE IF NOT EXISTS "post"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"message_id" INT NOT NULL,
"text" TEXT NOT NULL,
"deleted_from_channel_at" TIMESTAMPTZ,
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_post_channel_0a2cba" UNIQUE ("channel_id", "message_id")
);
CREATE INDEX IF NOT EXISTS "idx_post_channel_197bed" ON "post" ("channel_id");
CREATE TABLE IF NOT EXISTS "post_views_history"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"views_count" INT NOT NULL,
"fetched_at" TIMESTAMPTZ NOT NULL,
"post_id" UUID NOT NULL REFERENCES "post" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_post_views__post_id_c6cc9f" UNIQUE ("post_id", "fetched_at")
);
CREATE INDEX IF NOT EXISTS "idx_post_views__fetched_103eb5" ON "post_views_history" ("fetched_at");
CREATE INDEX IF NOT EXISTS "idx_post_views__post_id_dfb1ca" ON "post_views_history" ("post_id");
CREATE TABLE IF NOT EXISTS "user"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"telegram_id" BIGINT NOT NULL UNIQUE,
"username" VARCHAR(255),
"first_name" VARCHAR(255),
"last_name" VARCHAR(255)
);
CREATE INDEX IF NOT EXISTS "idx_user_telegra_66ffbd" ON "user" ("telegram_id");
CREATE TABLE IF NOT EXISTS "login_token"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"token" VARCHAR(255) NOT NULL UNIQUE,
"expires_at" TIMESTAMPTZ NOT NULL,
"used_at" TIMESTAMPTZ,
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_login_token_token_2582d2" ON "login_token" ("token");
CREATE INDEX IF NOT EXISTS "idx_login_token_user_id_c85d7c" ON "login_token" ("user_id");
CREATE TABLE IF NOT EXISTS "workspace"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"name" VARCHAR(255) NOT NULL
);
CREATE TABLE IF NOT EXISTS "project"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
"purchase_invite_type_default" VARCHAR(10) NOT NULL DEFAULT 'approval',
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_project_workspa_2f80f2" UNIQUE ("workspace_id", "channel_id")
);
CREATE INDEX IF NOT EXISTS "idx_project_channel_ce2466" ON "project" ("channel_id");
CREATE INDEX IF NOT EXISTS "idx_project_workspa_2354fd" ON "project" ("workspace_id");
COMMENT ON COLUMN "project"."status" IS 'ACTIVE: active\nINACTIVE: inactive\nARCHIVED: archived';
COMMENT ON COLUMN "project"."purchase_invite_type_default" IS 'PUBLIC: public\nAPPROVAL: approval';
CREATE TABLE IF NOT EXISTS "creative"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"name" VARCHAR(255) NOT NULL,
"text" TEXT NOT NULL,
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE,
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_creative_project_0766a6" ON "creative" ("project_id");
CREATE INDEX IF NOT EXISTS "idx_creative_workspa_2de546" ON "creative" ("workspace_id");
COMMENT ON COLUMN "creative"."status" IS 'ACTIVE: active\nARCHIVED: archived';
CREATE TABLE IF NOT EXISTS "purchase"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE,
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_purchase_creativ_6829cd" ON "purchase" ("creative_id");
CREATE INDEX IF NOT EXISTS "idx_purchase_project_95b1c2" ON "purchase" ("project_id");
CREATE INDEX IF NOT EXISTS "idx_purchase_workspa_8784a5" ON "purchase" ("workspace_id");
CREATE INDEX IF NOT EXISTS "idx_purchase_project_dee224" ON "purchase" ("project_id", "status");
COMMENT ON COLUMN "purchase"."status" IS 'ACTIVE: active\nARCHIVED: archived';
CREATE TABLE IF NOT EXISTS "purchase_channel"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"status" VARCHAR(11) NOT NULL DEFAULT 'planned',
"planned_cost" DOUBLE PRECISION,
"comment" TEXT,
"invite_link" VARCHAR(512) NOT NULL,
"invite_link_type" VARCHAR(8) NOT NULL,
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
"purchase_id" UUID NOT NULL REFERENCES "purchase" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_purchase_ch_purchas_f7c86e" UNIQUE ("purchase_id", "channel_id")
);
CREATE INDEX IF NOT EXISTS "idx_purchase_ch_channel_2b1264" ON "purchase_channel" ("channel_id");
CREATE INDEX IF NOT EXISTS "idx_purchase_ch_purchas_1fe84a" ON "purchase_channel" ("purchase_id");
COMMENT ON COLUMN "purchase_channel"."status" IS 'PLANNED: planned\nAPPROVED: approved\nREJECTED: rejected\nIN_PROGRESS: in_progress\nCOMPLETED: completed';
COMMENT ON COLUMN "purchase_channel"."invite_link_type" IS 'PUBLIC: public\nAPPROVAL: approval';
CREATE TABLE IF NOT EXISTS "placement"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"wanted_placement_date" TIMESTAMPTZ NOT NULL,
"cost" DOUBLE PRECISION,
"comment" TEXT,
"invite_link" VARCHAR(512) NOT NULL,
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
"placement_channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
"post_id" UUID REFERENCES "post" ("id") ON DELETE SET NULL,
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE,
"purchase_id" UUID REFERENCES "purchase" ("id") ON DELETE CASCADE,
"purchase_channel_id" UUID REFERENCES "purchase_channel" ("id") ON DELETE CASCADE,
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_placement_creativ_32111d" ON "placement" ("creative_id");
CREATE INDEX IF NOT EXISTS "idx_placement_placeme_20cf53" ON "placement" ("placement_channel_id");
CREATE INDEX IF NOT EXISTS "idx_placement_post_id_e4a16e" ON "placement" ("post_id");
CREATE INDEX IF NOT EXISTS "idx_placement_project_2c6aa4" ON "placement" ("project_id");
CREATE INDEX IF NOT EXISTS "idx_placement_purchas_3e3775" ON "placement" ("purchase_id");
CREATE INDEX IF NOT EXISTS "idx_placement_purchas_61f9e6" ON "placement" ("purchase_channel_id");
CREATE INDEX IF NOT EXISTS "idx_placement_workspa_632b20" ON "placement" ("workspace_id");
COMMENT ON COLUMN "placement"."status" IS 'ACTIVE: active\nARCHIVED: archived';
CREATE TABLE IF NOT EXISTS "subscription"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"invite_link" VARCHAR(512) NOT NULL,
"status" VARCHAR(12) NOT NULL DEFAULT 'active',
"unsubscribed_at" TIMESTAMPTZ,
"placement_id" UUID NOT NULL REFERENCES "placement" ("id") ON DELETE CASCADE,
"subscriber_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_subscriptio_placeme_8fcb8f" UNIQUE ("placement_id", "subscriber_id")
);
CREATE INDEX IF NOT EXISTS "idx_subscriptio_invite__f2058d" ON "subscription" ("invite_link");
CREATE INDEX IF NOT EXISTS "idx_subscriptio_placeme_5decb2" ON "subscription" ("placement_id");
CREATE INDEX IF NOT EXISTS "idx_subscriptio_subscri_e75729" ON "subscription" ("subscriber_id");
COMMENT ON COLUMN "subscription"."status" IS 'ACTIVE: active\nUNSUBSCRIBED: unsubscribed';
CREATE TABLE IF NOT EXISTS "workspace_invite"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"status" VARCHAR(8) NOT NULL DEFAULT 'pending',
"invited_by_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_workspace_i_workspa_abd9c4" UNIQUE ("workspace_id", "user_id")
);
CREATE INDEX IF NOT EXISTS "idx_workspace_i_invited_3ed9f8" ON "workspace_invite" ("invited_by_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_i_user_id_a96e1e" ON "workspace_invite" ("user_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_i_workspa_71e9d2" ON "workspace_invite" ("workspace_id");
COMMENT ON COLUMN "workspace_invite"."status" IS 'PENDING: pending\nACCEPTED: accepted\nREVOKED: revoked';
CREATE TABLE IF NOT EXISTS "workspace_user"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"status" VARCHAR(7) NOT NULL DEFAULT 'active',
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_workspace_u_workspa_13c46c" UNIQUE ("workspace_id", "user_id")
);
CREATE INDEX IF NOT EXISTS "idx_workspace_u_user_id_246318" ON "workspace_user" ("user_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_a403b0" ON "workspace_user" ("workspace_id");
COMMENT ON COLUMN "workspace_user"."status" IS 'ACTIVE: active\nINVITED: invited\nBLOCKED: blocked';
CREATE TABLE IF NOT EXISTS "workspace_user_permission"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"permission" VARCHAR(24) NOT NULL,
"workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_workspace_u_workspa_86f574" UNIQUE ("workspace_user_id", "permission")
);
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_b9b7b0" ON "workspace_user_permission" ("workspace_user_id");
COMMENT ON COLUMN "workspace_user_permission"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write';
CREATE TABLE IF NOT EXISTS "workspace_user_permission_scope"
(
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"permission" VARCHAR(24) NOT NULL,
"channel_id" UUID REFERENCES "channel" ("id") ON DELETE CASCADE,
"creative_id" UUID REFERENCES "creative" ("id") ON DELETE CASCADE,
"placement_id" UUID REFERENCES "placement" ("id") ON DELETE CASCADE,
"project_id" UUID REFERENCES "project" ("id") ON DELETE CASCADE,
"workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_workspace_u_channel_d94b48" ON "workspace_user_permission_scope" ("channel_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_u_creativ_8da562" ON "workspace_user_permission_scope" ("creative_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_u_placeme_ea1846" ON "workspace_user_permission_scope" ("placement_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_u_project_bd7fea" ON "workspace_user_permission_scope" ("project_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_82a35a" ON "workspace_user_permission_scope" ("workspace_user_id");
COMMENT ON COLUMN "workspace_user_permission_scope"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write';
CREATE TABLE IF NOT EXISTS "aerich"
(
"id" SERIAL NOT NULL PRIMARY KEY,
"version" VARCHAR(255) NOT NULL,
"app" VARCHAR(100) NOT NULL,
"content" JSONB NOT NULL
);"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
"""
MODELS_STATE = (
'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'
)

View File

@@ -0,0 +1,342 @@
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
CREATE TABLE IF NOT EXISTS "channel" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"telegram_id" BIGINT NOT NULL UNIQUE,
"title" VARCHAR(255) NOT NULL,
"username" VARCHAR(255) NOT NULL UNIQUE,
"access_hash" BIGINT,
"pts" INT
);
CREATE INDEX IF NOT EXISTS "idx_channel_telegra_414c3b" ON "channel" ("telegram_id");
CREATE INDEX IF NOT EXISTS "idx_channel_usernam_7fd87a" ON "channel" ("username");
CREATE TABLE IF NOT EXISTS "post" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"message_id" INT NOT NULL,
"text" TEXT NOT NULL,
"deleted_from_channel_at" TIMESTAMPTZ,
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_post_channel_0a2cba" UNIQUE ("channel_id", "message_id")
);
CREATE INDEX IF NOT EXISTS "idx_post_channel_197bed" ON "post" ("channel_id");
CREATE TABLE IF NOT EXISTS "post_views_history" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"views_count" INT NOT NULL,
"fetched_at" TIMESTAMPTZ NOT NULL,
"post_id" UUID NOT NULL REFERENCES "post" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_post_views__post_id_c6cc9f" UNIQUE ("post_id", "fetched_at")
);
CREATE INDEX IF NOT EXISTS "idx_post_views__fetched_103eb5" ON "post_views_history" ("fetched_at");
CREATE INDEX IF NOT EXISTS "idx_post_views__post_id_dfb1ca" ON "post_views_history" ("post_id");
CREATE TABLE IF NOT EXISTS "telegram_user" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"telegram_id" BIGINT NOT NULL UNIQUE,
"username" VARCHAR(255),
"first_name" VARCHAR(255),
"last_name" VARCHAR(255)
);
CREATE INDEX IF NOT EXISTS "idx_telegram_us_telegra_3a3100" ON "telegram_user" ("telegram_id");
CREATE TABLE IF NOT EXISTS "user" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"telegram_user_id" UUID NOT NULL UNIQUE REFERENCES "telegram_user" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_user_telegra_444a03" ON "user" ("telegram_user_id");
CREATE TABLE IF NOT EXISTS "login_token" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"token" VARCHAR(255) NOT NULL UNIQUE,
"expires_at" TIMESTAMPTZ NOT NULL,
"used_at" TIMESTAMPTZ,
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_login_token_token_2582d2" ON "login_token" ("token");
CREATE INDEX IF NOT EXISTS "idx_login_token_user_id_c85d7c" ON "login_token" ("user_id");
CREATE TABLE IF NOT EXISTS "workspace" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"name" VARCHAR(255) NOT NULL
);
CREATE TABLE IF NOT EXISTS "project" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
"purchase_invite_type_default" VARCHAR(10) NOT NULL DEFAULT 'approval',
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_project_workspa_2f80f2" UNIQUE ("workspace_id", "channel_id")
);
CREATE INDEX IF NOT EXISTS "idx_project_channel_ce2466" ON "project" ("channel_id");
CREATE INDEX IF NOT EXISTS "idx_project_workspa_2354fd" ON "project" ("workspace_id");
COMMENT ON COLUMN "project"."status" IS 'ACTIVE: active\nINACTIVE: inactive\nARCHIVED: archived';
COMMENT ON COLUMN "project"."purchase_invite_type_default" IS 'PUBLIC: public\nAPPROVAL: approval';
CREATE TABLE IF NOT EXISTS "creative" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"name" VARCHAR(255) NOT NULL,
"text" TEXT NOT NULL,
"media_type" VARCHAR(32),
"media_file_id" VARCHAR(512),
"media_s3_key" VARCHAR(512),
"buttons" JSONB NOT NULL,
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_creative_project_0766a6" ON "creative" ("project_id");
COMMENT ON COLUMN "creative"."status" IS 'ACTIVE: active\nARCHIVED: archived';
CREATE TABLE IF NOT EXISTS "purchase" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
"placement_at" TIMESTAMPTZ,
"payment_at" TIMESTAMPTZ,
"cost_type" VARCHAR(8),
"cost_value" DOUBLE PRECISION,
"cost_before_bargain" DOUBLE PRECISION,
"purchase_type" VARCHAR(16),
"format" TEXT,
"comment" TEXT,
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_purchase_creativ_6829cd" ON "purchase" ("creative_id");
CREATE INDEX IF NOT EXISTS "idx_purchase_project_95b1c2" ON "purchase" ("project_id");
CREATE INDEX IF NOT EXISTS "idx_purchase_project_dee224" ON "purchase" ("project_id", "status");
COMMENT ON COLUMN "purchase"."status" IS 'ACTIVE: active\nARCHIVED: archived';
COMMENT ON COLUMN "purchase"."cost_type" IS 'FIXED: fixed\nCPM: cpm';
COMMENT ON COLUMN "purchase"."purchase_type" IS 'SELF_PROMO: self_promo\nSTANDARD: standard';
CREATE TABLE IF NOT EXISTS "purchase_channel" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"status" VARCHAR(11) NOT NULL DEFAULT 'planned',
"placement_at" TIMESTAMPTZ,
"cost_type" VARCHAR(8),
"cost_value" DOUBLE PRECISION,
"cost_before_bargain" DOUBLE PRECISION,
"format" TEXT,
"comment" TEXT,
"invite_link" VARCHAR(512) NOT NULL,
"invite_link_type" VARCHAR(8) NOT NULL,
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
"purchase_id" UUID NOT NULL REFERENCES "purchase" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_purchase_ch_purchas_f7c86e" UNIQUE ("purchase_id", "channel_id")
);
CREATE INDEX IF NOT EXISTS "idx_purchase_ch_channel_2b1264" ON "purchase_channel" ("channel_id");
CREATE INDEX IF NOT EXISTS "idx_purchase_ch_purchas_1fe84a" ON "purchase_channel" ("purchase_id");
COMMENT ON COLUMN "purchase_channel"."status" IS 'PLANNED: planned\nAPPROVED: approved\nREJECTED: rejected\nIN_PROGRESS: in_progress\nCOMPLETED: completed';
COMMENT ON COLUMN "purchase_channel"."cost_type" IS 'FIXED: fixed\nCPM: cpm';
COMMENT ON COLUMN "purchase_channel"."invite_link_type" IS 'PUBLIC: public\nAPPROVAL: approval';
CREATE TABLE IF NOT EXISTS "placement" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"wanted_placement_date" TIMESTAMPTZ NOT NULL,
"cost" DOUBLE PRECISION,
"comment" TEXT,
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
"post_id" UUID REFERENCES "post" ("id") ON DELETE SET NULL,
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE,
"purchase_channel_id" UUID NOT NULL REFERENCES "purchase_channel" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_placement_creativ_32111d" ON "placement" ("creative_id");
CREATE INDEX IF NOT EXISTS "idx_placement_post_id_e4a16e" ON "placement" ("post_id");
CREATE INDEX IF NOT EXISTS "idx_placement_project_2c6aa4" ON "placement" ("project_id");
CREATE INDEX IF NOT EXISTS "idx_placement_purchas_61f9e6" ON "placement" ("purchase_channel_id");
COMMENT ON COLUMN "placement"."status" IS 'ACTIVE: active\nARCHIVED: archived';
CREATE TABLE IF NOT EXISTS "subscription" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"invite_link" VARCHAR(512) NOT NULL,
"status" VARCHAR(12) NOT NULL DEFAULT 'active',
"unsubscribed_at" TIMESTAMPTZ,
"placement_id" UUID NOT NULL REFERENCES "placement" ("id") ON DELETE CASCADE,
"telegram_user_id" UUID NOT NULL REFERENCES "telegram_user" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_subscriptio_placeme_75b40b" UNIQUE ("placement_id", "telegram_user_id")
);
CREATE INDEX IF NOT EXISTS "idx_subscriptio_invite__f2058d" ON "subscription" ("invite_link");
CREATE INDEX IF NOT EXISTS "idx_subscriptio_placeme_5decb2" ON "subscription" ("placement_id");
CREATE INDEX IF NOT EXISTS "idx_subscriptio_telegra_8be9bb" ON "subscription" ("telegram_user_id");
COMMENT ON COLUMN "subscription"."status" IS 'ACTIVE: active\nUNSUBSCRIBED: unsubscribed';
CREATE TABLE IF NOT EXISTS "workspace_invite" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"status" VARCHAR(8) NOT NULL DEFAULT 'pending',
"invited_by_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_workspace_i_workspa_abd9c4" UNIQUE ("workspace_id", "user_id")
);
CREATE INDEX IF NOT EXISTS "idx_workspace_i_invited_3ed9f8" ON "workspace_invite" ("invited_by_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_i_user_id_a96e1e" ON "workspace_invite" ("user_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_i_workspa_71e9d2" ON "workspace_invite" ("workspace_id");
COMMENT ON COLUMN "workspace_invite"."status" IS 'PENDING: pending\nACCEPTED: accepted\nREVOKED: revoked';
CREATE TABLE IF NOT EXISTS "workspace_user" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"status" VARCHAR(7) NOT NULL DEFAULT 'active',
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_workspace_u_workspa_13c46c" UNIQUE ("workspace_id", "user_id")
);
CREATE INDEX IF NOT EXISTS "idx_workspace_u_user_id_246318" ON "workspace_user" ("user_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_a403b0" ON "workspace_user" ("workspace_id");
COMMENT ON COLUMN "workspace_user"."status" IS 'ACTIVE: active\nINVITED: invited\nBLOCKED: blocked';
CREATE TABLE IF NOT EXISTS "workspace_user_permission" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"permission" VARCHAR(24) NOT NULL,
"workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE,
CONSTRAINT "uid_workspace_u_workspa_86f574" UNIQUE ("workspace_user_id", "permission")
);
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_b9b7b0" ON "workspace_user_permission" ("workspace_user_id");
COMMENT ON COLUMN "workspace_user_permission"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write';
CREATE TABLE IF NOT EXISTS "workspace_user_permission_scope" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"permission" VARCHAR(24) NOT NULL,
"channel_id" UUID REFERENCES "channel" ("id") ON DELETE CASCADE,
"creative_id" UUID REFERENCES "creative" ("id") ON DELETE CASCADE,
"placement_id" UUID REFERENCES "placement" ("id") ON DELETE CASCADE,
"project_id" UUID REFERENCES "project" ("id") ON DELETE CASCADE,
"workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_workspace_u_channel_d94b48" ON "workspace_user_permission_scope" ("channel_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_u_creativ_8da562" ON "workspace_user_permission_scope" ("creative_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_u_placeme_ea1846" ON "workspace_user_permission_scope" ("placement_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_u_project_bd7fea" ON "workspace_user_permission_scope" ("project_id");
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_82a35a" ON "workspace_user_permission_scope" ("workspace_user_id");
COMMENT ON COLUMN "workspace_user_permission_scope"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write';
CREATE TABLE IF NOT EXISTS "aerich" (
"id" SERIAL NOT NULL PRIMARY KEY,
"version" VARCHAR(255) NOT NULL,
"app" VARCHAR(100) NOT NULL,
"content" JSONB NOT NULL
);"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
"""
MODELS_STATE = (
'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'
'v0'
)

View File

@@ -1,99 +0,0 @@
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
-- Add CHECK constraint: exactly one FK must be set
ALTER TABLE "workspace_user_permission_scope"
ADD CONSTRAINT "check_exactly_one_scope"
CHECK (
(("project_id" IS NOT NULL)::int +
("creative_id" IS NOT NULL)::int +
("placement_id" IS NOT NULL)::int +
("channel_id" IS NOT NULL)::int) = 1
);
COMMENT ON TABLE "workspace_user_permission_scope"
IS 'Permission scoped to specific entity (project/creative/placement/channel)'; \
"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "workspace_user_permission_scope"
DROP CONSTRAINT IF EXISTS "check_exactly_one_scope"; \
"""
MODELS_STATE = (
'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'
)

View File

@@ -0,0 +1,107 @@
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
-- Add CHECK constraint: exactly one FK must be set
ALTER TABLE "workspace_user_permission_scope"
ADD CONSTRAINT "check_exactly_one_scope"
CHECK (
(("project_id" IS NOT NULL)::int +
("creative_id" IS NOT NULL)::int +
("placement_id" IS NOT NULL)::int +
("channel_id" IS NOT NULL)::int) = 1
);
COMMENT ON TABLE "workspace_user_permission_scope"
IS 'Permission scoped to specific entity (project/creative/placement/channel)'; \
"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "workspace_user_permission_scope"
DROP CONSTRAINT IF EXISTS "check_exactly_one_scope"; \
"""
MODELS_STATE = (
'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'
'v0'
)

View File

@@ -1,95 +0,0 @@
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "creative" ADD "buttons" JSONB NOT NULL;
ALTER TABLE "creative" ADD "media_data" BYTEA;
ALTER TABLE "creative" ADD "media_file_id" VARCHAR(512);
ALTER TABLE "creative" ADD "media_type" VARCHAR(32);"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "creative" DROP COLUMN "buttons";
ALTER TABLE "creative" DROP COLUMN "media_data";
ALTER TABLE "creative" DROP COLUMN "media_file_id";
ALTER TABLE "creative" DROP COLUMN "media_type";"""
MODELS_STATE = (
"eJztXW1z2roS/isMn3pmcjsJIU1O586dAUJbTgkwvKTtaToexyjEN8bm+CVpptP/fiTbwp"
"LfsI3BNuyXTipphf2sJGsf7a5+1ZfaHCnG286jqKpIqb+v/aqr4hLhP/xVJ7W6uFp5FaTA"
"FO8Vu63ENLo3TF2UTFz8ICoGwkVzZEi6vDJlTcWlqqUopFCTcENZXXhFlir/YyHB1BbIfE"
"Q6rvj+AxfL6hz9RAb97+pJeJCRMuceVp6T37bLBfN1ZZfNZr3rD3ZL8nP3gqQp1lL1Wq9e"
"zUdNXTe3LHn+lsiQugVSkS6aaM68BnlK94VpkfPEuMDULbR+1LlXMEcPoqUQMOr/fbBUiW"
"BQs3+J/NP8Xz0FPJKmEmhl1SRY/PrtvJX3znZpnfxU51Nr/Ob83R/2W2qGudDtShuR+m9b"
"UDRFR9TG1QNS0hF5bUE0g4Be4xpTXqJwUHlJH7hzV/Qt/SMLyLTAQ9kbYRRmCl82TOv4He"
"ZDVXl1NRiD8bR3051MWzcj8iZLw/hHsSFqTbukpmGXvvpK3zgq0fD8cObNupPal970U438"
"t/b3cND1K27dbvp3nTyTaJmaoGovgjhnBhstpcDglp5irdU8o2J5SVBsoYp1H97TK16LUT"
"a98pI56NV92j2qtSJqpK8dO0FNrI6FLi6FsG9ZW170VDNcjz5BnyIxVLv5oG05IRfkV/7z"
"Z6Nxfn7ZOD1/d3XRvLy8uDq9wm3tRwpWXcaou9372BtMee2Rgt88xrKpoCC6eI+jR2BLBX"
"yo4lcp6UK3FH8KClIX5iP+b+PiIgaz29bY3ingVr5xP3CrGk4dD6JlIN3+OwWOrEw+UO58"
"gO4eSFGSkGEIj6LxmG7G+wQzzfj9L9bFTPmVaQTBjUTWbV0tRBtnzcvm1fm75hrIdUkcfg"
"5WxKR6eGJsAVJwL0pPL6I+F7gaBlRFlNASqWHYtl3ZD5/HSBHtVwoi6hqXI9pPORfT33R4"
"0NKwDRjZB2wLA+6iygjo2v+RtDUITi9VxsHSJbwmI8GlQ7YFxO2OYWCqCgzSl7Jh4K4FQ9"
"LIGrQVMF80/clY4ZVjhncVo3XfE9J1KdfpSJDIWqs1tKjVN1i1bCz9JaIqLuynJr9NfokS"
"d4SPkZ9RPYzUo3Unsawe2wpoPaD1gP0BWg8UC7Qe0HrJab20TEm+LMlBEE4m+hkyC6a4NI"
"oQ/Rk29ksLYdxw736dciOdAvXmpvX1D26094eDj7Q5A2ynP2z78FyiuSw6sKQYmLxUJmwL"
"YEi40XneSDA4zxuRY5NUhUH5ICsolLbfhCYjWElAL86SIIpbRUJq14VhSrayYayoKuqvcZ"
"BSOR+e96+mY3aWDtE4hpPMXXb2Y9javUFr/C18+tP27Des/W3abfkQvrdMEz9EEN6/JsNB"
"OLiMiA/ZmYpf+PtclsyTmiIb5o9dLbKMqXVvyYopq8Zb8oM7srYIFvFLr3+V9SFPOvAvvY"
"YpmlYI8GSh6KrW0ga/hx9RVCUUUIInvb9PWx0b/S4HwGNcb3Wmvdvu+5rT4E4l6OCCa1yi"
"S4+4yBlFKZeUqwQLylXkcnLlX0xcbjJ0dY5mEnipPBmFIOQ5n1ttwx94sL1Qii0lcH65I4"
"EucIgSgmQQxg+ajuSF+hm9Bqb9BuKzzPAFWE5crIsva3YvMEbwezo2qw10a9JpXXfrYZM4"
"BwRzPWQoCD9+cQpHDw7xsh3d5HRkU2Uk4Kxm/2c1fW0hq1PtCan1kNMapvYk7rxGIe0Ec9"
"0QjmzgyAaYfTiyAcXCkQ0c2aTwxKZf0MRewlQAXFtdCNHPlYw1kmEu8JLVXOMOaTJYRrZP"
"lQHrWYlUqKfk8RgRoPAIGDlwTzO3m7KCtpF4YgZFWtZpl8azxz+F2M4cORVtOq+4ZmA4g+"
"Fc1b3HwdhXYDgfqGLBcD7UjeaLqBJtrD+mxBkn5NwzXqeRnVRz2h6SfiU3Ts23EVY0McIT"
"kwr4VPdAJKo2G6+Hs3a/WxuNu53epOf6Bq3VY1eSIlwgO9vicbfV99Eikrakm8ykzqyMSE"
"VcBPftziqrzxhxQZHVpzSknU+sKs7Ce3DABCe1TbDn7KRGA+1SkjQ+sSMhajhnhfUmwQ25"
"TevnFyF/jFDi6rToeSJbALb5A1VOvMCxNBNsNEY+JW682BEON19ugaz45bbOVRRHcGwGx2"
"ZwbC4nfkkcm8O2zTnAxybEqCx+PnsgwfDzb3/zQDLPbDVFDcQIsyABoowD+LYzOrkveVHf"
"4s1A8tu25PjlOCDTp1EqP5yph2U4cZsaymR5ysqLn2ewcphNutPaYNbvJ4ubMaz79VNuGR"
"0xYboqJ90IMSNljRmxJ2OYx4s7SWOcXWiLXP1cvtf5VWmJDAM/OfnfD/CB2TR2wAcmx/l4"
"MK4S4ANzoIoFH5hD9ZFgPnsBRUamneaF8sngv4fZmEP6acjztTvHCLoyPOjacm0yZl9gQr"
"qB1aZoj6xMp0FHedgdc5QB7CdHIycgl8qaX6REtj87TZ9l9GIIj7Jharq8dZIRPLZvSYef"
"7P5eS/2x2z8nwkETwY/44YvnSgRWfa+7YE5Y5xtkSo/Odh5oE6BNwLoG2gQUC7TJoRsyzi"
"dW0qyw6IhI3sQndazECbNnSDkLeMn9rG77swgrNwsKc8IuldqyG/L79XMorwkf5ehQdKYG"
"10kxzCrz/BdjjDGmUc4WmN93kyFBwAwDMwx262CGgWLBDDv0Deghh/72BrREVksdDux5TT"
"sh6qRDgb5xRtVs6nOfClvhfdyzqISobDRr93ud97WVda/IElbPaDQe3rb6WD2sUEr1nJ0m"
"0M/ZaaSCSJUvYBuOXZPv8yD4Dk6siz+xDh2HOeB3TKGLSU79aRDalofc+QbkFRMVAferwP"
"0qECtTCr8QOm7CmEdmTMVQj2yrXeaH/c6ynK5B9QMoR6AcgZkCyvF4FQuUI1CO1aMcS0kv"
"QrZBSPkGrF9poYtzb4F0UZAuqrh0UUCb+iHMjzaFYCn/2UZOVGGu5xsHRYZRZGI4MQa8zd"
"QYm7Mr9wApPpEYeOftfP8GVNmBMypAlR2oYoEqA6qsNFQZ3tfiL/U8hCsb9VuDAaHG3CbU"
"28tmy2xvL1I27v7V7UxJmY6ItUfKegMBN/w47k4mxJdPwG0Jysad2hnejPpdu7mkLVf24M"
"3CuJ2dJfEYO4v2GDsL+PQ5LymkvkLJLwhXKcFVSvZrwlVKW3/xd3+VEoOMg0fGtTusn4Jh"
"3pGzbt6HHeCpW89y1FH0NS1VAi6OsS8kH3x5CdPUCeHB0RlSc1WKXeVSuodQq/6U79G8qu"
"FvmTenur7lwhlO7u/dIx14VeBVgX4DXhUUC7zqMfCq5aY9ct5rwgXSW7l0zgaTWXvSGffa"
"hGW21PWuKRvRnAT9GPCD2LNPlOWLExSH5anorGC+jXpiNsYndyR0DLcScTZNCuwCgkcCXh"
"yXxRr725JZ+ca0Fn5PZFI6yxtWOYBIQj4rjV9gkpUpWZ2NbgiBQ1GPJm4s2mKXcaLAxwAf"
"A2Y78DHHq1jgYw7V4DGxOha6uAzds7flRWQ+cJ9gPvnAN3/QtpyQTjbwPxuN8/PLxun5u6"
"uL5uXlxdXpOi14sCouP3i795GkCOe0F8wZTjYp9t8BgKMJL1amIh5UPN3SuLhIwLfgVpGE"
"i13ny74u64YppMWSlwI0KZqKmAFMTui4sQwY8En8ARRtIWPEtCe07Y3mfdLTlHRUzt1Pog"
"g0uOE9gIhNctjnHXklrOrZvVUYEyb4E4AJB4ZsGPJMcFYxUHZJT3kB1CEcFRddHU1UcQHd"
"wFYBWxUEF0gNYKtAscBWAVsVOUHTGqtb2alFTMESGaqQXRqyS4dg4WSC2haJPHNiFYMDZN"
"nmPDrBIgeLvACL3B0ucXa5N6ISWOcuv5S/kR689pAMD4j+AfsdzDyw30GxYL8fg/1ewWiV"
"FVLnRD0BBdZH3cF1b/Dxfc1tcqe2Op3uyE6JJEoSWplOVqXb4WcnqdKz9pQtdCXnRB3OHm"
"cu3L+m9NQPCB6Jp77fmSclbIzIEQIGOcnzigqBpNAnmZJCh699OYBY+aiQwIK+GUAacXHs"
"0DGLeplCaXjGKI4X2Rxcw1NZwIkAJwKcSKlsLuBEjkaxwIkAJ1IaTiRxBo/e4LZn8yHuRv"
"NObfeHHZsOuVc0KSMdEheRQ+mQy0g65DKQxgMserDowaIvh1mV3qIHg3RrgzTsyvTdXJZe"
"zl1iMo+fUt0mXyaU9sZnMONoE7PBD7mkHIew4sV2RnewGwjvJ4H2ANoDrGOgPUCxQHscOu"
"3Bf2mzUB98D0Vf0dK6vukNhA+zPrmUZb6UVeEBt7tTR+MhuVZrIoy7LXIDl+s/L5AZytR+"
"GfemXab6RZdNdKd2sBShVKj4OijDlffq3Q68Bm4Po36r073pDrwnWEcz0GfwWtCn8Jq4vb"
"QGrf63aa9DO8HbNeXVlCXah1dPRs5wNhU6/V7n84Rt+SJjDVmmICmy9ERuEPtEbiXrr1/N"
"vYuWvhmtpS9Gq+0nykIbNZpJomma0cE0TT9zFLqfS8+GHB+blIQSEXIy7HfgQ184N1KFw2"
"e/TZvcYlvbwBnMNsc2z994A8tsB8sAWGYHv4EHy+xAFQuWGVhmYJmBZVZOy6zgu0g3L0Cl"
"McU41NwhmhY2Xuz4cCvBrSFVRc5ZVdPixkkdH2rAOwHvVE7eKWRu5wBl8nQxRU3ozffXcE"
"tWgsuYmbRJW6KXIgNTaeHzbTESDL6CblAqLYLpL1Aq4j7w0sK33XXguyTdW0iXpcd6CL3u"
"1pzEEemi16Y0fHnknRNJr5pwFbgdT74lz+fcNNE4a142r87fNdcXTKxL4rzY6R0S0fT4M9"
"KjGZpw9BiRI8+HyK5yZGqkANFtXk0Az05PEwCIW0VfgkrqfKSBppqh39m/JsNBBGHgifiA"
"nKn4Bb/PZck8qSmyYf4oJ6wxKJK35pheCt6bm9ZXP66d/rDtN1VIB+10STvz/7z8/hez6R"
"a4"
)

View File

@@ -1,90 +0,0 @@
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "creative" ADD "media_s3_key" VARCHAR(512);
ALTER TABLE "creative" DROP COLUMN "media_data";"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "creative" ADD "media_data" BYTEA;
ALTER TABLE "creative" DROP COLUMN "media_s3_key";"""
MODELS_STATE = (
"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"
"XdLO/D8vv/8PAyERnQ=="
)

View File

@@ -1,149 +0,0 @@
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "purchase"
ADD "comment" TEXT;
ALTER TABLE "purchase"
ADD "payment_at" TIMESTAMPTZ;
ALTER TABLE "purchase"
ADD "format" TEXT;
ALTER TABLE "purchase"
ADD "cost_before_bargain" DOUBLE PRECISION;
ALTER TABLE "purchase"
ADD "placement_at" TIMESTAMPTZ;
ALTER TABLE "purchase"
ADD "cost_type" VARCHAR(8);
ALTER TABLE "purchase"
ADD "cost_value" DOUBLE PRECISION;
ALTER TABLE "purchase"
ADD "purchase_type" VARCHAR(16);
ALTER TABLE "purchase_channel"
ADD "format" TEXT;
ALTER TABLE "purchase_channel"
ADD "cost_before_bargain" DOUBLE PRECISION;
ALTER TABLE "purchase_channel"
ADD "placement_at" TIMESTAMPTZ;
ALTER TABLE "purchase_channel"
ADD "cost_type" VARCHAR(8);
ALTER TABLE "purchase_channel"
ADD "cost_value" DOUBLE PRECISION;
ALTER TABLE "purchase_channel"
DROP COLUMN "planned_cost";
COMMENT ON COLUMN "purchase"."cost_type" IS 'FIXED: fixed\nCPM: cpm';
COMMENT ON COLUMN "purchase"."purchase_type" IS 'SELF_PROMO: self_promo\nSTANDARD: standard';
COMMENT ON COLUMN "purchase_channel"."cost_type" IS 'FIXED: fixed\nCPM: cpm';"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "purchase"
DROP COLUMN "comment";
ALTER TABLE "purchase"
DROP COLUMN "payment_at";
ALTER TABLE "purchase"
DROP COLUMN "format";
ALTER TABLE "purchase"
DROP COLUMN "cost_before_bargain";
ALTER TABLE "purchase"
DROP COLUMN "placement_at";
ALTER TABLE "purchase"
DROP COLUMN "cost_type";
ALTER TABLE "purchase"
DROP COLUMN "cost_value";
ALTER TABLE "purchase"
DROP COLUMN "purchase_type";
ALTER TABLE "purchase_channel"
ADD "planned_cost" DOUBLE PRECISION;
ALTER TABLE "purchase_channel"
DROP COLUMN "format";
ALTER TABLE "purchase_channel"
DROP COLUMN "cost_before_bargain";
ALTER TABLE "purchase_channel"
DROP COLUMN "placement_at";
ALTER TABLE "purchase_channel"
DROP COLUMN "cost_type";
ALTER TABLE "purchase_channel"
DROP COLUMN "cost_value";"""
MODELS_STATE = (
'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'
'49ruDVq8qUI6aKVL2pn/5+X3/wGuY8FA'
)

View File

@@ -1,151 +0,0 @@
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
DROP INDEX IF EXISTS "uid_user_telegra_66ffbd";
ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "uid_subscriptio_placeme_8fcb8f";
ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "fk_subscrip_user_6b9f4b8e";
CREATE TABLE IF NOT EXISTS "telegram_user" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"telegram_id" BIGINT NOT NULL UNIQUE,
"username" VARCHAR(255),
"first_name" VARCHAR(255),
"last_name" VARCHAR(255)
);
CREATE INDEX IF NOT EXISTS "idx_telegram_us_telegra_3a3100" ON "telegram_user" ("telegram_id");
INSERT INTO "telegram_user" (
"id",
"created_at",
"updated_at",
"deleted_at",
"telegram_id",
"username",
"first_name",
"last_name"
)
SELECT
"id",
"created_at",
"updated_at",
"deleted_at",
"telegram_id",
"username",
"first_name",
"last_name"
FROM "user"
WHERE "telegram_id" IS NOT NULL
ON CONFLICT ("id") DO NOTHING;
ALTER TABLE "subscription" RENAME COLUMN "subscriber_id" TO "telegram_user_id";
ALTER TABLE "user" ADD "telegram_user_id" UUID UNIQUE;
UPDATE "user" SET "telegram_user_id" = "id";
ALTER TABLE "user" ALTER COLUMN "telegram_user_id" SET NOT NULL;
ALTER TABLE "user" DROP COLUMN "first_name";
ALTER TABLE "user" DROP COLUMN "username";
ALTER TABLE "user" DROP COLUMN "telegram_id";
ALTER TABLE "user" DROP COLUMN "last_name";
ALTER TABLE "subscription" ADD CONSTRAINT "fk_subscrip_telegram_ab72b5bf" FOREIGN KEY ("telegram_user_id") REFERENCES "telegram_user" ("id") ON DELETE CASCADE;
CREATE UNIQUE INDEX IF NOT EXISTS "uid_subscriptio_placeme_75b40b" ON "subscription" ("placement_id", "telegram_user_id");
ALTER TABLE "user" ADD CONSTRAINT "fk_user_telegram_548d951e" FOREIGN KEY ("telegram_user_id") REFERENCES "telegram_user" ("id") ON DELETE CASCADE;"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
DROP INDEX IF EXISTS "uid_subscriptio_placeme_75b40b";
ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "fk_subscrip_telegram_ab72b5bf";
ALTER TABLE "user" DROP CONSTRAINT IF EXISTS "fk_user_telegram_548d951e";
ALTER TABLE "user" ADD "first_name" VARCHAR(255);
ALTER TABLE "user" ADD "username" VARCHAR(255);
ALTER TABLE "user" ADD "telegram_id" BIGINT NOT NULL UNIQUE;
ALTER TABLE "user" ADD "last_name" VARCHAR(255);
ALTER TABLE "user" DROP COLUMN "telegram_user_id";
ALTER TABLE "subscription" RENAME COLUMN "telegram_user_id" TO "subscriber_id";
DROP TABLE IF EXISTS "telegram_user";
ALTER TABLE "subscription" ADD CONSTRAINT "fk_subscrip_user_6b9f4b8e" FOREIGN KEY ("subscriber_id") REFERENCES "user" ("id") ON DELETE CASCADE;
CREATE UNIQUE INDEX IF NOT EXISTS "uid_subscriptio_placeme_8fcb8f" ON "subscription" ("placement_id", "subscriber_id");"""
MODELS_STATE = (
"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"
"1lQhHbSyJe3k/3n5/X8nR5KJ"
)

View File

@@ -291,11 +291,11 @@ class Postgres(DatabaseBase, Database):
)
async def get_purchase(self, workspace_id: uuid.UUID, purchase_id: uuid.UUID) -> domain.Purchase | None:
return await domain.Purchase.get_or_none(id=purchase_id, workspace_id=workspace_id)
return await domain.Purchase.get_or_none(id=purchase_id, project__workspace_id=workspace_id)
async def get_project_purchases(self, workspace_id: uuid.UUID, project_id: uuid.UUID) -> list[domain.Purchase]:
return (
await domain.Purchase.filter(workspace_id=workspace_id, project_id=project_id)
await domain.Purchase.filter(project__workspace_id=workspace_id, project_id=project_id)
.prefetch_related('channels', 'channels__channel')
.all()
)
@@ -315,6 +315,13 @@ class Postgres(DatabaseBase, Database):
.first()
)
async def get_purchase_channel_by_id(self, purchase_channel_id: uuid.UUID) -> domain.PurchaseChannel | None:
return (
await domain.PurchaseChannel.filter(id=purchase_channel_id)
.prefetch_related('channel', 'purchase', 'purchase__project')
.first()
)
async def get_purchase_channels_by_project(self, project_id: uuid.UUID) -> list[domain.PurchaseChannel]:
return (
await domain.PurchaseChannel.filter(
@@ -378,9 +385,9 @@ class Postgres(DatabaseBase, Database):
await domain.PurchaseChannel.filter(purchase_id=purchase_id, channel_id=channel_id).delete()
async def get_creative(self, workspace_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None:
return await domain.Creative.get_or_none(id=creative_id, workspace_id=workspace_id).prefetch_related(
'project', 'project__channel'
)
return await domain.Creative.get_or_none(
id=creative_id, project__workspace_id=workspace_id
).prefetch_related('project', 'project__channel')
async def update_creative(self, creative: domain.Creative) -> None:
await creative.save()
@@ -392,7 +399,7 @@ class Postgres(DatabaseBase, Database):
include_archived: bool = False,
allowed_project_ids: set[uuid.UUID] | None = None,
) -> list[domain.Creative]:
query = domain.Creative.filter(workspace_id=workspace_id)
query = domain.Creative.filter(project__workspace_id=workspace_id)
if project_id:
query = query.filter(project_id=project_id)
@@ -410,8 +417,16 @@ class Postgres(DatabaseBase, Database):
await domain.Creative.filter(id=creative_id).delete()
async def get_placement(self, workspace_id: uuid.UUID, placement_id: uuid.UUID) -> domain.Placement | None:
return await domain.Placement.get_or_none(id=placement_id, workspace_id=workspace_id).prefetch_related(
'project', 'project__channel', 'placement_channel', 'creative', 'post', 'post__channel'
return await domain.Placement.get_or_none(
id=placement_id, project__workspace_id=workspace_id
).prefetch_related(
'project',
'project__channel',
'purchase_channel',
'purchase_channel__channel',
'creative',
'post',
'post__channel',
)
async def create_placement(self, placement: domain.Placement) -> None:
@@ -431,7 +446,7 @@ class Postgres(DatabaseBase, Database):
date_from: datetime.datetime | None = None,
date_to: datetime.datetime | None = None,
) -> list[domain.Placement]:
query = domain.Placement.filter(workspace_id=workspace_id)
query = domain.Placement.filter(project__workspace_id=workspace_id)
if project_id:
query = query.filter(project_id=project_id)
@@ -440,7 +455,7 @@ class Postgres(DatabaseBase, Database):
return []
query = query.filter(project_id__in=list(allowed_project_ids))
if placement_channel_id:
query = query.filter(placement_channel_id=placement_channel_id)
query = query.filter(purchase_channel__channel_id=placement_channel_id)
if creative_id:
query = query.filter(creative_id=creative_id)
@@ -454,7 +469,7 @@ class Postgres(DatabaseBase, Database):
return (
await query.prefetch_related(
'project', 'project__channel', 'placement_channel', 'creative', 'post', 'post__channel'
'project', 'project__channel', 'purchase_channel', 'purchase_channel__channel', 'creative', 'post', 'post__channel'
)
.order_by('-wanted_placement_date')
.all()
@@ -464,9 +479,9 @@ class Postgres(DatabaseBase, Database):
await domain.Placement.filter(id=placement_id).delete()
async def get_placement_by_invite_link(self, invite_link: str) -> domain.Placement | None:
return await domain.Placement.get_or_none(invite_link=invite_link).prefetch_related(
'project', 'project__channel', 'placement_channel', 'creative'
)
return await domain.Placement.get_or_none(
purchase_channel__invite_link=invite_link
).prefetch_related('project', 'project__channel', 'purchase_channel', 'purchase_channel__channel', 'creative')
async def create_subscription(self, subscription: domain.Subscription) -> None:
await subscription.save()

View File

@@ -41,6 +41,7 @@ __all__ = (
'WorkspaceAccessDenied',
'WorkspaceInviteNotFound',
'WorkspaceInviteAlreadyExists',
'WorkspaceInviteAlreadyProcessed',
'WorkspaceMemberAlreadyExists',
'LoginTokenNotFound',
'LoginTokenExpired',
@@ -84,6 +85,7 @@ from .error import (
UserNotFound,
WorkspaceAccessDenied,
WorkspaceInviteAlreadyExists,
WorkspaceInviteAlreadyProcessed,
WorkspaceInviteNotFound,
WorkspaceMemberAlreadyExists,
WorkspaceNotFound,

View File

@@ -10,7 +10,6 @@ from .error import CreativeInviteLinkNotFound, CreativeMediaTooLarge, CreativeMu
if TYPE_CHECKING:
from .project import Project
from .workspace import Workspace
class CreativeStatus(str, enum.Enum):
@@ -27,15 +26,11 @@ class Creative(TimestampedModel):
buttons = fields.JSONField(default=list)
status = fields.CharEnumField(CreativeStatus, default=CreativeStatus.ACTIVE)
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
'models.Workspace', related_name='creatives', on_delete=fields.CASCADE, index=True
)
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
'models.Project', related_name='creatives', on_delete=fields.CASCADE, index=True
)
if TYPE_CHECKING:
workspace_id: UUID
project_id: UUID
class Meta:

View File

@@ -7,12 +7,10 @@ from tortoise import fields
from .base import TimestampedModel
if TYPE_CHECKING:
from .channel import Channel
from .creative import Creative
from .post import Post
from .project import Project
from .purchase import Purchase, PurchaseChannel
from .workspace import Workspace
from .purchase import PurchaseChannel
class PlacementStatus(str, enum.Enum):
@@ -31,27 +29,17 @@ class Placement(TimestampedModel):
wanted_placement_date = fields.DatetimeField(db_column='placement_date')
cost = fields.FloatField(null=True)
comment = fields.TextField(null=True)
invite_link = fields.CharField(max_length=512)
status = fields.CharEnumField(PlacementStatus, default=PlacementStatus.ACTIVE)
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
'models.Workspace', related_name='placements', on_delete=fields.CASCADE, index=True
)
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
'models.Project', related_name='placements', on_delete=fields.CASCADE, index=True
)
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
'models.Creative', related_name='placements', on_delete=fields.CASCADE, index=True
)
placement_channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
'models.Channel', related_name='placements', on_delete=fields.CASCADE, index=True
)
purchase: fields.ForeignKeyRelation['Purchase'] | None = fields.ForeignKeyField(
'models.Purchase', related_name='placements', on_delete=fields.CASCADE, null=True, index=True
)
purchase_channel: fields.ForeignKeyRelation['PurchaseChannel'] | None = fields.ForeignKeyField(
'models.PurchaseChannel', related_name='placements', on_delete=fields.CASCADE, null=True, index=True
purchase_channel: fields.ForeignKeyRelation['PurchaseChannel'] = fields.ForeignKeyField(
'models.PurchaseChannel', related_name='placements', on_delete=fields.CASCADE, index=True
)
post: fields.ForeignKeyRelation['Post'] | None = fields.ForeignKeyField(
@@ -59,13 +47,10 @@ class Placement(TimestampedModel):
)
if TYPE_CHECKING:
workspace_id: UUID
project_id: UUID
creative_id: UUID
placement_channel_id: UUID
purchase_id: UUID | None
post_id: UUID | None
purchase_channel_id: UUID | None
purchase_channel_id: UUID
class Meta:
table = 'placement'

View File

@@ -10,7 +10,6 @@ if TYPE_CHECKING:
from .channel import Channel
from .creative import Creative
from .project import Project
from .workspace import Workspace
class PurchaseStatus(enum.StrEnum):
@@ -54,9 +53,9 @@ class Purchase(TimestampedModel):
'models.Creative', related_name='purchases', on_delete=fields.CASCADE, index=True
)
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
'models.Workspace', related_name='purchases', on_delete=fields.CASCADE, index=True
)
if TYPE_CHECKING:
project_id: uuid.UUID
creative_id: uuid.UUID
class Meta:
table = 'purchase'

View File

@@ -47,9 +47,7 @@ class GetPlacementInput(pydantic.BaseModel):
class CreatePlacementInput(pydantic.BaseModel):
project_id: uuid.UUID
placement_channel_id: uuid.UUID
creative_id: uuid.UUID
purchase_channel_id: uuid.UUID
placement_date: datetime.datetime
cost: float | None = None
comment: str | None = None

View File

@@ -175,6 +175,8 @@ class Database(typing.Protocol):
self, purchase_channel_id: UUID, purchase_id: UUID
) -> domain.PurchaseChannel | None: ...
async def get_purchase_channel_by_id(self, purchase_channel_id: UUID) -> domain.PurchaseChannel | None: ...
async def get_purchase_channels_by_project(self, project_id: UUID) -> list[domain.PurchaseChannel]: ...
async def add_channel_to_purchase(
@@ -225,6 +227,10 @@ class Database(typing.Protocol):
date_to: datetime.datetime | None = None,
) -> list[domain.Placement]: ...
async def create_placement(self, placement: domain.Placement) -> None: ...
async def update_placement(self, placement: domain.Placement) -> None: ...
async def delete_placement(self, placement_id: UUID) -> None: ...
async def get_placement_by_invite_link(self, invite_link: str) -> domain.Placement | None: ...
@@ -398,7 +404,7 @@ class Usecase:
if purchase:
return purchase
purchase = domain.Purchase(workspace_id=project.workspace_id, project_id=project.id, creative_id=creative.id)
purchase = domain.Purchase(project_id=project.id, creative_id=creative.id)
await self.database.create_purchase(purchase)
return purchase

View File

@@ -47,8 +47,9 @@ async def get_channel_analytics(
if input.project_id is None and allowed_project_ids is not None:
channel_map: dict[UUID, domain.Channel] = {}
for placement in placements:
if placement.placement_channel:
channel_map[placement.placement_channel_id] = placement.placement_channel
purchase_channel = placement.purchase_channel
if purchase_channel and purchase_channel.channel:
channel_map[purchase_channel.channel_id] = purchase_channel.channel
channels = list(channel_map.values())
@dataclass
@@ -69,7 +70,10 @@ async def get_channel_analytics(
subscriptions_counts = await self.database.count_subscriptions_by_placement_batch(placement_ids)
for p in placements:
stats = channel_stats.get(p.placement_channel_id)
purchase_channel = p.purchase_channel
if not purchase_channel:
continue
stats = channel_stats.get(purchase_channel.channel_id)
if stats is None:
continue

View File

@@ -131,19 +131,21 @@ async def get_overview_analytics(
project_meta: dict[UUID, domain.Project] = {}
for placement in current_placements:
placement_date = placement.wanted_placement_date.date()
placement_day = placement.wanted_placement_date.date()
project_meta[placement.project_id] = placement.project
cost_value = placement.cost or 0.0
cost_per_day[placement_date] += cost_value
cost_per_day[placement_day] += cost_value
project_spending_map[placement.project_id] += cost_value
subs = subs_per_placement_current.get(placement.id, 0)
channel_id = placement.placement_channel_id
if channel_id not in channel_stats:
channel_stats[channel_id] = ChannelAggregate(channel=placement.placement_channel)
channel_stats[channel_id].total_cost += cost_value
channel_stats[channel_id].total_subs += subs
purchase_channel = placement.purchase_channel
if purchase_channel:
channel_id = purchase_channel.channel_id
if channel_id not in channel_stats:
channel_stats[channel_id] = ChannelAggregate(channel=purchase_channel.channel)
channel_stats[channel_id].total_cost += cost_value
channel_stats[channel_id].total_subs += subs
start_date = input.date_from.date()
end_date = input.date_to.date()

View File

@@ -57,8 +57,8 @@ async def get_placements_analytics(
id=p.id,
project_id=p.project_id,
project_channel_title=p.project.channel.title,
placement_channel_id=p.placement_channel_id,
placement_channel_title=p.placement_channel.title,
placement_channel_id=p.purchase_channel.channel_id,
placement_channel_title=p.purchase_channel.channel.title,
creative_id=p.creative_id,
creative_name=p.creative.name,
placement_date=p.wanted_placement_date,

View File

@@ -47,4 +47,5 @@ async def attach_channel_to_workspace(
title=channel.title,
username=channel.username,
status=project.status,
purchase_invite_type_default=project.purchase_invite_type_default,
)

View File

@@ -43,7 +43,6 @@ async def create_creative(
buttons=[button.model_dump() for button in input.buttons],
status=domain.CreativeStatus.ACTIVE,
project_id=project.id,
workspace_id=workspace_id,
)
await self.database.create_creative(creative)

View File

@@ -1,84 +0,0 @@
import logging
import uuid
from typing import TYPE_CHECKING
import domain.purchase
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def create_placement(
self: 'Usecase', input: dto.CreatePlacementInput, user_id: uuid.UUID, workspace_id: uuid.UUID
) -> dto.PlacementOutput:
await self.ensure_workspace_permission(
workspace_id,
user_id,
domain.PermissionKey.PLACEMENTS_WRITE,
for_project_id=input.project_id,
)
project = await self.database.get_project(workspace_id, project_id=input.project_id)
if not project:
raise domain.ProjectNotFound(input.project_id)
placement_channel = await self.database.get_channel(channel_id=input.placement_channel_id)
if not placement_channel:
raise domain.ChannelNotFound(input.placement_channel_id)
creative = await self.database.get_creative(workspace_id, input.creative_id)
if not creative:
raise domain.CreativeNotFound(input.creative_id)
if creative.project_id != project.id:
raise domain.CreativeNotFound(input.creative_id)
# Берем тип ссылки из настроек проекта
requires_approval = project.purchase_invite_type_default == domain.purchase.InviteLinkType.APPROVAL
if project.channel.telegram_id is None:
raise domain.ChannelNotFound(project.channel.id)
invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval)
log.info(
'Created invite link for project=%s channel_telegram_id=%s requires_approval=%s invite_link=%s',
project.id,
project.channel.telegram_id,
requires_approval,
invite_link,
)
placement = domain.Placement(
project_id=input.project_id,
placement_channel_id=input.placement_channel_id,
creative_id=input.creative_id,
workspace_id=workspace_id,
placement_date=input.placement_date,
cost=input.cost,
comment=input.comment,
invite_link=invite_link,
status=domain.PlacementStatus.ACTIVE,
)
await self.database.create_placement(placement)
return dto.PlacementOutput(
id=placement.id,
project_id=placement.project_id,
project_channel_title=project.channel.title,
placement_channel_id=placement.placement_channel_id,
placement_channel_title=placement_channel.title,
creative_id=placement.creative_id,
creative_name=creative.name,
placement_date=placement.wanted_placement_date,
cost=placement.cost,
comment=placement.comment,
ad_post_url=None,
invite_link_type=project.purchase_invite_type_default,
invite_link=placement.invite_link,
status=placement.status,
subscriptions_count=0,
created_at=placement.created_at,
)

View File

@@ -1,24 +0,0 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def delete_placement(self: 'Usecase', input: dto.DeletePlacementInput) -> None:
context = await self.ensure_workspace_permission(
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_WRITE
)
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
if not placement:
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
raise domain.PlacementNotFound(input.placement_id)
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_WRITE, placement.project_id)
await self.database.delete_placement(input.placement_id)

View File

@@ -44,12 +44,8 @@ async def fetch_placement_post_cycle(self: 'Usecase', interval_seconds: int) ->
wanted_placement_date=post.created_at,
cost=None,
comment=None,
invite_link=purchase_channel.invite_link,
workspace_id=purchase_channel.purchase.workspace_id,
project_id=purchase_channel.purchase.project_id,
creative_id=purchase_channel.purchase.creative_id,
placement_channel_id=purchase_channel.channel_id,
purchase_id=purchase_channel.purchase_id,
purchase_channel_id=purchase_channel.id,
post=post,
status=domain.PlacementStatus.ACTIVE,

View File

@@ -22,6 +22,10 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id)
ad_post_url = placement.post.url if placement.post else None
purchase_channel = placement.purchase_channel
if not purchase_channel or not purchase_channel.channel:
log.warning('Placement %s missing purchase channel data', placement.id)
raise domain.PurchaseChannelNotFound()
subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id)
@@ -29,17 +33,19 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl
id=placement.id,
project_id=placement.project_id,
project_channel_title=placement.project.channel.title,
placement_channel_id=placement.placement_channel_id,
placement_channel_title=placement.placement_channel.title,
placement_channel_id=purchase_channel.channel_id,
placement_channel_title=purchase_channel.channel.title,
creative_id=placement.creative_id,
creative_name=placement.creative.name,
placement_date=placement.wanted_placement_date,
cost=placement.cost,
comment=placement.comment,
ad_post_url=ad_post_url,
invite_link_type=placement.project.purchase_invite_type_default,
invite_link=placement.invite_link,
invite_link_type=purchase_channel.invite_link_type,
invite_link=purchase_channel.invite_link,
status=placement.status,
subscriptions_count=subscriptions_count,
purchase_id=purchase_channel.purchase_id,
purchase_channel_id=purchase_channel.id,
created_at=placement.created_at,
)

View File

@@ -1,3 +1,4 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
@@ -5,6 +6,8 @@ from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> list[dto.PlacementOutput]:
context = await self.ensure_workspace_permission(
@@ -32,24 +35,30 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> list
placement_outputs = []
for placement in placements:
ad_post_url = placement.post.url if placement.post else None
purchase_channel = placement.purchase_channel
if not purchase_channel or not purchase_channel.channel:
log.warning('Placement %s missing purchase channel data', placement.id)
continue
placement_outputs.append(
dto.PlacementOutput(
id=placement.id,
project_id=placement.project_id,
project_channel_title=placement.project.channel.title,
placement_channel_id=placement.placement_channel_id,
placement_channel_title=placement.placement_channel.title,
placement_channel_id=purchase_channel.channel_id,
placement_channel_title=purchase_channel.channel.title,
creative_id=placement.creative_id,
creative_name=placement.creative.name,
placement_date=placement.wanted_placement_date,
cost=placement.cost,
comment=placement.comment,
ad_post_url=ad_post_url,
invite_link_type=placement.project.purchase_invite_type_default,
invite_link=placement.invite_link,
invite_link_type=purchase_channel.invite_link_type,
invite_link=purchase_channel.invite_link,
status=placement.status,
subscriptions_count=subscriptions_counts.get(placement.id, 0),
purchase_id=purchase_channel.purchase_id,
purchase_channel_id=purchase_channel.id,
created_at=placement.created_at,
)
)

View File

@@ -1,61 +0,0 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def update_placement(
self: 'Usecase',
placement_id: uuid.UUID,
input: dto.UpdatePlacementInput,
user_id: uuid.UUID,
workspace_id: uuid.UUID,
) -> dto.PlacementOutput:
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
placement = await self.database.get_placement(workspace_id, placement_id)
if not placement:
log.warning('Placement %s not found for user %s', placement_id, user_id)
raise domain.PlacementNotFound(placement_id)
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_WRITE, placement.project_id)
if input.placement_date is not None:
placement.wanted_placement_date = input.placement_date
if input.cost is not None:
placement.cost = input.cost
if input.comment is not None:
placement.comment = input.comment
if input.status is not None:
placement.status = input.status
await self.database.update_placement(placement)
ad_post_url = placement.post.url if placement.post else None
subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id)
return dto.PlacementOutput(
id=placement.id,
project_id=placement.project_id,
project_channel_title=placement.project.channel.title,
placement_channel_id=placement.placement_channel_id,
placement_channel_title=placement.placement_channel.title,
creative_id=placement.creative_id,
creative_name=placement.creative.name,
placement_date=placement.wanted_placement_date,
cost=placement.cost,
comment=placement.comment,
ad_post_url=ad_post_url,
invite_link_type=placement.project.purchase_invite_type_default,
invite_link=placement.invite_link,
status=placement.status,
subscriptions_count=subscriptions_count,
created_at=placement.created_at,
)

View File

@@ -99,7 +99,6 @@ async def create_purchase(
details = input.details
purchase = domain.Purchase(
workspace_id=workspace_id,
project_id=project.id,
creative_id=creative.id,
placement_at=details.placement_at if details else None,

View File

@@ -3,7 +3,9 @@ package screens
import (
"context"
"fmt"
"strings"
"example.com/m/adapter/backend"
"example.com/m/bot"
"github.com/NicoNex/echotron/v3"
"github.com/rs/zerolog/log"
@@ -42,6 +44,8 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
return
}
placementsByChannel := s.loadPlacementsByChannel(b, jwt, purchase)
// Получаем информацию о креативе
creative, err := b.Backend.GetCreative(
context.Background(),
@@ -54,13 +58,18 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
creative = nil
}
totalPlacements := 0
for _, items := range placementsByChannel {
totalPlacements += len(items)
}
text := "<i>… Закупы Детали</i>\n\n<b>Детали закупа</b>\n\n"
// Информация о креативе
if creative != nil {
text += fmt.Sprintf(" <b>Креатив:</b> %s\n", creative.Name)
text += fmt.Sprintf(" <b>Креатив:</b> %s\n", creative.Name)
} else {
text += fmt.Sprintf(" <b>Креатив ID:</b> %s\n", purchase.CreativeID)
text += fmt.Sprintf(" <b>Креатив ID:</b> %s\n", purchase.CreativeID)
}
// Статус закупа
@@ -70,7 +79,9 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
statusSymbol = "■"
statusText = "Архивирован"
}
text += fmt.Sprintf(" <b>Статус:</b> %s %s\n\n", statusSymbol, statusText)
text += fmt.Sprintf(" <b>Статус:</b> %s %s\n", statusSymbol, statusText)
text += fmt.Sprintf("• <b>Каналов:</b> %d\n", len(purchase.Channels))
text += fmt.Sprintf("• <b>Размещений:</b> %d\n\n", totalPlacements)
// Информация о каналах
if len(purchase.Channels) == 0 {
@@ -79,20 +90,7 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
text += fmt.Sprintf("<b>Каналы (%d):</b>\n\n", len(purchase.Channels))
for i, ch := range purchase.Channels {
// Определяем статус
statusSymbol := "○"
switch ch.Status {
case "planned":
statusSymbol = "○"
case "approved":
statusSymbol = "◉"
case "rejected":
statusSymbol = "⊗"
case "in_progress":
statusSymbol = "◐"
case "completed":
statusSymbol = "●"
}
chStatusText := formatPurchaseChannelStatus(ch.Status)
// Название канала
channelName := "Без названия"
@@ -102,16 +100,43 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
channelName = "@" + *ch.Channel.Username
}
text += fmt.Sprintf("%d. %s <b>%s</b>\n", i+1, statusSymbol, channelName)
text += fmt.Sprintf("%d. <b>%s</b>\n", i+1, channelName)
text += fmt.Sprintf(" ◦ <i>%s</i>\n", chStatusText)
// Стоимость
if ch.Details != nil && ch.Details.Cost != nil {
text += fmt.Sprintf(" Стоимость: %.0f₽\n", ch.Details.Cost.Value)
text += fmt.Sprintf(" Стоимость: %.0f₽\n", ch.Details.Cost.Value)
}
// Дата размещения (план)
if ch.Details != nil && ch.Details.PlacementAt != nil && *ch.Details.PlacementAt != "" {
text += fmt.Sprintf(" ◦ План: %s\n", *ch.Details.PlacementAt)
}
// Комментарий
if ch.Comment != nil && *ch.Comment != "" {
text += fmt.Sprintf(" %s\n", *ch.Comment)
text += fmt.Sprintf(" %s\n", *ch.Comment)
}
// Информация о размещении
placements := placementsByChannel[ch.ID]
if len(placements) == 0 {
text += " ◦ Размещение: <b>нет</b>\n"
} else {
placement := placements[0]
text += fmt.Sprintf(" ◦ Размещение: <b>есть</b> (%d)\n", len(placements))
if placement.Status != "" {
text += fmt.Sprintf(" ◦ Статус размещения: %s\n", formatPlacementStatus(placement.Status))
}
if placement.PlacementDate != "" {
text += fmt.Sprintf(" ◦ Дата: %s\n", placement.PlacementDate)
}
if placement.AdPostURL != nil && *placement.AdPostURL != "" {
text += fmt.Sprintf(" ◦ Пост: %s\n", *placement.AdPostURL)
}
if placement.SubscriptionsCount > 0 {
text += fmt.Sprintf(" ◦ Подписки: %d\n", placement.SubscriptionsCount)
}
}
// Тип invite link
@@ -119,7 +144,7 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
if ch.InviteLinkType == "approval" {
inviteLinkTypeText = "с одобрением"
}
text += fmt.Sprintf(" Ссылка: %s\n", inviteLinkTypeText)
text += fmt.Sprintf(" Ссылка: %s\n", inviteLinkTypeText)
text += "\n"
}
@@ -136,6 +161,94 @@ func (s *PurchaseDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt str
b.Render(text, keyboard, mode)
}
func (s *PurchaseDetails) loadPlacementsByChannel(
b *bot.Bot,
jwt string,
purchase *backend.Purchase,
) map[string][]backend.Placement {
placementsByChannel := make(map[string][]backend.Placement)
if purchase == nil || purchase.CreativeID == "" || len(purchase.Channels) == 0 {
return placementsByChannel
}
projectID := s.ProjectID
creativeID := purchase.CreativeID
pageSize := 50
maxPages := 5
foundChannels := make(map[string]bool)
for page := 1; page <= maxPages; page++ {
resp, err := b.Backend.GetPlacements(
context.Background(),
jwt,
s.WorkspaceID,
&projectID,
nil,
&creativeID,
false,
page,
pageSize,
)
if err != nil {
log.Error().Err(err).Msg("Failed to get placements for purchase details")
break
}
for _, placement := range resp.Items {
if placement.PurchaseID == nil || *placement.PurchaseID != purchase.ID {
continue
}
if placement.PurchaseChannelID == nil || *placement.PurchaseChannelID == "" {
continue
}
chID := *placement.PurchaseChannelID
placementsByChannel[chID] = append(placementsByChannel[chID], placement)
foundChannels[chID] = true
}
if resp.Pages <= page || len(foundChannels) == len(purchase.Channels) {
break
}
}
return placementsByChannel
}
func formatPurchaseChannelStatus(status string) string {
switch status {
case "planned":
return "Планируется"
case "approved":
return "Согласовано"
case "rejected":
return "Отклонено"
case "in_progress":
return "В работе"
case "completed":
return "Завершено"
default:
return "Неизвестно"
}
}
func formatPlacementStatus(status string) string {
normalized := strings.TrimSpace(strings.ToLower(status))
switch normalized {
case "planned":
return "Планируется"
case "approved":
return "Согласовано"
case "rejected":
return "Отклонено"
case "in_progress":
return "В работе"
case "completed":
return "Размещено"
default:
return status
}
}
func (s *PurchaseDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return

View File

@@ -1283,9 +1283,13 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) {
}
}
b.SendNew("✅ Закуп создан", bot.Keyboard(
bot.Row(bot.Button("● Готово", "done")),
))
b.SetState(&PurchaseDetails{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
PurchaseID: purchase.ID,
BackState: s.BackState,
}, bot.NewMessage)
}
func (s *PurchaseOptionalDetails) buildPurchaseDetails(purchaseType *string) *backend.PurchaseDetails {
@@ -1449,5 +1453,15 @@ func (s *PurchaseOptionalDetails) injectInviteLink(text, inviteLink string) stri
return text
}
re := regexp.MustCompile(`<a\s+class="tg-link">([^<]*)</a>`)
return re.ReplaceAllString(text, fmt.Sprintf(`<a href="%s">$1</a>`, inviteLink))
return re.ReplaceAllStringFunc(text, func(match string) string {
sub := re.FindStringSubmatch(match)
if len(sub) < 2 {
return match
}
inner := strings.TrimSpace(sub[1])
if inner == "" {
return fmt.Sprintf(`<a href="%s">%s</a>`, inviteLink, inviteLink)
}
return fmt.Sprintf(`<a href="%s">%s</a>`, inviteLink, sub[1])
})
}

View File

@@ -245,22 +245,13 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
return
}
// Показываем успешное создание
successText := fmt.Sprintf(`
<i>… Создание Готово</i>
<b>Закуп создан</b>
▪ <b>ID:</b> %s
▪ <b>Креатив:</b> %s
▪ <b>Каналов:</b> %d
Закуп создан и готов к работе
`, purchase.ID, purchase.CreativeID, len(purchase.Channels))
b.Edit(successText, bot.Keyboard(
bot.Row(bot.Button("● Готово", "done")),
))
b.SetState(&PurchaseDetails{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
PurchaseID: purchase.ID,
BackState: s.BackState,
}, bot.EditMessage)
}
func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update) {