Skip to content
Merged

Dev #2485

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e92a491
AB#31883 improve timezone offset
AndreGAot May 15, 2026
069bdbe
AB#27996: Refactor CreatePaymentRequests to Create Reusable Service C…
aurelio-aot May 15, 2026
2d29595
AB#31883 copilot feedback
AndreGAot May 15, 2026
c9badb7
AB#31883 more copilot suggestions
AndreGAot May 15, 2026
e65494c
Potential fix for pull request finding 'CodeQL / Cookie 'Secure' attr…
AndreGAot May 15, 2026
45c2ece
Merge pull request #2477 from bcgov/bugfix/AB#31883-comment-timestamp
AndreGAot May 15, 2026
c911592
AB#27996: Create Historical Payments - Initial Draft
aurelio-aot May 16, 2026
c2ec744
AB#27996: Fix bug and address sonarqube issues
aurelio-aot May 19, 2026
66f9d01
AB#27996: Fix failing unit tests
aurelio-aot May 19, 2026
d6553de
AB#27996: Fix button alignment
aurelio-aot May 20, 2026
5832c69
AB#27996: Move Warning Inside Historical payment dialog box. Add Site…
aurelio-aot May 20, 2026
1d56015
AB#27996: Fix Invoice and Amounts validation
aurelio-aot May 20, 2026
9767a8c
AB#27996: AccountCoding is not required for Historical Payments
aurelio-aot May 20, 2026
e186d32
AB#33055 fix AI generate cooldown wait state
jacobwillsmith May 15, 2026
64f3f4b
AB#33055 sync AI generate button state
jacobwillsmith May 20, 2026
45e6f4b
AB#31474: Fix lastUpdatedinCAS=infinity in both Supplier and Site
aurelio-aot May 21, 2026
5b5caba
AB#27996: Add Note for missing Site in Historical Payments
aurelio-aot May 21, 2026
fc558ba
AB#32218 convert to as single query explicit for app attachments
AndreGAot May 22, 2026
29dbfff
feature/AB#32203-FixMappingArchive
JamesPasta May 22, 2026
465ad27
Merge pull request #2482 from bcgov/feature/AB#32203-FixArchive
JamesPasta May 22, 2026
67cc58a
Merge pull request #2478 from bcgov/feature/AB#27996-Add-Historical-P…
JamesPasta May 22, 2026
5d14237
Merge pull request #2480 from bcgov/bugfix/AB#31474-Infinity-In-LastU…
JamesPasta May 22, 2026
ef2c95b
Merge pull request #2481 from bcgov/bugfix/AB#32218-intermittent-fail…
JamesPasta May 22, 2026
8ad818d
Merge pull request #2479 from bcgov/bugfix/AB#33055-ai-generate-butto…
JamesPasta May 22, 2026
a61d79f
AB#32611: Additional Columns In Audit History
aurelio-aot May 22, 2026
21c5323
Merge pull request #2483 from bcgov/feature/AB#32611-Audit-History-En…
JamesPasta May 22, 2026
6bc921c
feature/AB#33117-FixScribian
JamesPasta May 22, 2026
3c5b8bd
Merge pull request #2484 from bcgov/feature/AB#33117-FixScribian7.1.0
JamesPasta May 22, 2026
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
6 changes: 6 additions & 0 deletions applications/Unity.GrantManager/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@
<Content Remove="$(UserProfile)\.nuget\packages\*\*\contentFiles\any\*\*.abppkg*.json" />
</ItemGroup>

