Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/contact-center/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1382,10 +1382,13 @@ Keep this section current. Use the checklist below to track phase-level progress
- [x] Registered in `.slnx` and the `Cms.Core.Targets` bundle
- [x] 13 unit tests (event envelope, event publisher dispatch/idempotency/resilience, interaction manager lifecycle, entity metadata extensibility)
- [x] Docs landing page + `v2.0.0` changelog entry
- [ ] Phase 2 — Agent, presence, queue, and reservation foundation
- [ ] Phase 3 — Routing MVP
- [x] **Phase 2 — Agent, presence, queue, and reservation foundation**
- [x] `Agents` feature: AgentProfile (presence, capacity, skills, queue/campaign membership), store/manager/index, presence manager, Agent Workspace sign-in/out
- [x] `Queues` feature: ActivityQueue, QueueItem, ActivityReservation models/stores/managers/indexes; queue + reservation lifecycle (reserve/accept/reject/expire); reservation-expiry background task
- [x] Availability-based assignment service (longest-idle agent ↔ highest-priority item); agent/queue/dialer permissions; admin menu + CRUD UI; unit tests
- [~] Phase 3 — Routing MVP (availability + priority + longest-idle assignment shipped; skills/sticky/business-hours/audit pending)
- [ ] Phase 4 — Voice integration with Telephony
- [ ] Phase 5 — Outbound dialer MVP
- [~] Phase 5 — Outbound dialer MVP (`Dialer` feature: profiles, modes, dialer-agnostic `IDialerProvider`/resolver, power/progressive pacing, dialer batch sources, DialPad.Dialer provider; retry/callback/suppression pending)
- [ ] Phase 6 — Wrap-up and disposition lifecycle
- [ ] Phase 7 — Agent desktop and supervisor real-time UX
- [ ] Phase 8 — Inbound entry points, IVR and self-service
Expand All @@ -1403,3 +1406,4 @@ Keep this section current. Use the checklist below to track phase-level progress
- Phase 0 started: promoted the session plan to this durable repo-tracked document, added the copilot-instructions pointer, and created the public Contact Center docs landing page.
- Phase 0 completed and Phase 1 (Domain foundation) implemented: added the `ContactCenter.Abstractions`, `ContactCenter.Core`, and `ContactCenter` base module projects; extended `OmnichannelActivity` with kind/source/assignment/reservation metadata so CRM activities remain the universal work item; made `Interaction` an Orchard `Entity` communication-history record linked to activities; added the durable `InteractionEvent` log with idempotency and the `DefaultContactCenterEventPublisher`; added the `IActivityDispositionService` contract; registered everything in `.slnx` and the `Cms.Core.Targets` bundle; added 13 unit tests; and documented the feature on the docs landing page and the `v2.0.0` changelog. The base feature is headless by design — all future CRUD/agent/supervisor UI must use Display Management, display drivers, shapes, placement, and AI Profile-style catalog screens. Next: Phase 2 (agents, presence, activity queues, reservations).
- Activity Batch source selection implemented: **Add Activity Batch** now opens a source modal like AI Provider Connections, sources are registered through `ActivityBatchSourceOptions`, and source cards use shape alternates. Manual batches keep the selected-user assignment flow. Dialer batches hide the user selector and load activities as unassigned `Available` dialer inventory for later reservation by dialer/routing services.
- Phase 2 implemented and Phases 3/5 started: added `Agents`, `Queues`, and `Dialer` features in the ContactCenter module. Core now has AgentProfile/ActivityQueue/QueueItem/ActivityReservation/DialerProfile models, indexes, stores, and managers, plus presence/queue/reservation/assignment/dialer orchestration services. Assignment pairs the highest-priority waiting item with the longest-idle available agent (Phase 3 core); reservations expire via background task. Outbound dialing is dialer-agnostic via `IDialerProvider`/`IDialerProviderResolver`, with power/progressive pacing and dialer batch sources (Phase 5 core). Added the `DialPad.Dialer` feature implementing `IDialerProvider` over the DialPad telephony provider. Added admin CRUD for queues/dialer profiles + agent sign-in workspace, agent/queue/dialer permissions, and 7 new unit tests (20 total pass). Docs + `v2.0.0` changelog updated. Next: skills/sticky/business-hours routing (Phase 3), voice integration (Phase 4), retry/callback/suppression (Phase 5), wrap-up (Phase 6).
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ public static class Feature
/// The identifier of the base Contact Center feature.
/// </summary>
public const string Area = "CrestApps.OrchardCore.ContactCenter";

/// <summary>
/// The identifier of the agent, presence, and queue-membership feature.
/// </summary>
public const string Agents = "CrestApps.OrchardCore.ContactCenter.Agents";

