Initial import into Gitea
This commit is contained in:
502
Gateway/Controllers/InitiativeController.cs
Normal file
502
Gateway/Controllers/InitiativeController.cs
Normal file
@@ -0,0 +1,502 @@
|
||||
using Gateway.Data;
|
||||
using Gateway.Models;
|
||||
using Gateway.Security;
|
||||
using Gateway.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Gateway.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Multi-channel initiative endpoints.
|
||||
///
|
||||
/// SECURITY MODEL:
|
||||
/// - Every endpoint requires authenticated session (via middleware)
|
||||
/// - Every resource-specific endpoint validates ownership (initiative → client)
|
||||
/// - Status changes are restricted to valid client-initiated transitions
|
||||
/// - Sync endpoint is restricted to admin role
|
||||
/// - Budget values are validated server-side against channel minimums
|
||||
/// - ClientId is injected server-side, never trusted from request body
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/initiative")]
|
||||
public sealed class InitiativeController : ControllerBase
|
||||
{
|
||||
private readonly SqlService _sql;
|
||||
private readonly ClientContext _client;
|
||||
private readonly AuthorizationGuard _guard;
|
||||
private readonly MultiChannelConfig _config;
|
||||
private readonly InitiativeLaunchService _launch;
|
||||
private readonly ProviderStatusNormalizer _statusNorm;
|
||||
private readonly ILogger<InitiativeController> _log;
|
||||
|
||||
public InitiativeController(
|
||||
SqlService sql,
|
||||
ClientContext client,
|
||||
AuthorizationGuard guard,
|
||||
IOptions<MultiChannelConfig> config,
|
||||
InitiativeLaunchService launch,
|
||||
ProviderStatusNormalizer statusNorm,
|
||||
ILogger<InitiativeController> log)
|
||||
{
|
||||
_sql = sql;
|
||||
_client = client;
|
||||
_guard = guard;
|
||||
_config = config.Value;
|
||||
_launch = launch;
|
||||
_statusNorm = statusNorm;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Initiative CRUD
|
||||
// ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Create a new initiative with channel allocations.</summary>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateInitiativeRequest request, CancellationToken ct)
|
||||
{
|
||||
var (ok, err) = _guard.RequireAuth();
|
||||
if (!ok) return Unauthorized(new { ok = false, error = err });
|
||||
|
||||
if (request.TotalBudget.HasValue)
|
||||
{
|
||||
var (budgetOk, budgetErr) = _guard.ValidateBudget(
|
||||
request.TotalBudget.Value, request.BudgetPeriod, _config);
|
||||
if (!budgetOk) return BadRequest(new { ok = false, error = budgetErr });
|
||||
}
|
||||
|
||||
var rqst = JsonSerializer.Serialize(new
|
||||
{
|
||||
clientId = _client.ClientId,
|
||||
userId = _client.UserId,
|
||||
name = request.Name,
|
||||
objective = request.Objective,
|
||||
totalBudget = request.TotalBudget,
|
||||
budgetPeriod = request.BudgetPeriod ?? "monthly",
|
||||
startDate = request.StartDate,
|
||||
endDate = request.EndDate,
|
||||
allocationStrategy = request.AllocationStrategy ?? "manual",
|
||||
businessCategory = request.BusinessCategory,
|
||||
wizardId = request.WizardId,
|
||||
channels = request.Channels
|
||||
});
|
||||
|
||||
return await Exec(SqlNames.Procs.Initiative, "create", rqst, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stage an initiative for confirmation with server-calculated billing.
|
||||
/// </summary>
|
||||
[HttpPost("stage")]
|
||||
public async Task<IActionResult> Stage([FromBody] CreateInitiativeRequest request, CancellationToken ct)
|
||||
{
|
||||
var (ok, err) = _guard.RequireAuth();
|
||||
if (!ok) return Unauthorized(new { ok = false, error = err });
|
||||
|
||||
if (request.InitiativeId.HasValue && request.InitiativeId > 0)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(request.InitiativeId.Value, ct);
|
||||
if (!ownership.IsAllowed)
|
||||
return NotFound(new { ok = false, error = ownership.Error });
|
||||
}
|
||||
|
||||
if (request.TotalBudget.HasValue)
|
||||
{
|
||||
var (budgetOk, budgetErr) = _guard.ValidateBudget(
|
||||
request.TotalBudget.Value, request.BudgetPeriod, _config);
|
||||
if (!budgetOk) return BadRequest(new { ok = false, error = budgetErr });
|
||||
}
|
||||
|
||||
var rqst = JsonSerializer.Serialize(new
|
||||
{
|
||||
clientId = _client.ClientId,
|
||||
userId = _client.UserId,
|
||||
name = request.Name,
|
||||
objective = request.Objective,
|
||||
totalBudget = request.TotalBudget,
|
||||
budgetPeriod = request.BudgetPeriod ?? "monthly",
|
||||
startDate = request.StartDate,
|
||||
endDate = request.EndDate,
|
||||
allocationStrategy = request.AllocationStrategy ?? "manual",
|
||||
businessCategory = request.BusinessCategory,
|
||||
wizardId = request.WizardId,
|
||||
initiativeId = request.InitiativeId,
|
||||
channels = request.Channels
|
||||
});
|
||||
|
||||
return await Exec(SqlNames.Procs.InitiativeStage, "stage", rqst, ct);
|
||||
}
|
||||
|
||||
/// <summary>Get billing for a staged initiative.</summary>
|
||||
[HttpGet("{initiativeId:long}/billing")]
|
||||
public async Task<IActionResult> GetBilling(long initiativeId, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed)
|
||||
return NotFound(new { ok = false, error = ownership.Error });
|
||||
|
||||
return await Exec(SqlNames.Procs.InitiativeStage, "getBilling",
|
||||
JsonSerializer.Serialize(new { initiativeId }), ct);
|
||||
}
|
||||
|
||||
/// <summary>Get initiative by ID (ownership verified).</summary>
|
||||
[HttpGet("{initiativeId:long}")]
|
||||
public async Task<IActionResult> Get(long initiativeId, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed)
|
||||
return NotFound(new { ok = false, error = ownership.Error });
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ownership.EntityJson))
|
||||
return Content(ownership.EntityJson, "application/json");
|
||||
|
||||
return await Exec(SqlNames.Procs.Initiative, "get",
|
||||
JsonSerializer.Serialize(new { initiativeId }), ct);
|
||||
}
|
||||
|
||||
/// <summary>List initiatives for current client (always scoped).</summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery] string? status, [FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken ct)
|
||||
{
|
||||
var (ok, err) = _guard.RequireAuth();
|
||||
if (!ok) return Unauthorized(new { ok = false, error = err });
|
||||
|
||||
return await Exec(SqlNames.Procs.Initiative, "list",
|
||||
JsonSerializer.Serialize(new { clientId = _client.ClientId, status, page, pageSize }), ct);
|
||||
}
|
||||
|
||||
/// <summary>Update initiative metadata (ownership verified, status stripped).</summary>
|
||||
[HttpPut("{initiativeId:long}")]
|
||||
public async Task<IActionResult> Update(long initiativeId, [FromBody] UpdateInitiativeRequest request, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed)
|
||||
return NotFound(new { ok = false, error = ownership.Error });
|
||||
|
||||
if (request.TotalBudget.HasValue)
|
||||
{
|
||||
var (budgetOk, budgetErr) = _guard.ValidateBudget(request.TotalBudget.Value, null, _config);
|
||||
if (!budgetOk) return BadRequest(new { ok = false, error = budgetErr });
|
||||
}
|
||||
|
||||
return await Exec(SqlNames.Procs.Initiative, "update",
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
initiativeId,
|
||||
clientId = _client.ClientId,
|
||||
name = request.Name,
|
||||
totalBudget = request.TotalBudget,
|
||||
startDate = request.StartDate,
|
||||
endDate = request.EndDate,
|
||||
businessCategory = request.BusinessCategory
|
||||
}), ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update status with transition enforcement.
|
||||
/// Clients: active↔paused, *→cancelled only. Admins: any transition.
|
||||
/// </summary>
|
||||
[HttpPatch("{initiativeId:long}/status")]
|
||||
public async Task<IActionResult> UpdateStatus(long initiativeId, [FromBody] UpdateInitiativeStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request?.Status))
|
||||
return BadRequest(new { ok = false, error = "status is required" });
|
||||
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed)
|
||||
return NotFound(new { ok = false, error = ownership.Error });
|
||||
|
||||
var isAdmin = string.Equals(_client.Role, "admin", StringComparison.OrdinalIgnoreCase);
|
||||
if (!isAdmin)
|
||||
{
|
||||
var (transOk, transErr) = _guard.ValidateClientStatusTransition(
|
||||
ownership.CurrentStatus, request.Status, "initiative");
|
||||
if (!transOk) return BadRequest(new { ok = false, error = transErr });
|
||||
}
|
||||
|
||||
return await Exec(SqlNames.Procs.Initiative, "updateStatus",
|
||||
JsonSerializer.Serialize(new { initiativeId, clientId = _client.ClientId, status = request.Status }), ct);
|
||||
}
|
||||
|
||||
/// <summary>Soft-delete (cannot delete active — cancel first).</summary>
|
||||
[HttpDelete("{initiativeId:long}")]
|
||||
public async Task<IActionResult> Delete(long initiativeId, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed)
|
||||
return NotFound(new { ok = false, error = ownership.Error });
|
||||
|
||||
if (ownership.CurrentStatus == "active")
|
||||
return BadRequest(new { ok = false, error = "Cannot delete an active initiative. Cancel it first." });
|
||||
|
||||
return await Exec(SqlNames.Procs.Initiative, "delete",
|
||||
JsonSerializer.Serialize(new { initiativeId, clientId = _client.ClientId }), ct);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Launch / Dispatch
|
||||
// ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Launch a staged initiative (ownership + status verified).</summary>
|
||||
[HttpPost("{initiativeId:long}/launch")]
|
||||
public async Task<IActionResult> Launch(long initiativeId, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed)
|
||||
return NotFound(new { ok = false, error = ownership.Error });
|
||||
|
||||
if (ownership.CurrentStatus != "staged")
|
||||
return BadRequest(new { ok = false, error = $"Initiative must be staged before launching (current: {ownership.CurrentStatus})" });
|
||||
|
||||
_log.LogInformation("[Initiative] Launch {InitiativeId} by {UserId}", initiativeId, _client.UserId);
|
||||
|
||||
var result = await _launch.LaunchAsync(initiativeId, _client.ClientId ?? "", _client.UserId, ct);
|
||||
|
||||
if (!result.Ok && result.Error != null)
|
||||
{
|
||||
_log.LogWarning("[Initiative] Launch failed {InitiativeId}: {Error}", initiativeId, result.Error);
|
||||
return BadRequest(result);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Channel Campaigns (all ownership-verified)
|
||||
// ────────────────────────────────────────────────
|
||||
|
||||
[HttpGet("{initiativeId:long}/channels")]
|
||||
public async Task<IActionResult> ListChannels(long initiativeId, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed) return NotFound(new { ok = false, error = ownership.Error });
|
||||
return await Exec(SqlNames.Procs.ChannelCampaign, "list", JsonSerializer.Serialize(new { initiativeId }), ct);
|
||||
}
|
||||
|
||||
[HttpGet("channel/{channelCampaignId:long}")]
|
||||
public async Task<IActionResult> GetChannel(long channelCampaignId, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyChannelOwnerAsync(channelCampaignId, ct);
|
||||
if (!ownership.IsAllowed) return NotFound(new { ok = false, error = ownership.Error });
|
||||
return await Exec(SqlNames.Procs.ChannelCampaign, "get", JsonSerializer.Serialize(new { channelCampaignId }), ct);
|
||||
}
|
||||
|
||||
/// <summary>Sync channel status — called by provider containers.</summary>
|
||||
[HttpPatch("channel/{channelCampaignId:long}/sync")]
|
||||
public async Task<IActionResult> SyncChannel(long channelCampaignId, [FromBody] SyncChannelRequest request, CancellationToken ct)
|
||||
{
|
||||
var (ok, err) = _guard.RequireServiceKey();
|
||||
if (!ok) return StatusCode(403, new { ok = false, error = err });
|
||||
|
||||
var normalizedStatus = _statusNorm.Resolve(request.ChannelType, request.Status, request.ProviderStatus);
|
||||
|
||||
_log.LogInformation("[Sync] Channel {Id} | {Provider} → {Status} | By={User}",
|
||||
channelCampaignId, request.ProviderStatus, normalizedStatus, _client.UserId);
|
||||
|
||||
return await Exec(SqlNames.Procs.ChannelCampaign, "sync",
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
channelCampaignId,
|
||||
externalCampaignId = request.ExternalCampaignId,
|
||||
externalAccountId = request.ExternalAccountId,
|
||||
status = normalizedStatus,
|
||||
providerStatus = request.ProviderStatus
|
||||
}), ct);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Budget Allocation (all ownership-verified)
|
||||
// ────────────────────────────────────────────────
|
||||
|
||||
[HttpGet("{initiativeId:long}/allocation")]
|
||||
public async Task<IActionResult> GetAllocation(long initiativeId, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed) return NotFound(new { ok = false, error = ownership.Error });
|
||||
return await Exec(SqlNames.Procs.Allocation, "get", JsonSerializer.Serialize(new { initiativeId }), ct);
|
||||
}
|
||||
|
||||
[HttpPut("{initiativeId:long}/allocation")]
|
||||
public async Task<IActionResult> UpdateAllocation(long initiativeId, [FromBody] UpdateAllocationRequest request, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed) return NotFound(new { ok = false, error = ownership.Error });
|
||||
return await Exec(SqlNames.Procs.Allocation, "update",
|
||||
JsonSerializer.Serialize(new { initiativeId, userId = _client.UserId, allocations = request.Allocations, reason = request.Reason }), ct);
|
||||
}
|
||||
|
||||
[HttpGet("{initiativeId:long}/allocation/recommend")]
|
||||
public async Task<IActionResult> GetRecommendation(long initiativeId, [FromQuery] string? businessCategory, [FromQuery] string? objective, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed) return NotFound(new { ok = false, error = ownership.Error });
|
||||
return await Exec(SqlNames.Procs.Allocation, "recommend", JsonSerializer.Serialize(new { initiativeId, businessCategory, objective }), ct);
|
||||
}
|
||||
|
||||
[HttpPost("{initiativeId:long}/allocation/apply")]
|
||||
public async Task<IActionResult> ApplyAllocation(long initiativeId, [FromBody] ApplyAllocationRequest request, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed) return NotFound(new { ok = false, error = ownership.Error });
|
||||
return await Exec(SqlNames.Procs.Allocation, "apply",
|
||||
JsonSerializer.Serialize(new { initiativeId, source = request.Source, allocations = request.Allocations, reason = request.Reason }), ct);
|
||||
}
|
||||
|
||||
[HttpGet("{initiativeId:long}/allocation/history")]
|
||||
public async Task<IActionResult> GetAllocationHistory(long initiativeId, [FromQuery] int? limit, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed) return NotFound(new { ok = false, error = ownership.Error });
|
||||
return await Exec(SqlNames.Procs.Allocation, "history", JsonSerializer.Serialize(new { initiativeId, limit }), ct);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Channel Config (read-only)
|
||||
// ────────────────────────────────────────────────
|
||||
|
||||
[HttpGet("channels/available")]
|
||||
public async Task<IActionResult> GetAvailableChannels(CancellationToken ct)
|
||||
{
|
||||
var (ok, err) = _guard.RequireAuth();
|
||||
if (!ok) return Unauthorized(new { ok = false, error = err });
|
||||
|
||||
var mappingsResp = await _sql.ExecProcAsync(SqlNames.Procs.ObjectiveMapping, "list", "{}", ct: ct);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
ok = true,
|
||||
channels = _config.EnabledChannels.Select(c => new
|
||||
{
|
||||
c.ChannelType, c.DisplayName, c.Description, c.Icon, c.Color,
|
||||
c.MinDailyBudget, c.MinMonthlyBudget, c.SupportedObjectives,
|
||||
c.SupportedCreativeFormats, c.ApprovalEstimateHours, c.IsStub
|
||||
}),
|
||||
allocation = new
|
||||
{
|
||||
_config.Allocation.MinMultiChannelMonthlyBudget,
|
||||
_config.Allocation.MaxChannelsPerInitiative,
|
||||
_config.Allocation.DefaultAllocationStrategy,
|
||||
_config.Allocation.MinChannelAllocationPct,
|
||||
_config.Allocation.MaxChannelAllocationPct
|
||||
},
|
||||
objectiveMappings = JsonSerializer.Deserialize<object>(mappingsResp ?? "{}")
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Status mappings — available to authenticated clients.</summary>
|
||||
[HttpGet("channels/status-mappings")]
|
||||
public IActionResult GetStatusMappings([FromQuery] string? channelType)
|
||||
{
|
||||
var (ok, err) = _guard.RequireAuth();
|
||||
if (!ok) return Unauthorized(new { ok = false, error = err });
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(channelType))
|
||||
return Ok(new { ok = true, channelType, mappings = _statusNorm.GetMappings(channelType) });
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
ok = true,
|
||||
channels = _config.Channels.Values.Select(c => new { c.ChannelType, c.DisplayName, c.Enabled, mappings = _statusNorm.GetMappings(c.ChannelType) })
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("templates")]
|
||||
public async Task<IActionResult> GetTemplates([FromQuery] string? businessCategory, [FromQuery] string? objective, CancellationToken ct)
|
||||
{
|
||||
var (ok, err) = _guard.RequireAuth();
|
||||
if (!ok) return Unauthorized(new { ok = false, error = err });
|
||||
return await Exec(SqlNames.Procs.Allocation, "getTemplates", JsonSerializer.Serialize(new { businessCategory, objective }), ct);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Performance Metrics (ownership-verified)
|
||||
// ────────────────────────────────────────────────
|
||||
|
||||
[HttpGet("{initiativeId:long}/metrics")]
|
||||
public async Task<IActionResult> MetricsSummary(long initiativeId, [FromQuery] string? fromDate, [FromQuery] string? toDate, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed) return NotFound(new { ok = false, error = ownership.Error });
|
||||
return await Exec(SqlNames.Procs.PerformanceMetric, "summary", JsonSerializer.Serialize(new { initiativeId, fromDate, toDate }), ct);
|
||||
}
|
||||
|
||||
[HttpGet("{initiativeId:long}/metrics/compare")]
|
||||
public async Task<IActionResult> MetricsCompare(long initiativeId, [FromQuery] int? lookbackDays, CancellationToken ct)
|
||||
{
|
||||
var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct);
|
||||
if (!ownership.IsAllowed) return NotFound(new { ok = false, error = ownership.Error });
|
||||
return await Exec(SqlNames.Procs.PerformanceMetric, "compare", JsonSerializer.Serialize(new { initiativeId, lookbackDays }), ct);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ────────────────────────────────────────────────
|
||||
|
||||
private async Task<IActionResult> Exec(string proc, string action, string rqst, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resp = await _sql.ExecProcAsync(proc, action, rqst, ct: ct);
|
||||
if (string.IsNullOrWhiteSpace(resp))
|
||||
return StatusCode(500, new { ok = false, error = "Service unavailable" });
|
||||
|
||||
using var doc = JsonDocument.Parse(resp);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("ok", out var okProp) && !okProp.GetBoolean())
|
||||
{
|
||||
var error = root.TryGetProperty("error", out var errProp) ? errProp.GetString() : "Unknown error";
|
||||
if (error?.Contains("not found", StringComparison.OrdinalIgnoreCase) == true)
|
||||
return NotFound(JsonSerializer.Deserialize<object>(resp));
|
||||
return BadRequest(JsonSerializer.Deserialize<object>(resp));
|
||||
}
|
||||
|
||||
return Content(resp, "application/json");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Initiative {Action} error", action);
|
||||
return StatusCode(500, new { ok = false, error = "Service error" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── DTOs ──
|
||||
|
||||
public sealed class CreateInitiativeRequest
|
||||
{
|
||||
public long? InitiativeId { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Objective { get; set; }
|
||||
public decimal? TotalBudget { get; set; }
|
||||
public string? BudgetPeriod { get; set; }
|
||||
public string? StartDate { get; set; }
|
||||
public string? EndDate { get; set; }
|
||||
public string? AllocationStrategy { get; set; }
|
||||
public string? BusinessCategory { get; set; }
|
||||
public string? WizardId { get; set; }
|
||||
public object[]? Channels { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateInitiativeRequest
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public decimal? TotalBudget { get; set; }
|
||||
public string? StartDate { get; set; }
|
||||
public string? EndDate { get; set; }
|
||||
public string? BusinessCategory { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateInitiativeStatusRequest { public string? Status { get; set; } }
|
||||
public sealed class SyncChannelRequest
|
||||
{
|
||||
public string? ExternalCampaignId { get; set; }
|
||||
public string? ExternalAccountId { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public string? ProviderStatus { get; set; }
|
||||
public string? ChannelType { get; set; }
|
||||
}
|
||||
public sealed class UpdateAllocationRequest { public object[]? Allocations { get; set; } public string? Reason { get; set; } }
|
||||
public sealed class ApplyAllocationRequest { public string? Source { get; set; } public object[]? Allocations { get; set; } public string? Reason { get; set; } }
|
||||
Reference in New Issue
Block a user