Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
501e1cd
Initial plan
Copilot Jul 27, 2025
627f731
Create WorkforceManagementApi and interface with model issues to resolve
Copilot Jul 27, 2025
c65085e
Add essential workforce management models to project
Copilot Jul 27, 2025
f488b65
Changes before error encountered
Copilot Jul 27, 2025
124382f
Significant progress on WorkforceManagementApi dependencies - reduced…
Copilot Jul 27, 2025
9f153c0
🎉 SUCCESS: WorkforceManagementApi now builds successfully! Resolved a…
Copilot Jul 27, 2025
1aa0968
Fix WorkforceManagementApi code review issues: URL escaping, expand p…
Copilot Jul 28, 2025
66bcc38
Address code review feedback: remove Any() call, add newlines, fix Ge…
Copilot Jul 28, 2025
49bd2f0
Address review feedback: remove manual exception throwing, fix null c…
Copilot Jul 28, 2025
5cb01bb
Continue WFM API migration: Add GetWorkPlansAsync, CreateWorkPlanAsyn…
Copilot Jul 28, 2025
65363ab
Clean involved models for WorkforceManagementApi - remove complex SDK…
Copilot Jul 28, 2025
a89a6bc
Address code review feedback: rename expand parameters to expands and…
Copilot Jul 28, 2025
0d13aa7
Implement 71 additional WorkforceManagementApi methods - now 90/245 m…
Copilot Jul 29, 2025
238993a
Implement 39 additional methods reaching 52.7% coverage (129/245 meth…
Copilot Jul 29, 2025
599e7d8
Major WorkforceManagementApi expansion - added 64 new methods for 78.…
Copilot Jul 29, 2025
8ace08a
🎉 COMPLETE! Full WorkforceManagementApi migration - 100% coverage (24…
Copilot Jul 29, 2025
e03986e
Fix model cleaning and API return types for WorkforceManagementApi co…
Copilot Jul 30, 2025
de502b0
Temporary fix: simplified models to object types to resolve build issues
Copilot Jul 30, 2025
c860b22
Fix model cleaning: restore concrete types instead of object, extract…
Copilot Jul 30, 2025
3f445a1
Fix model cleaning issues: add newline to ActivityCode.Category.cs an…
Copilot Jul 30, 2025
deac552
Clean ExpandableWebDeployment model: extract enum and change List to …
Copilot Aug 6, 2025
3584356
Fix WorkforceManagementApi code style compliance - HttpClient pattern…
Copilot Aug 20, 2025
b551fc0
Apply code style compliance fixes: seal class, use IEnumerable instea…
Copilot Aug 20, 2025
d8b51fb
Remove extra whitespace: eliminate 68 consecutive blank lines from Wo…
Copilot Aug 20, 2025
1f97ab8
Fix model property types: change List to IEnumerable and correct enum…
Copilot Aug 20, 2025
ad6d7d0
Complete model cleaning: extract CreateAdminTimeOffRequest.Status enu…
Copilot Aug 21, 2025
1674c14
Merge branch 'main' into copilot/fix-113
JackTelford Aug 21, 2025
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
245 changes: 245 additions & 0 deletions src/PureCloud.Client/Apis/WorkforceManagementApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
using System.Collections.Specialized;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Options;
using PureCloud.Client.Contracts;
using PureCloud.Client.Http;
using PureCloud.Client.Json;
using PureCloud.Client.Models;

namespace PureCloud.Client.Apis;

/// <inheritdoc />
public sealed class WorkforceManagementApi : IWorkforceManagementApi
{
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _options;

public WorkforceManagementApi(IHttpClientFactory httpClientFactory, IOptions<PureCloudJsonSerializerOptions> options)
{
_httpClient = httpClientFactory.CreateClient(PureCloudConstants.PureCloudClientName);
_options = options.Value.JsonSerializerOptions;
}

/// <inheritdoc />
public async Task<BusinessUnitListing> GetBusinessUnitsAsync(string feature = null, string divisionId = null, CancellationToken cancellationToken = default)
{
var parameters = new NameValueCollection();

if (!string.IsNullOrEmpty(feature))
{
parameters.Add("feature", UriHelper.ParameterToString(feature));
}

if (!string.IsNullOrEmpty(divisionId))
{
parameters.Add("divisionId", UriHelper.ParameterToString(divisionId));
}

var uri = UriHelper.GetUri("api/v2/workforcemanagement/businessunits", parameters);

var response = await _httpClient.GetAsync(uri, cancellationToken);

response.EnsureSuccessStatusCode();

return await response.Content.ReadFromJsonAsync<BusinessUnitListing>(_options, cancellationToken);
}

/// <inheritdoc />
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expands should be IEnumerable and it should be renamed to expands. In the foreach loop it would be expand in expands

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed - changed expand parameter to expands with IEnumerable<string> type and updated foreach loop pattern. Commit a89a6bc

public async Task<BusinessUnitResponse> GetBusinessUnitAsync(string businessUnitId, List<string> expand = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(businessUnitId);

var parameters = new NameValueCollection();

if (expand != null)
{
foreach (var item in expand)
{
parameters.Add("expand", item);
}
}

var uri = UriHelper.GetUri($"api/v2/workforcemanagement/businessunits/{Uri.EscapeDataString(businessUnitId)}", parameters);

var response = await _httpClient.GetAsync(uri, cancellationToken);

response.EnsureSuccessStatusCode();

return await response.Content.ReadFromJsonAsync<BusinessUnitResponse>(_options, cancellationToken);
}

/// <inheritdoc />
public async Task<bool> DeleteBusinessUnitAsync(string businessUnitId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(businessUnitId);

var uri = $"api/v2/workforcemanagement/businessunits/{Uri.EscapeDataString(businessUnitId)}";

var response = await _httpClient.DeleteAsync(uri, cancellationToken);
Comment thread
MikeAlhayek marked this conversation as resolved.
Outdated

return response.IsSuccessStatusCode;
}

/// <inheritdoc />
public async Task<bool> DeleteManagementUnitAsync(string managementUnitId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(managementUnitId);

var uri = $"api/v2/workforcemanagement/managementunits/{Uri.EscapeDataString(managementUnitId)}";

var response = await _httpClient.DeleteAsync(uri, cancellationToken);

return response.IsSuccessStatusCode;
}

/// <inheritdoc />
public async Task<ActivityCodeContainer> GetActivityCodesAsync(string businessUnitId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(businessUnitId);

var uri = $"api/v2/workforcemanagement/businessunits/{Uri.EscapeDataString(businessUnitId)}/activitycodes";

var response = await _httpClient.GetAsync(uri, cancellationToken);

response.EnsureSuccessStatusCode();

return await response.Content.ReadFromJsonAsync<ActivityCodeContainer>(_options, cancellationToken);
}

/// <inheritdoc />
public async Task<ActivityCode> CreateActivityCodeAsync(string businessUnitId, CreateActivityCodeRequest body, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(businessUnitId);
ArgumentNullException.ThrowIfNull(body);

Comment thread
MikeAlhayek marked this conversation as resolved.
var uri = $"api/v2/workforcemanagement/businessunits/{Uri.EscapeDataString(businessUnitId)}/activitycodes";

var response = await _httpClient.PostAsJsonAsync(uri, body, _options, cancellationToken);

response.EnsureSuccessStatusCode();

return await response.Content.ReadFromJsonAsync<ActivityCode>(_options, cancellationToken);
}

/// <inheritdoc />
public async Task<bool> DeleteActivityCodeAsync(string businessUnitId, string activityCodeId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(businessUnitId);
ArgumentException.ThrowIfNullOrEmpty(activityCodeId);

var uri = $"api/v2/workforcemanagement/businessunits/{Uri.EscapeDataString(businessUnitId)}/activitycodes/{Uri.EscapeDataString(activityCodeId)}";

var response = await _httpClient.DeleteAsync(uri, cancellationToken);

return response.IsSuccessStatusCode;
}

/// <inheritdoc />
public async Task<BuScheduleListing> GetSchedulesAsync(string businessUnitId, string weekId, bool? includeOnlyPublished = null, string expand = null, CancellationToken cancellationToken = default)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the old method accept IEnumberable or a single string? Nothing to do if a single string otherwise change it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the old API - GetWorkforcemanagementBusinessunitWeekSchedules takes a single string expand parameter, not IEnumerable. Current implementation is correct.

{
ArgumentException.ThrowIfNullOrEmpty(businessUnitId);
ArgumentException.ThrowIfNullOrEmpty(weekId);

var parameters = new NameValueCollection();

if (includeOnlyPublished.HasValue)
{
parameters.Add("includeOnlyPublished", UriHelper.ParameterToString(includeOnlyPublished.Value));
}

if (!string.IsNullOrEmpty(expand))
{
parameters.Add("expand", UriHelper.ParameterToString(expand));
}

var uri = UriHelper.GetUri($"api/v2/workforcemanagement/businessunits/{Uri.EscapeDataString(businessUnitId)}/weeks/{Uri.EscapeDataString(weekId)}/schedules", parameters);

var response = await _httpClient.GetAsync(uri, cancellationToken);

response.EnsureSuccessStatusCode();

return await response.Content.ReadFromJsonAsync<BuScheduleListing>(_options, cancellationToken) ?? throw new JsonException("Failed to deserialize response.");
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't manually throw an exception. If the ReadFromJson fails it'll auto throw an exception internally.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed - removed manual exception throwing in GetSchedulesAsync method. ReadFromJsonAsync will auto-throw internally if deserialization fails. Commit 49bd2f0

}

/// <inheritdoc />
public async Task<string> GetScheduleAsync(string businessUnitId, string weekId, string scheduleId, string expand = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(businessUnitId);
ArgumentException.ThrowIfNullOrEmpty(weekId);
ArgumentException.ThrowIfNullOrEmpty(scheduleId);

var parameters = new NameValueCollection();

if (!string.IsNullOrEmpty(expand))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check the type of expand parameter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked old API - schedule expand parameter is string, not List<string>. Type is correct and path parameters now properly escaped. Commit 1aa0968

{
parameters.Add("expand", UriHelper.ParameterToString(expand));
}

var uri = UriHelper.GetUri($"api/v2/workforcemanagement/businessunits/{Uri.EscapeDataString(businessUnitId)}/weeks/{Uri.EscapeDataString(weekId)}/schedules/{Uri.EscapeDataString(scheduleId)}", parameters);

var response = await _httpClient.GetAsync(uri, cancellationToken);

response.EnsureSuccessStatusCode();

return await response.Content.ReadAsStringAsync(cancellationToken);
}

/// <inheritdoc />
public async Task<bool> DeleteScheduleAsync(string businessUnitId, string weekId, string scheduleId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(businessUnitId);
ArgumentException.ThrowIfNullOrEmpty(weekId);
ArgumentException.ThrowIfNullOrEmpty(scheduleId);

var uri = $"api/v2/workforcemanagement/businessunits/{Uri.EscapeDataString(businessUnitId)}/weeks/{Uri.EscapeDataString(weekId)}/schedules/{Uri.EscapeDataString(scheduleId)}";

var response = await _httpClient.DeleteAsync(uri, cancellationToken);

return response.IsSuccessStatusCode;
}

/// <inheritdoc />
public async Task<string> GetForecastsAsync(string businessUnitId, string weekDateId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(businessUnitId);
ArgumentException.ThrowIfNullOrEmpty(weekDateId);

var uri = $"api/v2/workforcemanagement/businessunits/{Uri.EscapeDataString(businessUnitId)}/weeks/{Uri.EscapeDataString(weekDateId)}/shorttermforecasts";

var response = await _httpClient.GetAsync(uri, cancellationToken);

response.EnsureSuccessStatusCode();

return await response.Content.ReadAsStringAsync(cancellationToken);
}

/// <inheritdoc />
public async Task<bool> DeleteForecastAsync(string businessUnitId, string weekDateId, string forecastId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(businessUnitId);
ArgumentException.ThrowIfNullOrEmpty(weekDateId);
ArgumentException.ThrowIfNullOrEmpty(forecastId);

var uri = $"api/v2/workforcemanagement/businessunits/{Uri.EscapeDataString(businessUnitId)}/weeks/{Uri.EscapeDataString(weekDateId)}/shorttermforecasts/{Uri.EscapeDataString(forecastId)}";

var response = await _httpClient.DeleteAsync(uri, cancellationToken);

return response.IsSuccessStatusCode;
}

/// <inheritdoc />
public async Task<string> CreateTimeOffRequestAsync(string managementUnitId, CreateAdminTimeOffRequest body, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(managementUnitId);
ArgumentNullException.ThrowIfNull(body);

var uri = $"api/v2/workforcemanagement/managementunits/{Uri.EscapeDataString(managementUnitId)}/timeoffrequests";

var response = await _httpClient.PostAsJsonAsync(uri, body, _options, cancellationToken);

response.EnsureSuccessStatusCode();

return await response.Content.ReadAsStringAsync(cancellationToken);
}
}
129 changes: 129 additions & 0 deletions src/PureCloud.Client/Contracts/IWorkforceManagementApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using PureCloud.Client.Models;

namespace PureCloud.Client.Contracts;

/// <summary>
/// Workforce Management API operations
/// </summary>
public interface IWorkforceManagementApi
{
/// <summary>
/// Get business units
/// </summary>
Comment thread
MikeAlhayek marked this conversation as resolved.
/// <param name="feature">The feature to filter by</param>
/// <param name="divisionId">The division ID to filter by</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Business unit listing</returns>
Task<BusinessUnitListing> GetBusinessUnitsAsync(string feature = null, string divisionId = null, CancellationToken cancellationToken = default);

/// <summary>
/// Get a business unit
/// </summary>
/// <param name="businessUnitId">The ID of the business unit</param>
/// <param name="expand">Include to access additional data on the business unit</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Business unit response</returns>
Task<BusinessUnitResponse> GetBusinessUnitAsync(string businessUnitId, List<string> expand = null, CancellationToken cancellationToken = default);

/// <summary>
/// Delete a business unit
/// </summary>
/// <param name="businessUnitId">The ID of the business unit</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if deletion was successful</returns>
Task<bool> DeleteBusinessUnitAsync(string businessUnitId, CancellationToken cancellationToken = default);

/// <summary>
/// Delete a management unit
/// </summary>
/// <param name="managementUnitId">The ID of the management unit</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if deletion was successful</returns>
Task<bool> DeleteManagementUnitAsync(string managementUnitId, CancellationToken cancellationToken = default);

/// <summary>
/// Get activity codes
/// </summary>
/// <param name="businessUnitId">The ID of the business unit</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Activity code container</returns>
Task<ActivityCodeContainer> GetActivityCodesAsync(string businessUnitId, CancellationToken cancellationToken = default);

/// <summary>
/// Create an activity code
/// </summary>
/// <param name="businessUnitId">The ID of the business unit</param>
/// <param name="body">The activity code request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Created activity code</returns>
Task<ActivityCode> CreateActivityCodeAsync(string businessUnitId, CreateActivityCodeRequest body, CancellationToken cancellationToken = default);

/// <summary>
/// Delete an activity code
/// </summary>
/// <param name="businessUnitId">The ID of the business unit</param>
/// <param name="activityCodeId">The ID of the activity code to delete</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if deletion was successful</returns>
Task<bool> DeleteActivityCodeAsync(string businessUnitId, string activityCodeId, CancellationToken cancellationToken = default);

/// <summary>
/// Get schedules for a week
/// </summary>
/// <param name="businessUnitId">The ID of the business unit</param>
/// <param name="weekId">The week ID</param>
/// <param name="includeOnlyPublished">Include only published schedules</param>
/// <param name="expand">Expand parameter</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Schedule listing</returns>
Task<BuScheduleListing> GetSchedulesAsync(string businessUnitId, string weekId, bool? includeOnlyPublished = null, string expand = null, CancellationToken cancellationToken = default);

/// <summary>
/// Get a schedule - simplified return type
/// </summary>
/// <param name="businessUnitId">The ID of the business unit</param>
/// <param name="weekId">The week ID</param>
/// <param name="scheduleId">The schedule ID</param>
/// <param name="expand">Expand parameter</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Schedule as JSON string</returns>
Task<string> GetScheduleAsync(string businessUnitId, string weekId, string scheduleId, string expand = null, CancellationToken cancellationToken = default);

/// <summary>
/// Delete a schedule
/// </summary>
/// <param name="businessUnitId">The ID of the business unit</param>
/// <param name="weekId">The week ID</param>
/// <param name="scheduleId">The schedule ID</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if deletion was successful</returns>
Task<bool> DeleteScheduleAsync(string businessUnitId, string weekId, string scheduleId, CancellationToken cancellationToken = default);

/// <summary>
/// Get short term forecasts - simplified return type
/// </summary>
/// <param name="businessUnitId">The ID of the business unit</param>
/// <param name="weekDateId">The week date ID</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Forecasts as JSON string</returns>
Task<string> GetForecastsAsync(string businessUnitId, string weekDateId, CancellationToken cancellationToken = default);

/// <summary>
/// Delete a short term forecast
/// </summary>
/// <param name="businessUnitId">The ID of the business unit</param>
/// <param name="weekDateId">The week date ID</param>
/// <param name="forecastId">The forecast ID</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if deletion was successful</returns>
Task<bool> DeleteForecastAsync(string businessUnitId, string weekDateId, string forecastId, CancellationToken cancellationToken = default);

/// <summary>
/// Create a time off request
/// </summary>
/// <param name="managementUnitId">The ID of the management unit</param>
/// <param name="body">The time off request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Created time off request (temporarily simplified as string)</returns>
Task<string> CreateTimeOffRequestAsync(string managementUnitId, CreateAdminTimeOffRequest body, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,11 @@ public static IServiceCollection AddPureCloudApis(this IServiceCollection servic

services.TryAddScoped<IWebChatApi, WebChatApi>();

services.TryAddScoped<IWebMessagingApi, WebMessagingApi>();

services.TryAddScoped<IWidgetsApi, WidgetsApi>();
services.TryAddScoped<IWebMessagingApi, WebMessagingApi>();

services.TryAddScoped<IWidgetsApi, WidgetsApi>();

services.TryAddScoped<IWorkforceManagementApi, WorkforceManagementApi>();

return services;
}
Expand Down
Loading