/// <summary>
/// The identifier of the queue and reservation feature.
/// </summary>
public const string Queues = "CrestApps.OrchardCore.ContactCenter.Queues";

/// <summary>
/// The identifier of the outbound dialer feature.
/// </summary>
public const string Dialer = "CrestApps.OrchardCore.ContactCenter.Dialer";
}

/// <summary>
Expand Down Expand Up @@ -127,5 +142,75 @@ public static class Events
/// Raised when an interaction fails.
/// </summary>
public const string InteractionFailed = "InteractionFailed";

/// <summary>
/// Raised when an activity is added to a queue.
/// </summary>
public const string QueueItemAdded = "QueueItemAdded";

/// <summary>
/// Raised when a queue item is reserved for an agent.
/// </summary>
public const string QueueItemReserved = "QueueItemReserved";

/// <summary>
/// Raised when a queue item is assigned to an agent.
/// </summary>
public const string QueueItemAssigned = "QueueItemAssigned";

/// <summary>
/// Raised when a queue item leaves the queue.
/// </summary>
public const string QueueItemDequeued = "QueueItemDequeued";

/// <summary>
/// Raised when an agent signs in.
/// </summary>
public const string AgentSignedIn = "AgentSignedIn";

/// <summary>
/// Raised when an agent signs out.
/// </summary>
public const string AgentSignedOut = "AgentSignedOut";

/// <summary>
/// Raised when an agent presence state changes.
/// </summary>
public const string AgentPresenceChanged = "AgentPresenceChanged";

/// <summary>
/// Raised when an agent is reserved for an offer.
/// </summary>
public const string AgentReserved = "AgentReserved";

/// <summary>
/// Raised when an agent reservation is released.
/// </summary>
public const string AgentReleased = "AgentReleased";

/// <summary>
/// Raised when a dialer run starts.
/// </summary>
public const string DialerRunStarted = "DialerRunStarted";

/// <summary>
/// Raised when a dialer attempt is scheduled.
/// </summary>
public const string DialerAttemptScheduled = "DialerAttemptScheduled";

/// <summary>
/// Raised when a dialer attempt starts dialing.
/// </summary>
public const string DialerAttemptStarted = "DialerAttemptStarted";

/// <summary>
/// Raised when a dialer attempt completes.
/// </summary>
public const string DialerAttemptCompleted = "DialerAttemptCompleted";

/// <summary>
/// Raised when a callback is scheduled.
/// </summary>
public const string CallbackScheduled = "CallbackScheduled";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using CrestApps.OrchardCore.ContactCenter.Models;
using Microsoft.Extensions.Localization;

namespace CrestApps.OrchardCore.ContactCenter;

