Skip to content
Open
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
1,304 changes: 1,304 additions & 0 deletions docs/superpowers/plans/2026-05-14-vertical-slice-notifications-pilot.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using SimpleModule.Core;

namespace SimpleModule.Notifications.Contracts;
namespace SimpleModule.Notifications.Contracts.Features.Notifications.List;

[Dto]
public class QueryNotificationsRequest
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using SimpleModule.Core;
using SimpleModule.Notifications.Contracts.Features.Notifications.List;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications.Contracts;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;
using SimpleModule.Notifications.Contracts;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications.Infrastructure;

public sealed partial class NotificationService
{
public async Task<Notification?> GetByIdAsync(NotificationId id, UserId userId) =>
await db
.Notifications.AsNoTracking()
.FirstOrDefaultAsync(n => n.Id == id && n.UserId == userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
using SimpleModule.Core.Authorization;
using SimpleModule.Core.Extensions;
using SimpleModule.Notifications.Contracts;
using SimpleModule.Notifications.Contracts.Features.Notifications.List;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications.Endpoints.Notifications;
namespace SimpleModule.Notifications.Features.Notifications.List;

public class ListNotificationsEndpoint : IEndpoint
{
Expand All @@ -20,8 +21,7 @@ public void Map(IEndpointRouteBuilder app) =>
[AsParameters] QueryNotificationsRequest request,
HttpContext context,
INotificationsContracts notifications
) =>
notifications.ListAsync(UserId.From(context.User.GetUserId()!), request)
) => notifications.ListAsync(UserId.From(context.User.GetUserId()!), request)
)
.RequirePermission(NotificationsPermissions.ViewOwn);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore;
using SimpleModule.Core;
using SimpleModule.Notifications.Contracts;
using SimpleModule.Notifications.Contracts.Features.Notifications.List;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications.Infrastructure;

