| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- using Application.Abstractions.Data;
- using Application.Abstractions.Messaging;
- using SharedKernel.Results;
- using Domain.Entities.Donations;
- using Domain.Entities.Donations.ValueObject;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Features.Api.DonationRemote.TestDonate;
- internal sealed class Handler(IAppDbContext db) : ICommandHandler<Command, Result<Response>>
- {
- public async Task<Result<Response>> Handle(Command r, CancellationToken ct)
- {
- if (r.Amount <= 0)
- {
- return Result.Failure<Response>(Error.Problem("Donation.InvalidAmount", "금액은 0보다 커야 합니다."));
- }
- if (r.Amount < DonationConstants.MinAmount)
- {
- return Result.Failure<Response>(Error.Problem("Donation.BelowMinAmount", $"최소 후원 금액은 {DonationConstants.MinAmount}원입니다."));
- }
- if (r.Amount > DonationConstants.MaxAmount)
- {
- return Result.Failure<Response>(Error.Problem("Donation.AboveMaxAmount", $"최대 후원 금액은 {DonationConstants.MaxAmount:N0}원입니다."));
- }
- if (string.IsNullOrWhiteSpace(r.SendName))
- {
- return Result.Failure<Response>(Error.Problem("Donation.SendNameRequired", "별명이 필요합니다."));
- }
- var channel = await db.Channel.AsNoTracking()
- .Where(c => c.WidgetToken == r.WidgetToken && c.IsActive)
- .Select(c => new { c.ID, c.MemberID, c.PlatformFeeRate, c.Name })
- .FirstOrDefaultAsync(ct);
- if (channel is null)
- {
- return Result.Failure<Response>(Error.NotFound("Channel.NotFound", "채널을 찾을 수 없습니다."));
- }
- if (channel.MemberID != r.RequesterMemberID)
- {
- return Result.Failure<Response>(Error.Forbidden("Remote.Forbidden", "권한이 없습니다."));
- }
- // 크루 후원 검증 + 활성 세션 조회 (영구 반영 시에만 세션 필요)
- int? crewSessionID = null;
- string? crewMemberNickname = null;
- if (r.CrewMemberID.HasValue)
- {
- var crewMember = await db.CrewMember.AsNoTracking()
- .Where(m => m.ID == r.CrewMemberID.Value && m.IsActive)
- .Select(m => new { m.ID, m.CrewID, m.Nickname, CrewChannelID = m.Crew!.ChannelID })
- .FirstOrDefaultAsync(ct);
- if (crewMember is null)
- {
- return Result.Failure<Response>(Error.NotFound("CrewMember.NotFound", "크루원을 찾을 수 없습니다."));
- }
- if (crewMember.CrewChannelID != channel.ID)
- {
- return Result.Failure<Response>(Error.Forbidden("CrewMember.Forbidden", "다른 채널의 크루원입니다."));
- }
- crewMemberNickname = crewMember.Nickname;
- if (!r.IsTest)
- {
- crewSessionID = await db.CrewSession.AsNoTracking()
- .Where(s => s.CrewID == crewMember.CrewID && s.Status == CrewSessionStatus.Active)
- .OrderByDescending(s => s.CreatedAt)
- .Select(s => (int?)s.ID)
- .FirstOrDefaultAsync(ct);
- }
- }
- // Donation 생성 (Wallet/PG 우회 — Send Handler와 다른 흐름)
- var donation = Domain.Entities.Donations.Donation.Create(
- sponsorMemberID: channel.MemberID,
- receiverMemberID: channel.MemberID,
- channelID: channel.ID,
- amount: r.Amount,
- feeRate: channel.PlatformFeeRate,
- message: r.Message,
- sendName: r.SendName,
- crewSessionID: crewSessionID,
- crewMemberID: r.CrewMemberID,
- isTest: r.IsTest
- );
- db.Donation.Add(donation);
- await db.SaveChangesAsync(ct);
- // DonationAlert 생성
- var alert = Domain.Entities.Donations.DonationAlert.Create(
- donationID: donation.ID,
- sponsorMemberID: channel.MemberID,
- receiverMemberID: channel.MemberID
- );
- db.DonationAlert.Add(alert);
- await db.SaveChangesAsync(ct);
- // 첫 Alert Attempt 기록 (재전송 일관성)
- var firstAttempt = DonationAlertAttempt.Create(donation.ID, alert.ID);
- db.DonationAlertAttempt.Add(firstAttempt);
- // 영구 반영(IsTest=false)일 때만 크루 집계 갱신 — 테스트 모드는 SignalR로만 broadcast
- if (!r.IsTest && crewSessionID.HasValue && r.CrewMemberID.HasValue)
- {
- var summary = await db.CrewDonationSummary
- .FirstOrDefaultAsync(s => s.CrewSessionID == crewSessionID.Value && s.CrewMemberID == r.CrewMemberID.Value, ct);
- if (summary is null)
- {
- summary = CrewDonationSummary.Create(crewSessionID.Value, r.CrewMemberID.Value);
- db.CrewDonationSummary.Add(summary);
- }
- summary.AddDonation(r.Amount);
- var session = await db.CrewSession.FindAsync([crewSessionID.Value], ct);
- session?.AddDonation(r.Amount);
- if (session is not null && session.TotalAmount > 0)
- {
- var allSummaries = await db.CrewDonationSummary
- .Where(s => s.CrewSessionID == session.ID)
- .OrderByDescending(s => s.TotalAmount)
- .ToListAsync(ct);
- var rank = 1;
- foreach (var s in allSummaries)
- {
- var rate = (decimal)s.TotalAmount / session.TotalAmount * 100;
- s.UpdateContribution(rate, rank++);
- }
- }
- }
- await db.SaveChangesAsync(ct);
- return Result.Success(new Response(
- donation.ID,
- alert.ID,
- alert.CorrelationID,
- donation.Amount,
- donation.FeeAmount,
- donation.NetAmount,
- r.SendName,
- r.Message,
- channel.ID,
- channel.Name,
- r.WidgetToken,
- crewSessionID,
- r.CrewMemberID,
- crewMemberNickname,
- r.IsTest
- ));
- }
- }
|