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
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using MassTransit;
using Microsoft.EntityFrameworkCore;
using DonationService.Data;
using DonationService.Entities;
using Glense.Shared.Messages;

namespace DonationService.Consumers
{
public class UserRegisteredEventConsumer : IConsumer<UserRegisteredEvent>
{
private readonly DonationDbContext _context;
private readonly ILogger<UserRegisteredEventConsumer> _logger;

public UserRegisteredEventConsumer(
DonationDbContext context,
ILogger<UserRegisteredEventConsumer> logger)
{
_context = context;
_logger = logger;
}

public async Task Consume(ConsumeContext<UserRegisteredEvent> context)
{
var msg = context.Message;
_logger.LogInformation(
"Received UserRegisteredEvent: UserId={UserId}, Username={Username}",
msg.UserId, msg.Username);

try
{
// Check if wallet already exists (idempotency)
var existingWallet = await _context.Wallets
.FirstOrDefaultAsync(w => w.UserId == msg.UserId);

if (existingWallet != null)
{
_logger.LogInformation(
"Wallet already exists for user {UserId}, skipping creation", msg.UserId);
return;
}

var wallet = new Wallet
{
Id = Guid.NewGuid(),
UserId = msg.UserId,
Balance = 0,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};

_context.Wallets.Add(wallet);
await _context.SaveChangesAsync();

_logger.LogInformation(
"Wallet created for user {UserId} via UserRegisteredEvent", msg.UserId);
}
catch (DbUpdateException ex)
{
// Handle race condition - wallet may have been created by another consumer instance
_logger.LogWarning(ex,
"DbUpdateException creating wallet for user {UserId}, likely already exists", msg.UserId);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to create wallet for user {UserId}", msg.UserId);
throw;
}
}
}
}
30 changes: 20 additions & 10 deletions Glense.Server/DonationService/Controllers/DonationController.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System.Security.Claims;
using MassTransit;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using DonationService.Data;
using DonationService.Entities;
using DonationService.DTOs;
using Glense.Shared.Messages;
using DonationService.Services;

namespace DonationService.Controllers;
Expand All @@ -18,15 +20,18 @@ public class DonationController : ControllerBase
private readonly DonationDbContext _context;
private readonly ILogger<DonationController> _logger;
private readonly IAccountServiceClient _accountService;
private readonly IPublishEndpoint _publishEndpoint;

public DonationController(
DonationDbContext context,
ILogger<DonationController> logger,
IAccountServiceClient accountService)
IAccountServiceClient accountService,
IPublishEndpoint publishEndpoint)
{
_context = context;
_logger = logger;
_accountService = accountService;
_publishEndpoint = publishEndpoint;
}

/// <summary>
Expand Down Expand Up @@ -84,13 +89,15 @@ public async Task<ActionResult<DonationResponse>> CreateDonation([FromBody] Crea
return BadRequest(new { message = "Cannot donate to yourself" });
}

// Validate recipient exists in Account service
// Fetch both usernames upfront to validate and reuse later for the event
var recipientUsername = await _accountService.GetUsernameAsync(request.RecipientUserId);
if (recipientUsername == null)
{
return BadRequest(new { message = "Recipient user not found" });
}

var donorUsername = await _accountService.GetUsernameAsync(request.DonorUserId) ?? "Someone";

// Check if we can use transactions (not supported by in-memory database)
var supportsTransactions = !_context.Database.IsInMemory();
var transaction = supportsTransactions
Expand Down Expand Up @@ -160,20 +167,23 @@ public async Task<ActionResult<DonationResponse>> CreateDonation([FromBody] Crea
"Donation created: {DonationId}, from user {DonorId} to user {RecipientId}, amount: {Amount}",
donation.Id, request.DonorUserId, request.RecipientUserId, request.Amount);

// Send notification to recipient (fire-and-forget, don't fail the donation)
// Publish DonationMadeEvent so Account Service creates the notification asynchronously
try
{
var donorUsername = await _accountService.GetUsernameAsync(request.DonorUserId) ?? "Someone";
await _accountService.CreateDonationNotificationAsync(
request.RecipientUserId,
donorUsername,
request.Amount,
donation.Id);
await _publishEndpoint.Publish(new DonationMadeEvent
{
DonorId = request.DonorUserId,
RecipientId = request.RecipientUserId,
Amount = request.Amount,
DonorUsername = donorUsername
});
_logger.LogInformation(
"Published DonationMadeEvent for donation {DonationId}", donation.Id);
}
catch (Exception notifEx)
{
_logger.LogWarning(notifEx,
"Failed to send donation notification for donation {DonationId}",
"Failed to publish DonationMadeEvent for donation {DonationId}",
donation.Id);
}

