using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; using System.ComponentModel.DataAnnotations; using DysonNetwork.Shared.Models; namespace DysonNetwork.Pass.Wallet; [ApiController] [Route("/api/subscriptions/gifts")] public class SubscriptionGiftController( SubscriptionService subscriptions, AppDatabase db ) : ControllerBase { /// /// Lists gifts purchased by the current user. /// [HttpGet("sent")] [Authorize] public async Task>> ListSentGifts( [FromQuery] int offset = 0, [FromQuery] int take = 20 ) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); var query = await subscriptions.GetGiftsByGifterAsync(currentUser.Id); var totalCount = query.Count; var gifts = query .Skip(offset) .Take(take) .ToList(); Response.Headers["X-Total"] = totalCount.ToString(); return gifts; } /// /// Lists gifts received by the current user (both direct and redeemed open gifts). /// [HttpGet("received")] [Authorize] public async Task>> ListReceivedGifts( [FromQuery] int offset = 0, [FromQuery] int take = 20 ) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); var gifts = await subscriptions.GetGiftsByRecipientAsync(currentUser.Id); var totalCount = gifts.Count; gifts = gifts .Skip(offset) .Take(take) .ToList(); Response.Headers["X-Total"] = totalCount.ToString(); return gifts; } /// /// Gets a specific gift by ID (only if user is the gifter or recipient). /// [HttpGet("{giftId}")] [Authorize] public async Task> GetGift(Guid giftId) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); var gift = await db.WalletGifts .Include(g => g.Gifter).ThenInclude(a => a.Profile) .Include(g => g.Recipient).ThenInclude(a => a.Profile) .Include(g => g.Redeemer).ThenInclude(a => a.Profile) .Include(g => g.Subscription) .Include(g => g.Coupon) .FirstOrDefaultAsync(g => g.Id == giftId); if (gift is null) return NotFound(); if (gift.GifterId != currentUser.Id && gift.RecipientId != currentUser.Id && !(gift.IsOpenGift && gift.RedeemerId == currentUser.Id)) return NotFound(); return gift; } /// /// Checks if a gift code is valid and redeemable. /// [HttpGet("check/{giftCode}")] [Authorize] public async Task> CheckGiftCode(string giftCode) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); var gift = await subscriptions.GetGiftByCodeAsync(giftCode); if (gift is null) return NotFound("Gift code not found."); var canRedeem = false; var error = ""; if (gift.Status != DysonNetwork.Shared.Models.GiftStatus.Sent) { error = gift.Status switch { DysonNetwork.Shared.Models.GiftStatus.Created => "Gift has not been sent yet.", DysonNetwork.Shared.Models.GiftStatus.Redeemed => "Gift has already been redeemed.", DysonNetwork.Shared.Models.GiftStatus.Expired => "Gift has expired.", DysonNetwork.Shared.Models.GiftStatus.Cancelled => "Gift has been cancelled.", _ => "Gift is not redeemable." }; } else if (gift.ExpiresAt < SystemClock.Instance.GetCurrentInstant()) { error = "Gift has expired."; } else if (!gift.IsOpenGift && gift.RecipientId != currentUser.Id) { error = "This gift is intended for someone else."; } else { // Check if user already has this subscription type var subscriptionInfo = SubscriptionTypeData .SubscriptionDict.TryGetValue(gift.SubscriptionIdentifier, out var template) ? template : null; if (subscriptionInfo != null) { var subscriptionsInGroup = subscriptionInfo.GroupIdentifier is not null ? SubscriptionTypeData.SubscriptionDict .Where(s => s.Value.GroupIdentifier == subscriptionInfo.GroupIdentifier) .Select(s => s.Value.Identifier) .ToArray() : [gift.SubscriptionIdentifier]; var existingSubscription = await subscriptions.GetSubscriptionAsync(currentUser.Id, subscriptionsInGroup); if (existingSubscription is not null) { error = "You already have an active subscription of this type."; } else { canRedeem = true; } } } return new GiftCheckResponse { GiftCode = giftCode, SubscriptionIdentifier = gift.SubscriptionIdentifier, CanRedeem = canRedeem, Error = error, Message = gift.Message }; } public class GiftCheckResponse { public string GiftCode { get; set; } = null!; public string SubscriptionIdentifier { get; set; } = null!; public bool CanRedeem { get; set; } public string Error { get; set; } = null!; public string? Message { get; set; } } public class PurchaseGiftRequest { [Required] public string SubscriptionIdentifier { get; set; } = null!; public Guid? RecipientId { get; set; } [Required] public string PaymentMethod { get; set; } = null!; [Required] public SnPaymentDetails PaymentDetails { get; set; } = null!; public string? Message { get; set; } public string? Coupon { get; set; } public int? GiftDurationDays { get; set; } = 30; // Gift expires in 30 days by default public int? SubscriptionDurationDays { get; set; } = 30; // Subscription lasts 30 days when redeemed } const int MinimumAccountLevel = 60; /// /// Purchases a gift subscription. /// [HttpPost("purchase")] [Authorize] public async Task> PurchaseGift([FromBody] PurchaseGiftRequest request) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); if (currentUser.Profile.Level < MinimumAccountLevel) { return StatusCode(403, "Account level must be at least 60 to purchase a gift."); } Duration? giftDuration = null; if (request.GiftDurationDays.HasValue) giftDuration = Duration.FromDays(request.GiftDurationDays.Value); Duration? subscriptionDuration = null; if (request.SubscriptionDurationDays.HasValue) subscriptionDuration = Duration.FromDays(request.SubscriptionDurationDays.Value); try { var gift = await subscriptions.PurchaseGiftAsync( currentUser, request.RecipientId, request.SubscriptionIdentifier, request.PaymentMethod, request.PaymentDetails, request.Message, request.Coupon, giftDuration, subscriptionDuration ); return gift; } catch (ArgumentOutOfRangeException ex) { return BadRequest(ex.Message); } catch (InvalidOperationException ex) { return BadRequest(ex.Message); } } public class RedeemGiftRequest { [Required] public string GiftCode { get; set; } = null!; } /// /// Redeems a gift using its code, creating a subscription for the current user. /// [HttpPost("redeem")] [Authorize] public async Task> RedeemGift([FromBody] RedeemGiftRequest request) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); try { var (gift, subscription) = await subscriptions.RedeemGiftAsync(currentUser, request.GiftCode); return new RedeemGiftResponse { Gift = gift, Subscription = subscription }; } catch (InvalidOperationException ex) { return BadRequest(ex.Message); } } public class RedeemGiftResponse { public SnWalletGift Gift { get; set; } = null!; public SnWalletSubscription Subscription { get; set; } = null!; } /// /// Marks a gift as sent (ready for redemption). /// [HttpPost("{giftId}/send")] [Authorize] public async Task> SendGift(Guid giftId) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); try { var gift = await subscriptions.MarkGiftAsSentAsync(giftId, currentUser.Id); return gift; } catch (InvalidOperationException ex) { return BadRequest(ex.Message); } } /// /// Cancels a gift before it's redeemed. /// [HttpPost("{giftId}/cancel")] [Authorize] public async Task> CancelGift(Guid giftId) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); try { var gift = await subscriptions.CancelGiftAsync(giftId, currentUser.Id); return gift; } catch (InvalidOperationException ex) { return BadRequest(ex.Message); } } /// /// Creates an order for an unpaid gift. /// [HttpPost("{giftId}/order")] [Authorize] public async Task> CreateGiftOrder(Guid giftId) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); try { var order = await subscriptions.CreateGiftOrder(currentUser.Id, giftId); return order; } catch (InvalidOperationException ex) { return BadRequest(ex.Message); } } }