Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
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
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 && expand.Any())
{
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);

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);

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<string> GetSchedulesAsync(string businessUnitId, string weekId, bool? includeOnlyPublished = null, string expand = null, CancellationToken cancellationToken = default)
Copy link
Owner

Choose a reason for hiding this comment

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

Typically expand is a IEnumerable can you check the old code and see if this method is a single string or a collection.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Checked old code - schedule-related expand parameters are single string values, not collections. Kept as string type and added proper Uri.EscapeDataString() escaping for path parameters. Commit 1aa0968

{
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.ReadAsStringAsync(cancellationToken);
}

/// <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
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
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>
/// <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 - simplified return type
/// </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>Schedules as JSON string</returns>
Task<string> 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