Expand Down
1 change: 1 addition & 0 deletions Glense.Server/DonationService/DonationService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.12" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="MassTransit.RabbitMQ" Version="8.2.0" />
</ItemGroup>

</Project>
Expand Down
10 changes: 10 additions & 0 deletions Glense.Server/DonationService/Messages/DonationMadeEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Glense.Shared.Messages
{
public class DonationMadeEvent
{
public Guid DonorId { get; set; }
public Guid RecipientId { get; set; }
public decimal Amount { get; set; }
public string DonorUsername { get; set; } = string.Empty;
}
}
9 changes: 9 additions & 0 deletions Glense.Server/DonationService/Messages/UserRegisteredEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Glense.Shared.Messages
{
public class UserRegisteredEvent
{
public Guid UserId { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
}
25 changes: 24 additions & 1 deletion Glense.Server/DonationService/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using MassTransit;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using DonationService.Consumers;
using DonationService.Data;
using DonationService.Services;

Expand Down Expand Up @@ -33,7 +35,7 @@
Console.WriteLine("[WARNING] No connection string found, using in-memory database");
}

// HttpClient for Account Service
// HttpClient for Account Service (kept for synchronous profile lookup/validation)
builder.Services.AddHttpClient("AccountService", client =>
{
var serviceUrl = Environment.GetEnvironmentVariable("ACCOUNT_SERVICE_URL")
Expand All @@ -44,6 +46,27 @@

builder.Services.AddScoped<IAccountServiceClient, AccountServiceClient>();

// Configure MassTransit with RabbitMQ
var rabbitHost = builder.Configuration["RabbitMQ:Host"] ?? "localhost";
var rabbitUser = builder.Configuration["RabbitMQ:Username"] ?? "guest";
var rabbitPass = builder.Configuration["RabbitMQ:Password"] ?? "guest";

builder.Services.AddMassTransit(x =>
{
x.AddConsumer<UserRegisteredEventConsumer>();

x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(rabbitHost, "/", h =>
{
h.Username(rabbitUser);
h.Password(rabbitPass);
});

cfg.ConfigureEndpoints(context);
});
});

// JWT Authentication
var jwtIssuer = builder.Configuration["JwtSettings:Issuer"] ?? "GlenseAccountService";
var jwtAudience = builder.Configuration["JwtSettings:Audience"] ?? "GlenseApp";
Expand Down
37 changes: 13 additions & 24 deletions Glense.Server/DonationService/Services/AccountServiceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,22 @@ public AccountServiceClient(IHttpClientFactory httpClientFactory, ILogger<Accoun

public async Task<string?> GetUsernameAsync(Guid userId)
{
var response = await _httpClient.GetAsync($"/api/profile/{userId}");

if (response.StatusCode == HttpStatusCode.NotFound)
return null;
try
{
var response = await _httpClient.GetAsync($"/api/profile/{userId}");

response.EnsureSuccessStatusCode();
if (response.StatusCode == HttpStatusCode.NotFound)
return null;

var json = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
return json.TryGetProperty("username", out var username) ? username.GetString() : null;
}
response.EnsureSuccessStatusCode();

public async Task CreateDonationNotificationAsync(
Guid recipientUserId,
string donorUsername,
decimal amount,
Guid donationId)
{
var request = new
var json = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
return json.TryGetProperty("username", out var username) ? username.GetString() : null;
}
catch (Exception ex)
{
UserId = recipientUserId,
Title = "New Donation!",
Message = $"{donorUsername} donated ${amount:F2} to you!",
Type = "donation",
RelatedEntityId = donationId
};

var response = await _httpClient.PostAsJsonAsync("/api/internal/notifications", request);
response.EnsureSuccessStatusCode();
_logger.LogError(ex, "Failed to get username for user {UserId} from Account Service", userId);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ namespace DonationService.Services;
public interface IAccountServiceClient
{
/// <summary>
/// Gets the username for a user. Returns null if the user doesn't exist.
/// Gets the username for a user via HTTP. Returns null if the user doesn't exist.
/// This is kept as HTTP for synchronous profile validation during donations.
/// </summary>
Task<string?> GetUsernameAsync(Guid userId);

Task CreateDonationNotificationAsync(Guid recipientUserId, string donorUsername, decimal amount, Guid donationId);
}
5 changes: 5 additions & 0 deletions Glense.Server/DonationService/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@
"AllowedHosts": "*",
"ConnectionStrings": {
"DonationDb": ""
},
"RabbitMQ": {
"Host": "localhost",
"Username": "guest",
"Password": "guest"
}
}
Loading
Loading