642 lines
25 KiB
C#
642 lines
25 KiB
C#
using System.Text.Json;
|
|
using TikTokApi.Configuration;
|
|
using TikTokApi.Models;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace TikTokApi.Services;
|
|
|
|
/// <summary>
|
|
/// Core service for TikTok Marketing API operations.
|
|
/// Follows the same dual-mode pattern as GoogleAdsService / MetaMarketingService:
|
|
/// - When EnableRealApi=false: returns emulated responses
|
|
/// - When EnableRealApi=true: makes real Marketing API calls
|
|
///
|
|
/// TikTok Marketing API endpoints:
|
|
/// Campaign: /campaign/create/, /campaign/get/, /campaign/update/, /campaign/status/update/
|
|
/// Ad Group: /adgroup/create/, /adgroup/get/, /adgroup/update/
|
|
/// Report: /report/integrated/get/
|
|
/// BC: /bc/advertiser/create, /bc/advertiser/get, /bc/transfer/
|
|
/// </summary>
|
|
public sealed class TikTokMarketingService
|
|
{
|
|
private readonly TikTokConfig _config;
|
|
private readonly TikTokApiClient _apiClient;
|
|
private readonly ILogger<TikTokMarketingService> _logger;
|
|
|
|
public TikTokMarketingService(
|
|
IOptions<TikTokConfig> config,
|
|
TikTokApiClient apiClient,
|
|
ILogger<TikTokMarketingService> logger)
|
|
{
|
|
_config = config.Value;
|
|
_apiClient = apiClient;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<ProviderResponse> ExecuteAsync(ProviderRequest request, CancellationToken ct)
|
|
{
|
|
var requestId = request.RequestId ?? Guid.NewGuid().ToString("N");
|
|
var operation = (request.Operation ?? string.Empty).Trim();
|
|
|
|
_logger.LogInformation(
|
|
"[TikTokAds] Executing {Operation} | RequestId={RequestId} TenantId={TenantId} RealApi={RealApi}",
|
|
operation, requestId, request.TenantId, _apiClient.IsRealApiEnabled);
|
|
|
|
try
|
|
{
|
|
var context = new TikTokApiContext
|
|
{
|
|
AdvertiserId = request.TenantId ?? string.Empty,
|
|
BusinessCenterId = request.LoginCustomerId ?? _config.BusinessCenterId
|
|
};
|
|
|
|
var result = operation switch
|
|
{
|
|
"Ping" => Ping(requestId),
|
|
"TestPing" => Ping(requestId),
|
|
|
|
// Campaign operations
|
|
"CreateCampaign" => await CreateCampaignAsync(request, context, requestId, ct),
|
|
"GetCampaign" => await GetCampaignAsync(request, context, requestId, ct),
|
|
"UpdateCampaign" => await UpdateCampaignAsync(request, context, requestId, ct),
|
|
"ListCampaigns" => await ListCampaignsAsync(request, context, requestId, ct),
|
|
"UpdateCampaignStatus" => await UpdateCampaignStatusAsync(request, context, requestId, ct),
|
|
|
|
// Reporting
|
|
"GetReport" => await GetReportAsync(request, context, requestId, ct),
|
|
|
|
// Advertiser (ad account) management via Business Center
|
|
"CreateAdvertiser" => await CreateAdvertiserAsync(request, context, requestId, ct),
|
|
"ListAdvertisers" => await ListAdvertisersAsync(context, requestId, ct),
|
|
|
|
// Fund management (BC-specific)
|
|
"TransferFunds" => await TransferFundsAsync(request, context, requestId, ct),
|
|
|
|
"" => ProviderResponse.Fail(requestId, "VALIDATION", "Operation is required"),
|
|
_ => ProviderResponse.Fail(requestId, "UNKNOWN_OPERATION", $"Unknown operation: {operation}")
|
|
};
|
|
|
|
_logger.LogInformation(
|
|
"[TikTokAds] Completed {Operation} | RequestId={RequestId} Ok={Ok}",
|
|
operation, requestId, result.Ok);
|
|
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[TikTokAds] Error in {Operation} | RequestId={RequestId}", operation, requestId);
|
|
return ProviderResponse.Fail(requestId, "INTERNAL_ERROR", ex.Message);
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// Ping
|
|
// ================================================================
|
|
|
|
private ProviderResponse Ping(string requestId)
|
|
=> ProviderResponse.Success(requestId, new
|
|
{
|
|
message = "TikTokApi provider is healthy",
|
|
service = "TikTokApi",
|
|
realApiEnabled = _apiClient.IsRealApiEnabled,
|
|
apiVersion = _config.ApiVersion,
|
|
businessCenterId = _config.BusinessCenterId,
|
|
timestamp = DateTimeOffset.UtcNow
|
|
});
|
|
|
|
// ================================================================
|
|
// Campaign Operations
|
|
// ================================================================
|
|
|
|
private async Task<ProviderResponse> CreateCampaignAsync(
|
|
ProviderRequest request, TikTokApiContext context, string requestId, CancellationToken ct)
|
|
{
|
|
var payload = request.GetPayload<CreateCampaignPayload>();
|
|
|
|
if (string.IsNullOrWhiteSpace(payload.Name))
|
|
return ProviderResponse.Fail(requestId, "VALIDATION", "Campaign name is required");
|
|
|
|
if (_apiClient.IsRealApiEnabled && !string.IsNullOrWhiteSpace(context.AdvertiserId))
|
|
return await CreateCampaignRealAsync(payload, context, requestId, ct);
|
|
|
|
// Emulated
|
|
var emulatedId = GenerateId().ToString();
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
campaignId = emulatedId,
|
|
name = payload.Name,
|
|
objective = MapObjectiveToApi(payload.Objective),
|
|
status = MapStatusToApi(payload.Status),
|
|
budgetMode = MapBudgetModeToApi(payload.BudgetMode),
|
|
budget = payload.Budget,
|
|
advertiserId = context.AdvertiserId,
|
|
emulated = true
|
|
});
|
|
}
|
|
|
|
private async Task<ProviderResponse> CreateCampaignRealAsync(
|
|
CreateCampaignPayload payload, TikTokApiContext context, string requestId, CancellationToken ct)
|
|
{
|
|
// POST /campaign/create/
|
|
var body = new Dictionary<string, object?>
|
|
{
|
|
["advertiser_id"] = context.AdvertiserId,
|
|
["campaign_name"] = payload.Name,
|
|
["objective_type"] = MapObjectiveToApi(payload.Objective),
|
|
["budget_mode"] = MapBudgetModeToApi(payload.BudgetMode),
|
|
["operation_status"] = MapStatusToApi(payload.Status)
|
|
};
|
|
|
|
if (payload.Budget.HasValue)
|
|
body["budget"] = payload.Budget.Value;
|
|
|
|
if (payload.SpecialIndustries.Count > 0)
|
|
body["special_industries"] = payload.SpecialIndustries;
|
|
|
|
var result = await _apiClient.PostAsync("campaign/create/", body, ct);
|
|
|
|
if (!result.IsSuccess)
|
|
return ProviderResponse.Fail(requestId, "TIKTOK_API_ERROR",
|
|
result.Message ?? "Failed to create campaign",
|
|
new { tikTokCode = result.Code, tikTokRequestId = result.TikTokRequestId });
|
|
|
|
var campaignId = result.Data?.TryGetProperty("campaign_id", out var idProp) == true
|
|
? idProp.GetString() : null;
|
|
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
campaignId,
|
|
name = payload.Name,
|
|
objective = MapObjectiveToApi(payload.Objective),
|
|
advertiserId = context.AdvertiserId,
|
|
emulated = false
|
|
});
|
|
}
|
|
|
|
private async Task<ProviderResponse> GetCampaignAsync(
|
|
ProviderRequest request, TikTokApiContext context, string requestId, CancellationToken ct)
|
|
{
|
|
var payload = request.GetPayload<GetCampaignPayload>();
|
|
|
|
if (string.IsNullOrWhiteSpace(payload.CampaignId))
|
|
return ProviderResponse.Fail(requestId, "VALIDATION", "CampaignId is required");
|
|
|
|
if (_apiClient.IsRealApiEnabled && !string.IsNullOrWhiteSpace(context.AdvertiserId))
|
|
{
|
|
// GET /campaign/get/ with filtering
|
|
var queryParams = new Dictionary<string, string>
|
|
{
|
|
["advertiser_id"] = context.AdvertiserId,
|
|
["filtering"] = JsonSerializer.Serialize(new
|
|
{
|
|
campaign_ids = new[] { payload.CampaignId }
|
|
})
|
|
};
|
|
|
|
var result = await _apiClient.GetAsync("campaign/get/", queryParams, ct);
|
|
|
|
if (!result.IsSuccess)
|
|
return ProviderResponse.Fail(requestId, "TIKTOK_API_ERROR",
|
|
result.Message ?? "Failed to get campaign");
|
|
|
|
return ProviderResponse.Success(requestId, new { raw = result.Data, emulated = false });
|
|
}
|
|
|
|
// Emulated
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
campaignId = payload.CampaignId,
|
|
name = $"Emulated Campaign {payload.CampaignId}",
|
|
objectiveType = "TRAFFIC",
|
|
operationStatus = "DISABLE",
|
|
budgetMode = "BUDGET_MODE_DAY",
|
|
budget = 50.00m,
|
|
createTime = DateTimeOffset.UtcNow.AddDays(-7).ToString("o"),
|
|
modifyTime = DateTimeOffset.UtcNow.ToString("o"),
|
|
emulated = true
|
|
});
|
|
}
|
|
|
|
private async Task<ProviderResponse> UpdateCampaignAsync(
|
|
ProviderRequest request, TikTokApiContext context, string requestId, CancellationToken ct)
|
|
{
|
|
var payload = request.GetPayload<UpdateCampaignPayload>();
|
|
|
|
if (string.IsNullOrWhiteSpace(payload.CampaignId))
|
|
return ProviderResponse.Fail(requestId, "VALIDATION", "CampaignId is required");
|
|
|
|
if (_apiClient.IsRealApiEnabled && !string.IsNullOrWhiteSpace(context.AdvertiserId))
|
|
{
|
|
// POST /campaign/update/
|
|
var body = new Dictionary<string, object?>
|
|
{
|
|
["advertiser_id"] = context.AdvertiserId,
|
|
["campaign_id"] = payload.CampaignId
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(payload.Name))
|
|
body["campaign_name"] = payload.Name;
|
|
|
|
if (payload.Budget.HasValue)
|
|
body["budget"] = payload.Budget.Value;
|
|
|
|
var result = await _apiClient.PostAsync("campaign/update/", body, ct);
|
|
|
|
if (!result.IsSuccess)
|
|
return ProviderResponse.Fail(requestId, "TIKTOK_API_ERROR",
|
|
result.Message ?? "Failed to update campaign");
|
|
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
campaignId = payload.CampaignId,
|
|
updated = true,
|
|
emulated = false
|
|
});
|
|
}
|
|
|
|
// Emulated
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
campaignId = payload.CampaignId,
|
|
updated = true,
|
|
name = payload.Name,
|
|
emulated = true
|
|
});
|
|
}
|
|
|
|
private async Task<ProviderResponse> UpdateCampaignStatusAsync(
|
|
ProviderRequest request, TikTokApiContext context, string requestId, CancellationToken ct)
|
|
{
|
|
var payload = request.GetPayload<UpdateCampaignPayload>();
|
|
|
|
if (string.IsNullOrWhiteSpace(payload.CampaignId))
|
|
return ProviderResponse.Fail(requestId, "VALIDATION", "CampaignId is required");
|
|
|
|
if (!payload.Status.HasValue)
|
|
return ProviderResponse.Fail(requestId, "VALIDATION", "Status is required");
|
|
|
|
if (_apiClient.IsRealApiEnabled && !string.IsNullOrWhiteSpace(context.AdvertiserId))
|
|
{
|
|
// POST /campaign/status/update/ (separate endpoint from /campaign/update/)
|
|
var body = new Dictionary<string, object?>
|
|
{
|
|
["advertiser_id"] = context.AdvertiserId,
|
|
["campaign_ids"] = new[] { payload.CampaignId },
|
|
["operation_status"] = MapStatusToApi(payload.Status.Value)
|
|
};
|
|
|
|
var result = await _apiClient.PostAsync("campaign/status/update/", body, ct);
|
|
|
|
if (!result.IsSuccess)
|
|
return ProviderResponse.Fail(requestId, "TIKTOK_API_ERROR",
|
|
result.Message ?? "Failed to update campaign status");
|
|
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
campaignId = payload.CampaignId,
|
|
status = MapStatusToApi(payload.Status.Value),
|
|
emulated = false
|
|
});
|
|
}
|
|
|
|
// Emulated
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
campaignId = payload.CampaignId,
|
|
status = MapStatusToApi(payload.Status.Value),
|
|
emulated = true
|
|
});
|
|
}
|
|
|
|
private async Task<ProviderResponse> ListCampaignsAsync(
|
|
ProviderRequest request, TikTokApiContext context, string requestId, CancellationToken ct)
|
|
{
|
|
var payload = request.GetPayload<ListCampaignsPayload>();
|
|
|
|
if (_apiClient.IsRealApiEnabled && !string.IsNullOrWhiteSpace(context.AdvertiserId))
|
|
{
|
|
var queryParams = new Dictionary<string, string>
|
|
{
|
|
["advertiser_id"] = context.AdvertiserId,
|
|
["page_size"] = payload.PageSize.ToString(),
|
|
["page"] = payload.Page.ToString()
|
|
};
|
|
|
|
if (payload.StatusFilter.HasValue)
|
|
{
|
|
queryParams["filtering"] = JsonSerializer.Serialize(new
|
|
{
|
|
operation_status = MapStatusToApi(payload.StatusFilter.Value)
|
|
});
|
|
}
|
|
|
|
var result = await _apiClient.GetAsync("campaign/get/", queryParams, ct);
|
|
|
|
if (!result.IsSuccess)
|
|
return ProviderResponse.Fail(requestId, "TIKTOK_API_ERROR",
|
|
result.Message ?? "Failed to list campaigns");
|
|
|
|
return ProviderResponse.Success(requestId, new { raw = result.Data, emulated = false });
|
|
}
|
|
|
|
// Emulated
|
|
var campaigns = Enumerable.Range(1, 3).Select(i => new
|
|
{
|
|
campaign_id = GenerateId().ToString(),
|
|
campaign_name = $"Emulated Campaign {i}",
|
|
objective_type = "TRAFFIC",
|
|
operation_status = i == 1 ? "ENABLE" : "DISABLE",
|
|
budget_mode = "BUDGET_MODE_DAY",
|
|
budget = 50.00m * i,
|
|
create_time = DateTimeOffset.UtcNow.AddDays(-i * 7).ToString("o")
|
|
});
|
|
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
campaigns,
|
|
advertiserId = context.AdvertiserId,
|
|
pageInfo = new { page = 1, pageSize = 50, totalNumber = 3, totalPage = 1 },
|
|
emulated = true
|
|
});
|
|
}
|
|
|
|
// ================================================================
|
|
// Reporting
|
|
// ================================================================
|
|
|
|
private async Task<ProviderResponse> GetReportAsync(
|
|
ProviderRequest request, TikTokApiContext context, string requestId, CancellationToken ct)
|
|
{
|
|
var payload = request.GetPayload<ReportPayload>();
|
|
|
|
if (_apiClient.IsRealApiEnabled && !string.IsNullOrWhiteSpace(context.AdvertiserId))
|
|
{
|
|
// POST /report/integrated/get/
|
|
var body = new Dictionary<string, object?>
|
|
{
|
|
["advertiser_id"] = context.AdvertiserId,
|
|
["report_type"] = payload.ReportType,
|
|
["data_level"] = payload.DataLevel,
|
|
["dimensions"] = payload.Dimensions,
|
|
["metrics"] = payload.Metrics,
|
|
["page_size"] = payload.PageSize,
|
|
["page"] = payload.Page,
|
|
["lifetime"] = payload.Lifetime
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(payload.StartDate))
|
|
body["start_date"] = payload.StartDate;
|
|
if (!string.IsNullOrWhiteSpace(payload.EndDate))
|
|
body["end_date"] = payload.EndDate;
|
|
if (payload.Filters?.Count > 0)
|
|
body["filters"] = payload.Filters;
|
|
|
|
var result = await _apiClient.PostAsync("report/integrated/get/", body, ct);
|
|
|
|
if (!result.IsSuccess)
|
|
return ProviderResponse.Fail(requestId, "TIKTOK_API_ERROR",
|
|
result.Message ?? "Failed to get report");
|
|
|
|
return ProviderResponse.Success(requestId, new { raw = result.Data, emulated = false });
|
|
}
|
|
|
|
// Emulated report data
|
|
var rng = new Random();
|
|
var rows = Enumerable.Range(0, 7).Select(i =>
|
|
{
|
|
var date = DateTime.UtcNow.Date.AddDays(-i);
|
|
var impressions = rng.Next(2000, 80000);
|
|
var clicks = rng.Next(100, impressions / 8);
|
|
var spend = Math.Round(clicks * (rng.NextDouble() * 1.5 + 0.3), 2);
|
|
return new
|
|
{
|
|
dimensions = new { stat_time_day = date.ToString("yyyy-MM-dd"), campaign_id = GenerateId().ToString() },
|
|
metrics = new
|
|
{
|
|
spend = spend.ToString("F2"),
|
|
impressions = impressions.ToString(),
|
|
clicks = clicks.ToString(),
|
|
cpc = (spend / clicks).ToString("F2"),
|
|
ctr = (clicks * 100.0 / impressions).ToString("F2"),
|
|
cpm = (spend / impressions * 1000).ToString("F2")
|
|
}
|
|
};
|
|
}).Reverse();
|
|
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
list = rows,
|
|
pageInfo = new { page = 1, pageSize = 50, totalNumber = 7, totalPage = 1 },
|
|
emulated = true
|
|
});
|
|
}
|
|
|
|
// ================================================================
|
|
// Advertiser (Ad Account) Management via Business Center
|
|
// ================================================================
|
|
|
|
private async Task<ProviderResponse> CreateAdvertiserAsync(
|
|
ProviderRequest request, TikTokApiContext context, string requestId, CancellationToken ct)
|
|
{
|
|
var payload = request.GetPayload<CreateAdvertiserPayload>();
|
|
|
|
if (string.IsNullOrWhiteSpace(payload.Name))
|
|
return ProviderResponse.Fail(requestId, "VALIDATION", "Advertiser name is required");
|
|
|
|
if (string.IsNullOrWhiteSpace(context.BusinessCenterId))
|
|
return ProviderResponse.Fail(requestId, "VALIDATION", "BusinessCenterId is required");
|
|
|
|
if (_apiClient.IsRealApiEnabled)
|
|
{
|
|
// POST /bc/advertiser/create
|
|
var body = new Dictionary<string, object?>
|
|
{
|
|
["bc_id"] = context.BusinessCenterId,
|
|
["advertiser_name"] = payload.Name,
|
|
["currency"] = payload.Currency,
|
|
["timezone"] = payload.Timezone,
|
|
["company"] = payload.Company
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(payload.IndustryId))
|
|
body["industry_id"] = payload.IndustryId;
|
|
if (!string.IsNullOrWhiteSpace(payload.ContactEmail))
|
|
body["contact_email"] = payload.ContactEmail;
|
|
if (!string.IsNullOrWhiteSpace(payload.ContactPhone))
|
|
body["contact_phone"] = payload.ContactPhone;
|
|
|
|
var result = await _apiClient.PostAsync("bc/advertiser/create", body, ct);
|
|
|
|
if (!result.IsSuccess)
|
|
return ProviderResponse.Fail(requestId, "TIKTOK_API_ERROR",
|
|
result.Message ?? "Failed to create advertiser",
|
|
new { tikTokCode = result.Code, tikTokRequestId = result.TikTokRequestId });
|
|
|
|
var advertiserId = result.Data?.TryGetProperty("advertiser_id", out var idProp) == true
|
|
? idProp.GetString() : null;
|
|
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
advertiserId,
|
|
name = payload.Name,
|
|
currency = payload.Currency,
|
|
businessCenterId = context.BusinessCenterId,
|
|
emulated = false
|
|
});
|
|
}
|
|
|
|
// Emulated
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
advertiserId = GenerateId().ToString(),
|
|
name = payload.Name,
|
|
currency = payload.Currency,
|
|
timezone = payload.Timezone,
|
|
businessCenterId = context.BusinessCenterId,
|
|
status = "STATUS_ENABLE",
|
|
emulated = true
|
|
});
|
|
}
|
|
|
|
private async Task<ProviderResponse> ListAdvertisersAsync(
|
|
TikTokApiContext context, string requestId, CancellationToken ct)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(context.BusinessCenterId))
|
|
return ProviderResponse.Fail(requestId, "VALIDATION", "BusinessCenterId is required");
|
|
|
|
if (_apiClient.IsRealApiEnabled)
|
|
{
|
|
// GET /bc/advertiser/get
|
|
var queryParams = new Dictionary<string, string>
|
|
{
|
|
["bc_id"] = context.BusinessCenterId,
|
|
["page_size"] = "100",
|
|
["page"] = "1"
|
|
};
|
|
|
|
var result = await _apiClient.GetAsync("bc/advertiser/get", queryParams, ct);
|
|
|
|
if (!result.IsSuccess)
|
|
return ProviderResponse.Fail(requestId, "TIKTOK_API_ERROR",
|
|
result.Message ?? "Failed to list advertisers");
|
|
|
|
return ProviderResponse.Success(requestId, new { raw = result.Data, emulated = false });
|
|
}
|
|
|
|
// Emulated
|
|
var advertisers = Enumerable.Range(1, 3).Select(i => new
|
|
{
|
|
advertiser_id = GenerateId().ToString(),
|
|
advertiser_name = $"Client Account {i}",
|
|
status = "STATUS_ENABLE",
|
|
currency = "USD",
|
|
timezone = "America/Los_Angeles",
|
|
balance = (i * 500.00m).ToString("F2")
|
|
});
|
|
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
advertisers,
|
|
businessCenterId = context.BusinessCenterId,
|
|
emulated = true
|
|
});
|
|
}
|
|
|
|
// ================================================================
|
|
// Fund Management (Business Center)
|
|
// ================================================================
|
|
|
|
private async Task<ProviderResponse> TransferFundsAsync(
|
|
ProviderRequest request, TikTokApiContext context, string requestId, CancellationToken ct)
|
|
{
|
|
var payload = request.GetPayload<TransferFundsPayload>();
|
|
|
|
if (string.IsNullOrWhiteSpace(payload.AdvertiserId))
|
|
return ProviderResponse.Fail(requestId, "VALIDATION", "AdvertiserId is required");
|
|
|
|
if (string.IsNullOrWhiteSpace(context.BusinessCenterId))
|
|
return ProviderResponse.Fail(requestId, "VALIDATION", "BusinessCenterId is required");
|
|
|
|
if (payload.Amount <= 0)
|
|
return ProviderResponse.Fail(requestId, "VALIDATION", "Amount must be greater than zero");
|
|
|
|
if (_apiClient.IsRealApiEnabled)
|
|
{
|
|
// POST /bc/transfer/
|
|
var body = new Dictionary<string, object?>
|
|
{
|
|
["bc_id"] = context.BusinessCenterId,
|
|
["advertiser_id"] = payload.AdvertiserId,
|
|
["transfer_type"] = payload.TransferType,
|
|
["cash_amount"] = payload.Amount
|
|
};
|
|
|
|
var result = await _apiClient.PostAsync("bc/transfer/", body, ct);
|
|
|
|
if (!result.IsSuccess)
|
|
return ProviderResponse.Fail(requestId, "TIKTOK_API_ERROR",
|
|
result.Message ?? "Failed to transfer funds");
|
|
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
advertiserId = payload.AdvertiserId,
|
|
transferType = payload.TransferType,
|
|
amount = payload.Amount,
|
|
emulated = false
|
|
});
|
|
}
|
|
|
|
// Emulated
|
|
return ProviderResponse.Success(requestId, new
|
|
{
|
|
advertiserId = payload.AdvertiserId,
|
|
transferType = payload.TransferType,
|
|
amount = payload.Amount,
|
|
balanceAfter = payload.TransferType == "RECHARGE" ? 1500.00m : 500.00m,
|
|
emulated = true
|
|
});
|
|
}
|
|
|
|
// ================================================================
|
|
// Helpers
|
|
// ================================================================
|
|
|
|
/// <summary>
|
|
/// Map platform objective enum to TikTok API string.
|
|
/// </summary>
|
|
private static string MapObjectiveToApi(TikTokObjective objective) => objective switch
|
|
{
|
|
TikTokObjective.Reach => "REACH",
|
|
TikTokObjective.Traffic => "TRAFFIC",
|
|
TikTokObjective.VideoViews => "VIDEO_VIEWS",
|
|
TikTokObjective.LeadGeneration => "LEAD_GENERATION",
|
|
TikTokObjective.CommunityInteraction => "COMMUNITY_INTERACTION",
|
|
TikTokObjective.AppPromotion => "APP_PROMOTION",
|
|
TikTokObjective.WebConversions => "WEB_CONVERSIONS",
|
|
TikTokObjective.ProductSales => "PRODUCT_SALES",
|
|
_ => "TRAFFIC"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Map platform status enum to TikTok API string.
|
|
/// TikTok uses ENABLE/DISABLE, not ACTIVE/PAUSED.
|
|
/// </summary>
|
|
private static string MapStatusToApi(TikTokCampaignStatus status) => status switch
|
|
{
|
|
TikTokCampaignStatus.Enable => "ENABLE",
|
|
TikTokCampaignStatus.Disable => "DISABLE",
|
|
TikTokCampaignStatus.Delete => "DELETE",
|
|
_ => "DISABLE"
|
|
};
|
|
|
|
private static string MapBudgetModeToApi(TikTokBudgetMode mode) => mode switch
|
|
{
|
|
TikTokBudgetMode.Day => "BUDGET_MODE_DAY",
|
|
TikTokBudgetMode.Total => "BUDGET_MODE_TOTAL",
|
|
TikTokBudgetMode.Infinite => "BUDGET_MODE_INFINITE",
|
|
_ => "BUDGET_MODE_DAY"
|
|
};
|
|
|
|
private static long GenerateId() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1000 + Random.Shared.Next(999);
|
|
}
|