Files
AdPlatform-Server/Gateway/Migrations/001_ChannelConfig.sql
2026-03-14 13:50:09 -07:00

322 lines
16 KiB
Transact-SQL

-- ============================================================
-- 001_ChannelConfig.sql
-- Move channel provider configuration from appsettings.json
-- into database-driven configuration.
-- ============================================================
-- ── Table ──────────────────────────────────────────────────
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'tbChannelConfig')
BEGIN
CREATE TABLE dbo.tbChannelConfig (
chcChannelType VARCHAR(50) NOT NULL PRIMARY KEY,
chcDisplayName NVARCHAR(100) NOT NULL,
chcDescription NVARCHAR(500) NULL,
chcIcon VARCHAR(50) NULL,
chcColor VARCHAR(20) NULL,
chcEnabled BIT NOT NULL DEFAULT 1,
chcIsStub BIT NOT NULL DEFAULT 1,
chcEndpoint VARCHAR(500) NULL,
chcInternalKey VARCHAR(500) NULL,
chcMinDailyBudget DECIMAL(10,2) NOT NULL DEFAULT 5.00,
chcMinMonthlyBudget DECIMAL(10,2) NOT NULL DEFAULT 150.00,
chcSupportedObjectives NVARCHAR(500) NULL, -- JSON array: ["sales","leads","traffic"]
chcSupportedCreativeFormats NVARCHAR(500) NULL, -- JSON array: ["text","image","video"]
chcApprovalEstimateHours INT NOT NULL DEFAULT 24,
chcMetricsRefreshIntervalMinutes INT NOT NULL DEFAULT 60,
chcAuthMethod VARCHAR(50) NULL,
chcKeyVaultSecretName VARCHAR(200) NULL,
chcStatusMappings NVARCHAR(MAX) NULL, -- JSON object: {"ENABLED":"active",...}
chcSortOrder INT NOT NULL DEFAULT 0,
chcCreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
chcUpdatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);
PRINT 'Created table tbChannelConfig';
END
GO
-- ── Stored Procedure ───────────────────────────────────────
CREATE OR ALTER PROCEDURE dbo.spChannelConfig
@action VARCHAR(50),
@rqst NVARCHAR(MAX) = '{}',
@resp NVARCHAR(MAX) OUTPUT
AS
BEGIN
SET NOCOUNT ON;
-- ── list: return all enabled channels ──
IF @action = 'list'
BEGIN
SET @resp = (
SELECT
chcChannelType AS channelType,
chcDisplayName AS displayName,
chcDescription AS [description],
chcIcon AS icon,
chcColor AS color,
chcEnabled AS [enabled],
chcIsStub AS isStub,
chcEndpoint AS endpoint,
chcInternalKey AS internalKey,
chcMinDailyBudget AS minDailyBudget,
chcMinMonthlyBudget AS minMonthlyBudget,
JSON_QUERY(chcSupportedObjectives) AS supportedObjectives,
JSON_QUERY(chcSupportedCreativeFormats) AS supportedCreativeFormats,
chcApprovalEstimateHours AS approvalEstimateHours,
chcMetricsRefreshIntervalMinutes AS metricsRefreshIntervalMinutes,
chcAuthMethod AS authMethod,
chcKeyVaultSecretName AS keyVaultSecretName,
JSON_QUERY(chcStatusMappings) AS statusMappings,
chcSortOrder AS sortOrder
FROM dbo.tbChannelConfig
ORDER BY chcSortOrder, chcChannelType
FOR JSON PATH
);
IF @resp IS NULL SET @resp = '[]';
SET @resp = '{"ok":true,"channels":' + @resp + '}';
RETURN;
END
-- ── listAll: return all channels including disabled (for admin) ──
IF @action = 'listAll'
BEGIN
SET @resp = (
SELECT
chcChannelType AS channelType,
chcDisplayName AS displayName,
chcDescription AS [description],
chcIcon AS icon,
chcColor AS color,
chcEnabled AS [enabled],
chcIsStub AS isStub,
chcEndpoint AS endpoint,
chcMinDailyBudget AS minDailyBudget,
chcMinMonthlyBudget AS minMonthlyBudget,
JSON_QUERY(chcSupportedObjectives) AS supportedObjectives,
JSON_QUERY(chcSupportedCreativeFormats) AS supportedCreativeFormats,
chcApprovalEstimateHours AS approvalEstimateHours,
chcMetricsRefreshIntervalMinutes AS metricsRefreshIntervalMinutes,
chcAuthMethod AS authMethod,
chcKeyVaultSecretName AS keyVaultSecretName,
JSON_QUERY(chcStatusMappings) AS statusMappings,
chcSortOrder AS sortOrder,
chcCreatedAt AS createdAt,
chcUpdatedAt AS updatedAt
FROM dbo.tbChannelConfig
ORDER BY chcSortOrder, chcChannelType
FOR JSON PATH
);
IF @resp IS NULL SET @resp = '[]';
SET @resp = '{"ok":true,"channels":' + @resp + '}';
RETURN;
END
-- ── get: return single channel by type ──
IF @action = 'get'
BEGIN
DECLARE @channelType VARCHAR(50) = JSON_VALUE(@rqst, '$.channelType');
IF NOT EXISTS (SELECT 1 FROM dbo.tbChannelConfig WHERE chcChannelType = @channelType)
BEGIN
SET @resp = '{"ok":false,"error":"Channel not found"}';
RETURN;
END
SET @resp = (
SELECT
chcChannelType AS channelType,
chcDisplayName AS displayName,
chcDescription AS [description],
chcIcon AS icon,
chcColor AS color,
chcEnabled AS [enabled],
chcIsStub AS isStub,
chcEndpoint AS endpoint,
chcMinDailyBudget AS minDailyBudget,
chcMinMonthlyBudget AS minMonthlyBudget,
JSON_QUERY(chcSupportedObjectives) AS supportedObjectives,
JSON_QUERY(chcSupportedCreativeFormats) AS supportedCreativeFormats,
chcApprovalEstimateHours AS approvalEstimateHours,
chcMetricsRefreshIntervalMinutes AS metricsRefreshIntervalMinutes,
chcAuthMethod AS authMethod,
chcKeyVaultSecretName AS keyVaultSecretName,
JSON_QUERY(chcStatusMappings) AS statusMappings
FROM dbo.tbChannelConfig
WHERE chcChannelType = @channelType
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
);
SET @resp = '{"ok":true,"channel":' + @resp + '}';
RETURN;
END
-- ── upsert: create or update a channel (admin) ──
IF @action = 'upsert'
BEGIN
DECLARE @uChannelType VARCHAR(50) = JSON_VALUE(@rqst, '$.channelType');
IF @uChannelType IS NULL
BEGIN
SET @resp = '{"ok":false,"error":"channelType is required"}';
RETURN;
END
MERGE dbo.tbChannelConfig AS tgt
USING (SELECT @uChannelType AS chcChannelType) AS src
ON tgt.chcChannelType = src.chcChannelType
WHEN MATCHED THEN
UPDATE SET
chcDisplayName = ISNULL(JSON_VALUE(@rqst, '$.displayName'), tgt.chcDisplayName),
chcDescription = ISNULL(JSON_VALUE(@rqst, '$.description'), tgt.chcDescription),
chcIcon = ISNULL(JSON_VALUE(@rqst, '$.icon'), tgt.chcIcon),
chcColor = ISNULL(JSON_VALUE(@rqst, '$.color'), tgt.chcColor),
chcEnabled = ISNULL(CAST(JSON_VALUE(@rqst, '$.enabled') AS BIT), tgt.chcEnabled),
chcIsStub = ISNULL(CAST(JSON_VALUE(@rqst, '$.isStub') AS BIT), tgt.chcIsStub),
chcEndpoint = CASE WHEN JSON_VALUE(@rqst, '$.endpoint') IS NOT NULL
THEN JSON_VALUE(@rqst, '$.endpoint')
ELSE tgt.chcEndpoint END,
chcInternalKey = CASE WHEN JSON_VALUE(@rqst, '$.internalKey') IS NOT NULL
THEN JSON_VALUE(@rqst, '$.internalKey')
ELSE tgt.chcInternalKey END,
chcMinDailyBudget = ISNULL(CAST(JSON_VALUE(@rqst, '$.minDailyBudget') AS DECIMAL(10,2)), tgt.chcMinDailyBudget),
chcMinMonthlyBudget = ISNULL(CAST(JSON_VALUE(@rqst, '$.minMonthlyBudget') AS DECIMAL(10,2)), tgt.chcMinMonthlyBudget),
chcSupportedObjectives = CASE WHEN JSON_QUERY(@rqst, '$.supportedObjectives') IS NOT NULL
THEN JSON_QUERY(@rqst, '$.supportedObjectives')
ELSE tgt.chcSupportedObjectives END,
chcSupportedCreativeFormats = CASE WHEN JSON_QUERY(@rqst, '$.supportedCreativeFormats') IS NOT NULL
THEN JSON_QUERY(@rqst, '$.supportedCreativeFormats')
ELSE tgt.chcSupportedCreativeFormats END,
chcApprovalEstimateHours = ISNULL(CAST(JSON_VALUE(@rqst, '$.approvalEstimateHours') AS INT), tgt.chcApprovalEstimateHours),
chcMetricsRefreshIntervalMinutes = ISNULL(CAST(JSON_VALUE(@rqst, '$.metricsRefreshIntervalMinutes') AS INT), tgt.chcMetricsRefreshIntervalMinutes),
chcAuthMethod = ISNULL(JSON_VALUE(@rqst, '$.authMethod'), tgt.chcAuthMethod),
chcKeyVaultSecretName = ISNULL(JSON_VALUE(@rqst, '$.keyVaultSecretName'), tgt.chcKeyVaultSecretName),
chcStatusMappings = CASE WHEN JSON_QUERY(@rqst, '$.statusMappings') IS NOT NULL
THEN JSON_QUERY(@rqst, '$.statusMappings')
ELSE tgt.chcStatusMappings END,
chcSortOrder = ISNULL(CAST(JSON_VALUE(@rqst, '$.sortOrder') AS INT), tgt.chcSortOrder),
chcUpdatedAt = GETUTCDATE()
WHEN NOT MATCHED THEN
INSERT (chcChannelType, chcDisplayName, chcDescription, chcIcon, chcColor,
chcEnabled, chcIsStub, chcEndpoint, chcInternalKey,
chcMinDailyBudget, chcMinMonthlyBudget,
chcSupportedObjectives, chcSupportedCreativeFormats,
chcApprovalEstimateHours, chcMetricsRefreshIntervalMinutes,
chcAuthMethod, chcKeyVaultSecretName, chcStatusMappings, chcSortOrder)
VALUES (
@uChannelType,
JSON_VALUE(@rqst, '$.displayName'),
JSON_VALUE(@rqst, '$.description'),
JSON_VALUE(@rqst, '$.icon'),
JSON_VALUE(@rqst, '$.color'),
ISNULL(CAST(JSON_VALUE(@rqst, '$.enabled') AS BIT), 1),
ISNULL(CAST(JSON_VALUE(@rqst, '$.isStub') AS BIT), 1),
JSON_VALUE(@rqst, '$.endpoint'),
JSON_VALUE(@rqst, '$.internalKey'),
ISNULL(CAST(JSON_VALUE(@rqst, '$.minDailyBudget') AS DECIMAL(10,2)), 5.00),
ISNULL(CAST(JSON_VALUE(@rqst, '$.minMonthlyBudget') AS DECIMAL(10,2)), 150.00),
JSON_QUERY(@rqst, '$.supportedObjectives'),
JSON_QUERY(@rqst, '$.supportedCreativeFormats'),
ISNULL(CAST(JSON_VALUE(@rqst, '$.approvalEstimateHours') AS INT), 24),
ISNULL(CAST(JSON_VALUE(@rqst, '$.metricsRefreshIntervalMinutes') AS INT), 60),
JSON_VALUE(@rqst, '$.authMethod'),
JSON_VALUE(@rqst, '$.keyVaultSecretName'),
JSON_QUERY(@rqst, '$.statusMappings'),
ISNULL(CAST(JSON_VALUE(@rqst, '$.sortOrder') AS INT), 0)
);
SET @resp = '{"ok":true}';
RETURN;
END
-- ── delete: remove a channel (admin) ──
IF @action = 'delete'
BEGIN
DECLARE @dChannelType VARCHAR(50) = JSON_VALUE(@rqst, '$.channelType');
DELETE FROM dbo.tbChannelConfig WHERE chcChannelType = @dChannelType;
SET @resp = '{"ok":true,"deleted":' + CAST(@@ROWCOUNT AS VARCHAR) + '}';
RETURN;
END
SET @resp = '{"ok":false,"error":"Unknown action: ' + @action + '"}';
END
GO
-- ── Seed Data ──────────────────────────────────────────────
-- Google Ads
IF NOT EXISTS (SELECT 1 FROM dbo.tbChannelConfig WHERE chcChannelType = 'google_ads')
INSERT INTO dbo.tbChannelConfig (
chcChannelType, chcDisplayName, chcDescription, chcIcon, chcColor,
chcEnabled, chcIsStub, chcEndpoint,
chcMinDailyBudget, chcMinMonthlyBudget,
chcSupportedObjectives, chcSupportedCreativeFormats,
chcApprovalEstimateHours, chcAuthMethod, chcKeyVaultSecretName,
chcStatusMappings, chcSortOrder
) VALUES (
'google_ads',
'Google Ads',
'Search, Display, Shopping & Performance Max across Google properties',
'google', '#4285F4',
1, 0, NULL,
10.00, 300.00,
'["awareness","traffic","conversions","leads","sales"]',
'["text","image","responsive","video"]',
24, 'mcc', 'google-ads-refresh-token',
'{"ENABLED":"active","Enabled":"active","PAUSED":"paused","Paused":"paused","REMOVED":"cancelled","Removed":"cancelled","UNKNOWN":"error","UNSPECIFIED":"error"}',
1
);
-- Meta
IF NOT EXISTS (SELECT 1 FROM dbo.tbChannelConfig WHERE chcChannelType = 'meta')
INSERT INTO dbo.tbChannelConfig (
chcChannelType, chcDisplayName, chcDescription, chcIcon, chcColor,
chcEnabled, chcIsStub, chcEndpoint,
chcMinDailyBudget, chcMinMonthlyBudget,
chcSupportedObjectives, chcSupportedCreativeFormats,
chcApprovalEstimateHours, chcAuthMethod, chcKeyVaultSecretName,
chcStatusMappings, chcSortOrder
) VALUES (
'meta',
'Meta Ads',
'Facebook, Instagram, Messenger & Threads advertising',
'meta', '#1877F2',
1, 1, NULL,
5.00, 250.00,
'["awareness","traffic","conversions","leads","sales"]',
'["image","video","carousel","stories"]',
48, 'oauth2', 'meta-access-token',
'{"ACTIVE":"active","PAUSED":"paused","DELETED":"cancelled","ARCHIVED":"completed","IN_PROCESS":"pending","WITH_ISSUES":"error","CAMPAIGN_PAUSED":"paused","ADSET_PAUSED":"paused","DISAPPROVED":"error","PREAPPROVED":"pending","PENDING_REVIEW":"pending","PENDING_BILLING_INFO":"error"}',
2
);
-- TikTok
IF NOT EXISTS (SELECT 1 FROM dbo.tbChannelConfig WHERE chcChannelType = 'tiktok')
INSERT INTO dbo.tbChannelConfig (
chcChannelType, chcDisplayName, chcDescription, chcIcon, chcColor,
chcEnabled, chcIsStub, chcEndpoint,
chcMinDailyBudget, chcMinMonthlyBudget,
chcSupportedObjectives, chcSupportedCreativeFormats,
chcApprovalEstimateHours, chcAuthMethod, chcKeyVaultSecretName,
chcStatusMappings, chcSortOrder
) VALUES (
'tiktok',
'TikTok Ads',
'In-feed video ads across TikTok and partner apps',
'tiktok', '#000000',
1, 1, NULL,
20.00, 200.00,
'["awareness","traffic","conversions","leads","sales"]',
'["video","image","spark_ads"]',
24, 'oauth2', 'tiktok-access-token',
'{"ENABLE":"active","CAMPAIGN_STATUS_ENABLE":"active","DISABLE":"paused","CAMPAIGN_STATUS_DISABLE":"paused","DELETE":"cancelled","CAMPAIGN_STATUS_DELETE":"cancelled","BUDGET_EXCEED":"paused","CAMPAIGN_STATUS_BUDGET_EXCEED":"paused","ADVERTISER_AUDIT_DENY":"error","CAMPAIGN_STATUS_ADVERTISER_AUDIT_DENY":"error","NOT_DELETE":"active","ADVERTISER_AUDIT":"pending","CAMPAIGN_STATUS_ADVERTISER_AUDIT":"pending","REAUDIT":"pending","ALL":"active"}',
3
);
PRINT 'Channel config seeded successfully';
GO