public sealed partial class NotificationService
{
public async Task<PagedResult<Notification>> ListAsync(
UserId userId,
QueryNotificationsRequest request
)
{
var query = db.Notifications.AsNoTracking().Where(n => n.UserId == userId);

if (request.UnreadOnly == true)
{
query = query.Where(n => n.ReadAt == null);
}
if (!string.IsNullOrWhiteSpace(request.Channel))
{
query = query.Where(n => n.Channel == request.Channel);
}
if (!string.IsNullOrWhiteSpace(request.Type))
{
query = query.Where(n => n.Type == request.Type);
}

var totalCount = await query.CountAsync();
var page = request.EffectivePage;
var pageSize = request.EffectivePageSize;

var items = await query
.OrderByDescending(n => n.CreatedAt)
.ThenByDescending(n => n.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();

return new PagedResult<Notification>
{
Items = items,
TotalCount = totalCount,
Page = page,
PageSize = pageSize,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using SimpleModule.Notifications.Contracts;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications.Endpoints.Notifications;
namespace SimpleModule.Notifications.Features.Notifications.MarkAllRead;

public class MarkAllReadEndpoint : IEndpoint
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.EntityFrameworkCore;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications.Infrastructure;

public sealed partial class NotificationService
{
public async Task<int> MarkAllReadAsync(UserId userId)
{
var now = DateTimeOffset.UtcNow;
return await db
.Notifications.Where(n => n.UserId == userId && n.ReadAt == null)
.ExecuteUpdateAsync(s => s.SetProperty(n => n.ReadAt, now));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using SimpleModule.Notifications.Contracts;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications.Endpoints.Notifications;
namespace SimpleModule.Notifications.Features.Notifications.MarkRead;

public class MarkReadEndpoint : IEndpoint
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using SimpleModule.Notifications.Contracts;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications.Infrastructure;

public sealed partial class NotificationService
{
public async Task<bool> MarkReadAsync(NotificationId id, UserId userId)
{
var notification = await db.Notifications.FirstOrDefaultAsync(n =>
n.Id == id && n.UserId == userId
);
if (notification is null)
{
return false;
}

if (notification.ReadAt is not null)
{
return true;
}

notification.ReadAt = DateTimeOffset.UtcNow;
await db.SaveChangesAsync();
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications.Infrastructure;

public sealed partial class NotificationService
{
public Task<int> GetUnreadCountAsync(
UserId userId,
CancellationToken cancellationToken = default
) =>
db.Notifications.CountAsync(n => n.UserId == userId && n.ReadAt == null, cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using SimpleModule.Notifications.Contracts;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications.Endpoints.Notifications;
namespace SimpleModule.Notifications.Features.Notifications.UnreadCount;

public class UnreadCountEndpoint : IEndpoint
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using SimpleModule.Notifications.Contracts.Events;
using Wolverine.EntityFrameworkCore;

namespace SimpleModule.Notifications.Channels;
namespace SimpleModule.Notifications.Infrastructure.Channels;

public partial class DatabaseChannel(
NotificationsDbContext db,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using SimpleModule.Notifications.Contracts;

namespace SimpleModule.Notifications.Channels;
namespace SimpleModule.Notifications.Infrastructure.Channels;

/// <summary>
/// A delivery channel — mail, database, sms, slack, push, etc. Each channel
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using SimpleModule.Notifications.Contracts;

namespace SimpleModule.Notifications.Channels;
namespace SimpleModule.Notifications.Infrastructure.Channels;

public interface INotificationChannelRegistry
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Microsoft.Extensions.Logging;
using SimpleModule.Notifications.Contracts;

namespace SimpleModule.Notifications.Channels;
namespace SimpleModule.Notifications.Infrastructure.Channels;

/// <summary>
/// Default SMS channel: writes the message to the log. A real provider (Twilio, etc.)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using SimpleModule.Email.Contracts;
using SimpleModule.Notifications.Contracts;

namespace SimpleModule.Notifications.Channels;
namespace SimpleModule.Notifications.Infrastructure.Channels;

/// <summary>
/// Forwards a notification's mail payload to the Email module. Skips silently when the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SimpleModule.Notifications.Contracts;

namespace SimpleModule.Notifications.EntityConfigurations;
namespace SimpleModule.Notifications.Infrastructure.EntityConfigurations;

public class NotificationConfiguration : IEntityTypeConfiguration<Notification>
{
Expand All @@ -20,7 +20,12 @@ public void Configure(EntityTypeBuilder<Notification> builder)

// Covers the inbox list query (filter UserId + order CreatedAt DESC, Id as tiebreaker)
// and the unread-count query (predicate on UserId + ReadAt).
builder.HasIndex(n => new { n.UserId, n.CreatedAt, n.Id });
builder.HasIndex(n => new
{
n.UserId,
n.CreatedAt,
n.Id,
});
builder.HasIndex(n => new { n.UserId, n.ReadAt });
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using SimpleModule.BackgroundJobs.Contracts;
using SimpleModule.Notifications.Channels;
using SimpleModule.Notifications.Contracts;
using SimpleModule.Notifications.Contracts.Events;
using SimpleModule.Notifications.Services;
using SimpleModule.Notifications.Infrastructure.Channels;
using SimpleModule.Users.Contracts;
using Wolverine;

namespace SimpleModule.Notifications.Jobs;
namespace SimpleModule.Notifications.Infrastructure.Jobs;

public sealed record DispatchNotificationJobData(
string UserId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using SimpleModule.Notifications.Contracts;

namespace SimpleModule.Notifications.Infrastructure;

public sealed partial class NotificationService(NotificationsDbContext db)
: INotificationsContracts { }
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
using Microsoft.Extensions.Options;
using SimpleModule.Database;
using SimpleModule.Notifications.Contracts;
using SimpleModule.Notifications.EntityConfigurations;
using SimpleModule.Notifications.Infrastructure.EntityConfigurations;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications;
namespace SimpleModule.Notifications.Infrastructure;

public class NotificationsDbContext(
DbContextOptions<NotificationsDbContext> options,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Logging;

namespace SimpleModule.Notifications.Services;
namespace SimpleModule.Notifications.Infrastructure;

internal static partial class NotificationsLog
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using Microsoft.Extensions.Logging;
using SimpleModule.BackgroundJobs.Contracts;
using SimpleModule.Notifications.Channels;
using SimpleModule.Notifications.Contracts;
using SimpleModule.Notifications.Contracts.Events;
using SimpleModule.Notifications.Jobs;
using SimpleModule.Notifications.Infrastructure.Channels;
using SimpleModule.Notifications.Infrastructure.Jobs;
using Wolverine;

namespace SimpleModule.Notifications.Services;
namespace SimpleModule.Notifications.Infrastructure;

public class Notifier(
INotificationChannelRegistry channels,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
using SimpleModule.Core;
using SimpleModule.Core.Settings;
using SimpleModule.Database;
using SimpleModule.Notifications.Channels;
using SimpleModule.Notifications.Contracts;
using SimpleModule.Notifications.Jobs;
using SimpleModule.Notifications.Services;
using SimpleModule.Notifications.Infrastructure;
using SimpleModule.Notifications.Infrastructure.Channels;
using SimpleModule.Notifications.Infrastructure.Jobs;

namespace SimpleModule.Notifications;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using SimpleModule.Core.Extensions;
using SimpleModule.Core.Inertia;
using SimpleModule.Notifications.Contracts;
using SimpleModule.Notifications.Contracts.Features.Notifications.List;
using SimpleModule.Users.Contracts;

namespace SimpleModule.Notifications.Pages;
Expand Down
Loading
Loading