<ItemGroup>
<!-- Add direct reference to the patched Scriban to ensure all projects importing common.props
use the safe version (pins transitive consumers that do not specify a version). -->
<PackageReference Include="Scriban" Version="7.2.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ namespace Unity.AI.RateLimit;
public class AIRateLimitStateDto
{
public int RetryAfterSeconds { get; set; }
public bool IsGenerating { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Threading.Tasks;

namespace Unity.AI.RateLimit;

public interface IAIGenerationActivityProvider
{
Task<bool> HasActiveGenerationAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ public interface IAIRateLimiter
Task StampAsync(Guid? userId);

/// <summary>
/// Returns the remaining cooldown for the current user. RetryAfterSeconds is 0 when
/// the user can generate immediately.
/// Returns the current user's shared AI generation state. RetryAfterSeconds is 0 when
/// the user can generate immediately, unless a generation is still active.
/// </summary>
Task<AIRateLimitStateDto> GetStateAsync();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Medallion.Threading;
Expand All @@ -20,7 +21,8 @@ public class AIRateLimiter(
IDistributedCache cache,
ICurrentUser currentUser,
IConfiguration configuration,
IDistributedLockProvider distributedLockProvider) : IAIRateLimiter, ITransientDependency
IDistributedLockProvider distributedLockProvider,
IEnumerable<IAIGenerationActivityProvider> generationActivityProviders) : IAIRateLimiter, ITransientDependency
{
private const string CooldownKeyPrefix = "ai-generation:cooldown:";
private const string CooldownLockPrefix = "ai-generation:cooldown-lock:";
Expand Down Expand Up @@ -81,19 +83,33 @@ public virtual async Task<AIRateLimitStateDto> GetStateAsync()
{
if (currentUser.Id is not Guid userId)
{
return new AIRateLimitStateDto { RetryAfterSeconds = 0 };
return new AIRateLimitStateDto { RetryAfterSeconds = 0, IsGenerating = false };
}

var userLock = distributedLockProvider.CreateLock(CooldownLockPrefix + userId);
using (await userLock.AcquireAsync())
{
return new AIRateLimitStateDto
{
RetryAfterSeconds = await GetRemainingSecondsAsync(userId)
RetryAfterSeconds = await GetRemainingSecondsAsync(userId),
IsGenerating = await HasActiveGenerationAsync()
};
}
}

private async Task<bool> HasActiveGenerationAsync()
{
foreach (var provider in generationActivityProviders)
{
if (await provider.HasActiveGenerationAsync())
{
return true;
}
}

return false;
}

private async Task<int> GetRemainingSecondsAsync(Guid userId)
{
var raw = await cache.GetStringAsync(KeyFor(userId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public override void Map(Worksheet source, WorksheetDto destination)
destination.Version = source.Version;
destination.Published = source.Published;
destination.ReportViewName = source.ReportViewName;
destination.IsArchived = source.IsArchived;
destination.Sections = source.Sections?
.Select(s => new WorksheetSectionMapper().Map(s))
.ToList() ?? [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ public enum PaymentRequestStatus
Paid = 10,
Failed = 11,
FSB = 12, // Financial Services Branch - Prevent CAS Payment
HistoricalPayment = 13,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;

namespace Unity.Payments.PaymentRequests
{
#pragma warning disable CS8618
[Serializable]
public class CreateHistoricalPaymentRequestDto
{
public string InvoiceNumber { get; set; }
public decimal Amount { get; set; }
public string? Description { get; set; }
public Guid CorrelationId { get; set; }
public Guid? SiteId { get; set; }
public string PayeeName { get; set; }
public string ContractNumber { get; set; }
public string? SupplierNumber { get; set; }
public string? SupplierName { get; set; }
public string CorrelationProvider { get; set; } = string.Empty;
public string BatchName { get; set; }
public decimal BatchNumber { get; set; } = 0;
public string ReferenceNumber { get; set; } = string.Empty;
public string SubmissionConfirmationCode { get; set; } = string.Empty;
public Guid? AccountCodingId { get; set; }
public string? Note { get; set; }
public string PaidDate { get; set; }
}
#pragma warning restore CS8618
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Unity.Payments.PaymentRequests
public interface IPaymentRequestAppService : IApplicationService
{
Task<List<PaymentRequestDto>> CreateAsync(List<CreatePaymentRequestDto> paymentRequests);
Task<List<PaymentRequestDto>> CreateHistoricalAsync(List<CreateHistoricalPaymentRequestDto> paymentRequests);
Task<PagedResultDto<PaymentRequestDto>> GetListAsync(PagedAndSortedResultRequestDto input);
Task<decimal> GetTotalPaymentRequestAmountByCorrelationIdAsync(Guid correlationId);
Task<List<PaymentDetailsDto>> GetListByApplicationIdAsync(Guid applicationId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ public static class ErrorConsts
public const string InvalidAccountCodingField = "Unity.Payments:Errors:InvalidAccountCodingFiled";
public const string L2ApproverRestriction = "Unity.Payments:Errors:L2ApproverRestriction";
public const string MissingSupplierNumber = "Unity.Payments:Errors:MissingSupplierNumber";
public const string MissingSite = "Unity.Payments:Errors:MissingSite";
public const string MissingAccountCoding = "Unity.Payments:Errors:MissingAccountCoding";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Volo.Abp;
using Unity.Payments.Domain.Exceptions;
using Unity.Payments.PaymentRequests;
using Unity.Payments.Codes;
using Unity.Payments.Domain.PaymentTags;
using Unity.Payments.Domain.AccountCodings;

Expand All @@ -17,14 +18,8 @@ namespace Unity.Payments.Domain.PaymentRequests
public class PaymentRequest : FullAuditedAggregateRoot<Guid>, IMultiTenant, ICorrelationEntity
{
public Guid? TenantId { get; set; }
public virtual Guid SiteId { get; set; }
public virtual Site Site
{
set => _site = value;
get => _site
?? throw new InvalidOperationException("Uninitialized property: " + nameof(Site));
}
private Site? _site;
public virtual Guid? SiteId { get; set; }
public virtual Site? Site { get; set; }

public virtual string InvoiceNumber { get; private set; } = string.Empty;
public virtual decimal Amount { get; private set; }
Expand Down Expand Up @@ -110,6 +105,35 @@ public PaymentRequest(Guid id, CreatePaymentRequestDto createPaymentRequestDto)
ValidatePaymentRequest();
}

public PaymentRequest(Guid id, CreateHistoricalPaymentRequestDto dto) : base(id)
{
InvoiceNumber = dto.InvoiceNumber;
Amount = dto.Amount;
PayeeName = dto.PayeeName;
ContractNumber = dto.ContractNumber;
SupplierNumber = dto.SupplierNumber ?? string.Empty;
SupplierName = dto.SupplierName;
SiteId = dto.SiteId;
Description = dto.Description;
CorrelationId = dto.CorrelationId;
CorrelationProvider = dto.CorrelationProvider;
ReferenceNumber = dto.ReferenceNumber;
SubmissionConfirmationCode = dto.SubmissionConfirmationCode;
BatchName = dto.BatchName;
BatchNumber = dto.BatchNumber;
AccountCodingId = dto.AccountCodingId;
Note = dto.Note;
Status = PaymentRequestStatus.HistoricalPayment;
PaymentStatus = CasPaymentRequestStatus.Paid;
InvoiceStatus = CasPaymentRequestStatus.Paid;
PaymentDate = dto.PaidDate;
ExpenseApprovals = [];
PaymentTags = null;

if (Amount <= 0)
throw new BusinessException(ErrorConsts.ZeroPayment);
}

public PaymentRequest SetNote(string note)
{
Note = note;
Expand Down Expand Up @@ -210,6 +234,16 @@ public PaymentRequest ValidatePaymentRequest()
throw new BusinessException(ErrorConsts.MissingSupplierNumber);
}

if (!SiteId.HasValue || SiteId.Value == Guid.Empty)
{
throw new BusinessException(ErrorConsts.MissingSite);
}

if (!AccountCodingId.HasValue || AccountCodingId.Value == Guid.Empty)
{
throw new BusinessException(ErrorConsts.MissingAccountCoding);
}

return this;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public class InvoiceManager(
{
public async Task<Site?> GetSiteByPaymentRequestAsync(PaymentRequest paymentRequest)
{
Site? site = await siteRepository.GetAsync(paymentRequest.SiteId, true);
if (!paymentRequest.SiteId.HasValue) return null;
Site? site = await siteRepository.GetAsync(paymentRequest.SiteId.Value, true);
if (site?.SupplierId != null)
{
Supplier supplier = await supplierRepository.GetAsync(site.SupplierId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,11 @@ public async Task ManuallyAddPaymentRequestsToReconciliationQueueAsync(List<Guid
if (paymentRequest != null)
{
var paymentRequestDto = objectMapper.Map<PaymentRequest, PaymentRequestDto>(paymentRequest);
Site site = await siteRepository.GetAsync(paymentRequest.SiteId);
paymentRequestDto.Site = objectMapper.Map<Site, SiteDto>(site);
if (paymentRequest.SiteId.HasValue)
{
Site site = await siteRepository.GetAsync(paymentRequest.SiteId.Value);
paymentRequestDto.Site = objectMapper.Map<Site, SiteDto>(site);
}
paymentRequestDtos.Add(paymentRequestDto);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public static void ConfigurePayments(
b.HasOne(e => e.Site)
.WithMany()
.HasForeignKey(x => x.SiteId)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);

b.HasOne(e => e.AccountCoding)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,9 @@ public async Task<List<ApplicationPaymentRollupDto>> GetBatchPaymentRollupsByCor
{
ApplicationId = g.Key,
TotalPaid = g
.Where(p => p.PaymentStatus != null
&& p.PaymentStatus.Trim().ToUpper() == CasPaymentRequestStatus.FullyPaid.ToUpper())
.Where(p => (p.PaymentStatus != null
&& p.PaymentStatus.Trim().ToUpper() == CasPaymentRequestStatus.FullyPaid.ToUpper())
|| p.Status == PaymentRequestStatus.HistoricalPayment)
.Sum(p => p.Amount),
TotalPending = g
.Where(p => p.Status == PaymentRequestStatus.L1Pending
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ string GetProp(string name) =>
string supplierprotected = GetProp("supplierprotected");
string standardindustryclassification = GetProp("standardindustryclassification");

_ = DateTime.TryParse(lastUpdated, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime lastUpdatedDate);
DateTime? lastUpdatedDate = DateTime.TryParse(lastUpdated, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime parsedLastUpdated)
? parsedLastUpdated
: null;

var siteEtos = new List<SiteEto>();
if (casSupplierResponse.TryGetProperty("supplieraddress", out var sitesJson) &&
Expand Down Expand Up @@ -181,7 +183,9 @@ public static SiteEto GetSiteEto(JsonElement site)
? new string('*', accountNumber.Length - 4) + accountNumber[^4..]
: accountNumber;
string siteLastUpdated = GetJsonProperty("lastupdated", site);
_ = DateTime.TryParse(siteLastUpdated, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime siteLastUpdatedDate);
DateTime? siteLastUpdatedDate = DateTime.TryParse(siteLastUpdated, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime parsedSiteLastUpdated)
? parsedSiteLastUpdated
: null;

return new SiteEto
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public async Task AddPaymentRequestsToInvoiceQueue(PaymentRequest paymentRequest
PaymentRequestId = paymentRequest.Id,
InvoiceNumber = paymentRequest.InvoiceNumber,
SupplierNumber = paymentRequest.SupplierNumber,
SiteNumber = paymentRequest.Site.Number,
SiteNumber = paymentRequest.Site?.Number ?? string.Empty,
TenantId = (Guid)currentTenant.Id
};

Expand Down Expand Up @@ -103,7 +103,7 @@ public async Task AddPaymentRequestsToReconciliationQueue()
PaymentRequestId = paymentRequest.Id,
InvoiceNumber = paymentRequest.InvoiceNumber,
SupplierNumber = paymentRequest.SupplierNumber,
SiteNumber = paymentRequest.Site.Number,
SiteNumber = paymentRequest.Site?.Number ?? string.Empty,
TenantId = tenantId
};

Expand Down
Loading
Loading