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
7 changes: 7 additions & 0 deletions Compendium.sln
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Architecture", "Architectur
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.ArchitectureTests", "tests\Architecture\Compendium.ArchitectureTests\Compendium.ArchitectureTests.csproj", "{D685E8D8-B3DC-4A65-9ED3-675776610190}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Adapters.Shared", "src\Adapters\Compendium.Adapters.Shared\Compendium.Adapters.Shared.csproj", "{DE527E82-7509-4E3F-B002-8D53CFAA97DA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -232,6 +234,10 @@ Global
{D685E8D8-B3DC-4A65-9ED3-675776610190}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D685E8D8-B3DC-4A65-9ED3-675776610190}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D685E8D8-B3DC-4A65-9ED3-675776610190}.Release|Any CPU.Build.0 = Release|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{FE421F00-7FFD-4666-A961-F1FF325ECD34} = {E35C8F52-5000-4427-9589-AEB5987C1AC6}
Expand Down Expand Up @@ -278,5 +284,6 @@ Global
{C4495E66-500F-4582-B56D-C30ED0F41FF3} = {A0005488-4247-4872-9CD5-95696E47BEA5}
{72B1C880-12A5-4568-85E0-3800536158DC} = {B23A5693-C266-43DC-8D3E-CBB108131762}
{D685E8D8-B3DC-4A65-9ED3-675776610190} = {72B1C880-12A5-4568-85E0-3800536158DC}
{DE527E82-7509-4E3F-B002-8D53CFAA97DA} = {73261E87-8FCA-40B6-940B-E25CBDBE33FB}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ private async Task<Result<JsonApiResource<T>>> GetResourceAsync<T>(
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in LemonSqueezy GetResourceAsync");
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand All @@ -307,7 +307,7 @@ private async Task<Result<List<JsonApiResource<T>>>> GetCollectionAsync<T>(
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in LemonSqueezy GetCollectionAsync");
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand Down Expand Up @@ -343,7 +343,7 @@ private async Task<Result<JsonApiResource<TResponse>>> PostResourceAsync<TReques
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in LemonSqueezy PostResourceAsync");
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand Down Expand Up @@ -379,7 +379,7 @@ private async Task<Result<JsonApiResource<TResponse>>> PatchResourceAsync<TReque
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in LemonSqueezy PatchResourceAsync");
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand All @@ -401,7 +401,7 @@ private async Task<Result> DeleteAsync(
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in LemonSqueezy DeleteAsync");
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand Down Expand Up @@ -432,7 +432,7 @@ private async Task<Result<TResponse>> PostLicenseApiAsync<TRequest, TResponse>(
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy License API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in LemonSqueezy License PostAsync");
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// </copyright>
// -----------------------------------------------------------------------

using System.Diagnostics;
using Compendium.Adapters.LemonSqueezy.Configuration;
using Compendium.Adapters.LemonSqueezy.Http;
using Compendium.Adapters.LemonSqueezy.Http.Models;
Expand Down Expand Up @@ -130,7 +131,7 @@ public async Task<Result<BillingCustomer>> GetCustomerByEmailAsync(
{
ArgumentNullException.ThrowIfNull(email);

_logger.LogDebug("Getting customer by email {Email}", email);
_logger.LogDebug("Getting customer by email (activity {ActivityId})", Activity.Current?.Id);

var result = await _httpClient.ListCustomersAsync(email, cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ private async Task<Result<T>> GetAsync<T>(
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in Listmonk GetAsync");
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand All @@ -366,7 +366,7 @@ private async Task<Result<List<T>>> GetListAsync<T>(
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in Listmonk GetListAsync");
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand All @@ -391,7 +391,7 @@ private async Task<Result<ListmonkPaginatedData<T>>> GetPaginatedAsync<T>(
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in Listmonk GetPaginatedAsync");
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand Down Expand Up @@ -423,7 +423,7 @@ private async Task<Result<TResponse>> PostAsync<TRequest, TResponse>(
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in Listmonk PostAsync");
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand Down Expand Up @@ -455,7 +455,7 @@ private async Task<Result<TResponse>> PutAsync<TRequest, TResponse>(
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in Listmonk PutAsync");
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand All @@ -479,7 +479,7 @@ private async Task<Result> PutAsync<TRequest>(
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in Listmonk PutAsync (no response)");
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand All @@ -501,7 +501,7 @@ private async Task<Result> DeleteAsync(
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error in Listmonk DeleteAsync");
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// </copyright>
// -----------------------------------------------------------------------

using System.Diagnostics;
using Compendium.Adapters.Listmonk.Configuration;
using Compendium.Adapters.Listmonk.Http;
using Compendium.Adapters.Listmonk.Http.Models;
Expand Down Expand Up @@ -40,7 +41,7 @@ public async Task<Result<Subscriber>> SubscribeAsync(
{
ArgumentNullException.ThrowIfNull(request);

_logger.LogInformation("Subscribing {Email} to newsletter", request.Email);
_logger.LogInformation("Subscribing to newsletter (activity {ActivityId})", Activity.Current?.Id);

var listIds = new List<int>();

Expand Down Expand Up @@ -74,12 +75,12 @@ public async Task<Result<Subscriber>> SubscribeAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to subscribe {Email}: {Error}", request.Email, result.Error.Message);
_logger.LogWarning("Failed to subscribe (activity {ActivityId}): {Error}", Activity.Current?.Id, result.Error.Message);
return result.Error;
}

var subscriber = MapToSubscriber(result.Value);
_logger.LogInformation("Subscribed {Email} with ID {SubscriberId}", request.Email, subscriber.Id);
_logger.LogInformation("Subscribed with ID {SubscriberId}", subscriber.Id);

return subscriber;
}
Expand All @@ -92,7 +93,7 @@ public async Task<Result> UnsubscribeAsync(
{
ArgumentNullException.ThrowIfNull(email);

_logger.LogInformation("Unsubscribing {Email} from list {ListId}", email, listId ?? "all");
_logger.LogInformation("Unsubscribing from list {ListId} (activity {ActivityId})", listId ?? "all", Activity.Current?.Id);

// First, find the subscriber by email
var subscriberResult = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken);
Expand All @@ -111,12 +112,12 @@ public async Task<Result> UnsubscribeAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to unsubscribe {Email} from list {ListId}: {Error}",
email, listId, result.Error.Message);
_logger.LogWarning("Failed to unsubscribe {SubscriberId} from list {ListId}: {Error}",
subscriberId, listId, result.Error.Message);
}
else
{
_logger.LogInformation("Unsubscribed {Email} from list {ListId}", email, listId);
_logger.LogInformation("Unsubscribed {SubscriberId} from list {ListId}", subscriberId, listId);
}

return result;
Expand All @@ -133,11 +134,11 @@ public async Task<Result> UnsubscribeAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to unsubscribe {Email}: {Error}", email, result.Error.Message);
_logger.LogWarning("Failed to unsubscribe {SubscriberId}: {Error}", subscriberId, result.Error.Message);
}
else
{
_logger.LogInformation("Unsubscribed {Email} from all lists", email);
_logger.LogInformation("Unsubscribed {SubscriberId} from all lists", subscriberId);
}

return result.IsSuccess ? Result.Success() : result.Error;
Expand All @@ -151,7 +152,7 @@ public async Task<Result<Subscriber>> GetSubscriberAsync(
{
ArgumentNullException.ThrowIfNull(email);

_logger.LogDebug("Getting subscriber by email {Email}", email);
_logger.LogDebug("Getting subscriber by email (activity {ActivityId})", Activity.Current?.Id);

var result = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken);

Expand All @@ -172,7 +173,7 @@ public async Task<Result> UpdateSubscriberAttributesAsync(
ArgumentNullException.ThrowIfNull(email);
ArgumentNullException.ThrowIfNull(attributes);

_logger.LogInformation("Updating attributes for subscriber {Email}", email);
_logger.LogInformation("Updating subscriber attributes (activity {ActivityId})", Activity.Current?.Id);

// First, find the subscriber by email
var subscriberResult = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken);
Expand All @@ -191,12 +192,12 @@ public async Task<Result> UpdateSubscriberAttributesAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to update attributes for {Email}: {Error}",
email, result.Error.Message);
_logger.LogWarning("Failed to update attributes for {SubscriberId}: {Error}",
subscriberResult.Value.Id, result.Error.Message);
}
else
{
_logger.LogInformation("Updated attributes for subscriber {Email}", email);
_logger.LogInformation("Updated attributes for subscriber {SubscriberId}", subscriberResult.Value.Id);
}

return result.IsSuccess ? Result.Success() : result.Error;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>Compendium.Adapters.Shared</AssemblyName>
<RootNamespace>Compendium.Adapters.Shared</RootNamespace>
<PackageId>Compendium.Adapters.Shared</PackageId>
<Description>Shared utilities for Compendium adapters: PII masking helpers, logging conventions.</Description>
</PropertyGroup>

</Project>
30 changes: 30 additions & 0 deletions src/Adapters/Compendium.Adapters.Shared/Logging/PiiMasking.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// -----------------------------------------------------------------------
// <copyright file="PiiMasking.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

namespace Compendium.Adapters.Shared.Logging;

/// <summary>
/// PII masking helpers for log statements. Use sparingly — prefer non-PII identifiers
/// (subscriber_id, customer_id, activity_id) over masked PII per GDPR data-minimization.
/// </summary>
public static class PiiMasking
{
/// <summary>
/// Masks an email for logging: "john.doe@acme.com" → "j***@acme.com".
/// Returns "&lt;empty&gt;" or "&lt;null&gt;" for non-values.
/// Use only when subscriber_id/customer_id is unavailable AND email correlation is required for debugging.
/// </summary>
/// <param name="email">The email to mask.</param>
/// <returns>Masked email, or a placeholder for empty/invalid input.</returns>
public static string MaskEmail(string? email)
{
if (string.IsNullOrWhiteSpace(email)) return "<empty>";
var atIndex = email.IndexOf('@');
if (atIndex <= 0) return "***";
return $"{email[0]}***{email[atIndex..]}";
Comment on lines +17 to +28
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

The XML docs say this returns "" or "" for non-values, but the implementation returns "" for both null and whitespace, and returns "***" for invalid formats (no '@'). Please align the docs with the actual behavior, or update the implementation to distinguish null vs empty (and document the invalid-email case).

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// </copyright>
// -----------------------------------------------------------------------

using System.Diagnostics;
using Compendium.Adapters.Stripe.Configuration;
using Stripe.Checkout;

Expand Down Expand Up @@ -131,7 +132,7 @@ public async Task<Result<BillingCustomer>> GetCustomerByEmailAsync(
}
catch (StripeException ex)
{
_logger.LogError(ex, "Stripe customer lookup by email failed for {Email}", email);
_logger.LogError(ex, "Stripe customer lookup by email failed (activity {ActivityId})", Activity.Current?.Id);
return Result.Failure<BillingCustomer>(
Error.Failure("Billing.Stripe.GetCustomerByEmailFailed", ex.Message));
}
Expand Down Expand Up @@ -184,7 +185,7 @@ public async Task<Result<BillingCustomer>> UpsertCustomerAsync(
}
catch (StripeException ex)
{
_logger.LogError(ex, "Stripe customer upsert failed for {Email}", request.Email);
_logger.LogError(ex, "Stripe customer upsert failed (activity {ActivityId})", Activity.Current?.Id);
return Result.Failure<BillingCustomer>(
Error.Failure("Billing.Stripe.UpsertCustomerFailed", ex.Message));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public async Task<Result<IdentityUser>> GetUserByEmailAsync(

// POM-170: debug logs also flow into centralised log stores, so use the
// short hash here as well.
_logger.LogDebug("Getting user by email (hash {EmailHashPrefix})", HashPrefix(email));
_logger.LogDebug("Getting user by email (hash {HashPrefix})", HashPrefix(email));
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

This placeholder was renamed to {HashPrefix} here, but the same file still contains log templates using {EmailHashPrefix} (e.g., in CreateUserAsync). That leaves {Email placeholders in adapter logs and contradicts the PR description/verification grep. Please update the remaining templates to use {HashPrefix} as well for consistency and to keep the grep clean.

Copilot uses AI. Check for mistakes.

var searchRequest = new ZitadelUserSearchRequest
{
Expand Down
Loading