/// <summary>
/// Defines a dialer-agnostic provider that executes outbound calling on behalf of the Contact Center.
/// The Contact Center owns all assignment, queue, pacing, and compliance logic; the provider only places
/// and ends calls so any telephony platform can act as the calling engine.
/// </summary>
public interface IDialerProvider
{
/// <summary>
/// Gets the stable technical name used to resolve the provider.
/// </summary>
string TechnicalName { get; }

/// <summary>
/// Gets the localized, human-readable name of the provider.
/// </summary>
LocalizedString DisplayName { get; }

/// <summary>
/// Gets the calling capabilities supported by the provider.
/// </summary>
DialerProviderCapabilities Capabilities { get; }

/// <summary>
/// Places an outbound call for a reserved activity.
/// </summary>
/// <param name="request">The provider-agnostic dial request.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The result of the dial operation.</returns>
Task<DialerDialResult> PlaceCallAsync(DialerDialRequest request, CancellationToken cancellationToken = default);

/// <summary>
/// Ends or cancels a provider call previously placed for an attempt.
/// </summary>
/// <param name="providerCallId">The provider call identifier returned by <see cref="PlaceCallAsync"/>.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The result of the end-call operation.</returns>
Task<DialerDialResult> EndCallAsync(string providerCallId, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace CrestApps.OrchardCore.ContactCenter;

/// <summary>
/// Resolves the registered <see cref="IDialerProvider"/> implementations so the Contact Center can
/// dial through any installed provider without depending on a specific telephony platform.
/// </summary>
public interface IDialerProviderResolver
{
/// <summary>
/// Resolves the dialer provider with the specified technical name, or the only registered provider when no name is supplied.
/// </summary>
/// <param name="technicalName">The provider technical name, or <see langword="null"/> to resolve the default.</param>
/// <returns>The matching provider, or <see langword="null"/> when none is found.</returns>
IDialerProvider Get(string technicalName = null);

/// <summary>
/// Gets every registered dialer provider.
/// </summary>
/// <returns>The registered dialer providers.</returns>
IEnumerable<IDialerProvider> GetAll();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace CrestApps.OrchardCore.ContactCenter.Models;

/// <summary>
/// Identifies the live availability state of a Contact Center agent.
/// </summary>
public enum AgentPresenceStatus
{
/// <summary>
/// The agent is signed out and cannot receive work.
/// </summary>
Offline,

/// <summary>
/// The agent is signed in and available to receive work.
/// </summary>
Available,

/// <summary>
/// The agent has been reserved for an offer but has not yet accepted it.
/// </summary>
Reserved,

/// <summary>
/// The agent is actively working an interaction.
/// </summary>
Busy,

/// <summary>
/// The agent is completing post-interaction wrap-up work.
/// </summary>
WrapUp,

/// <summary>
/// The agent is signed in but temporarily not ready for work.
/// </summary>
Break,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace CrestApps.OrchardCore.ContactCenter.Models;

/// <summary>
/// Represents a provider-agnostic request to place an outbound call for a reserved activity.
/// The Contact Center owns the activity, queue, agent, campaign, and compliance decisions; the
/// dialer provider only executes the calling operation.
/// </summary>
public sealed class DialerDialRequest
{
/// <summary>
/// Gets or sets the CRM activity identifier being dialed.
/// </summary>
public string ActivityId { get; set; }

/// <summary>
/// Gets or sets the Contact Center interaction identifier for this attempt.
/// </summary>
public string InteractionId { get; set; }

/// <summary>
/// Gets or sets the queue identifier the activity belongs to.
/// </summary>
public string QueueId { get; set; }

/// <summary>
/// Gets or sets the campaign identifier the activity belongs to.
/// </summary>
public string CampaignId { get; set; }

/// <summary>
/// Gets or sets the reserved agent identifier when the dialing mode requires an agent before dialing.
/// </summary>
public string AgentId { get; set; }

/// <summary>
/// Gets or sets the destination address to dial.
/// </summary>
public string Destination { get; set; }

/// <summary>
/// Gets or sets the caller identifier to present when supported.
/// </summary>
public string CallerId { get; set; }

/// <summary>
/// Gets or sets provider-specific metadata for the dial request.
/// </summary>
public IDictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace CrestApps.OrchardCore.ContactCenter.Models;

/// <summary>
/// Represents the result of a dialer provider call operation.
/// </summary>
public sealed class DialerDialResult
{
/// <summary>
/// Gets or sets a value indicating whether the provider accepted the dial request.
/// </summary>
public bool Succeeded { get; set; }

/// <summary>
/// Gets or sets the provider call identifier returned by the provider.
/// </summary>
public string ProviderCallId { get; set; }

/// <summary>
/// Gets or sets the provider error code when the dial request failed.
/// </summary>
public string ErrorCode { get; set; }

/// <summary>
/// Gets or sets the provider error message when the dial request failed.
/// </summary>
public string ErrorMessage { get; set; }

/// <summary>
/// Gets or sets provider-specific result metadata.
/// </summary>
public IDictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>();

/// <summary>
/// Creates a successful result for the specified provider call identifier.
/// </summary>
/// <param name="providerCallId">The provider call identifier.</param>
/// <returns>A successful <see cref="DialerDialResult"/>.</returns>
public static DialerDialResult Success(string providerCallId)
{
return new DialerDialResult
{
Succeeded = true,
ProviderCallId = providerCallId,
};
}

/// <summary>
/// Creates a failed result with the specified error details.
/// </summary>
/// <param name="errorCode">The provider error code.</param>
/// <param name="errorMessage">The provider error message.</param>
/// <returns>A failed <see cref="DialerDialResult"/>.</returns>
public static DialerDialResult Failure(string errorCode, string errorMessage)
{
return new DialerDialResult
{
Succeeded = false,
ErrorCode = errorCode,
ErrorMessage = errorMessage,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace CrestApps.OrchardCore.ContactCenter.Models;

/// <summary>
/// Identifies the outbound dialing strategy used by a dialer profile.
/// </summary>
public enum DialerMode
{
/// <summary>
/// The agent chooses and places the call manually.
/// </summary>
Manual,

/// <summary>
/// The agent reviews the activity, then accepts or skips before dialing.
/// </summary>
Preview,

/// <summary>
/// The system reserves agents and dials a controlled number of calls per available agent.
/// </summary>
Power,

/// <summary>
/// The system dials one call per reserved agent as agents become available.
/// </summary>
Progressive,

/// <summary>
/// The system forecasts answer rates and dials ahead of agent availability.
/// </summary>
Predictive,
}
Loading
Loading