336 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			336 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
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
 | 
						|
{
 | 
						|
    /// <summary>
 | 
						|
    /// Lists gifts purchased by the current user.
 | 
						|
    /// </summary>
 | 
						|
    [HttpGet("sent")]
 | 
						|
    [Authorize]
 | 
						|
    public async Task<ActionResult<List<SnWalletGift>>> 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;
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Lists gifts received by the current user (both direct and redeemed open gifts).
 | 
						|
    /// </summary>
 | 
						|
    [HttpGet("received")]
 | 
						|
    [Authorize]
 | 
						|
    public async Task<ActionResult<List<SnWalletGift>>> 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;
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Gets a specific gift by ID (only if user is the gifter or recipient).
 | 
						|
    /// </summary>
 | 
						|
    [HttpGet("{giftId}")]
 | 
						|
    [Authorize]
 | 
						|
    public async Task<ActionResult<SnWalletGift>> 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;
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Checks if a gift code is valid and redeemable.
 | 
						|
    /// </summary>
 | 
						|
    [HttpGet("check/{giftCode}")]
 | 
						|
    [Authorize]
 | 
						|
    public async Task<ActionResult<GiftCheckResponse>> 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;
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Purchases a gift subscription.
 | 
						|
    /// </summary>
 | 
						|
    [HttpPost("purchase")]
 | 
						|
    [Authorize]
 | 
						|
    public async Task<ActionResult<SnWalletGift>> PurchaseGift([FromBody] PurchaseGiftRequest request)
 | 
						|
    {
 | 
						|
        if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
 | 
						|
 | 
						|
        if (currentUser.Profile.Level < MinimumAccountLevel)
 | 
						|
        {
 | 
						|
            if (currentUser.PerkSubscription is null)
 | 
						|
                return StatusCode(403, "Account level must be at least 60 or a member of the Stellar Program 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!;
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Redeems a gift using its code, creating a subscription for the current user.
 | 
						|
    /// </summary>
 | 
						|
    [HttpPost("redeem")]
 | 
						|
    [Authorize]
 | 
						|
    public async Task<ActionResult<RedeemGiftResponse>> 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!;
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Marks a gift as sent (ready for redemption).
 | 
						|
    /// </summary>
 | 
						|
    [HttpPost("{giftId}/send")]
 | 
						|
    [Authorize]
 | 
						|
    public async Task<ActionResult<SnWalletGift>> 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);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Cancels a gift before it's redeemed.
 | 
						|
    /// </summary>
 | 
						|
    [HttpPost("{giftId}/cancel")]
 | 
						|
    [Authorize]
 | 
						|
    public async Task<ActionResult<SnWalletGift>> 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);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Creates an order for an unpaid gift.
 | 
						|
    /// </summary>
 | 
						|
    [HttpPost("{giftId}/order")]
 | 
						|
    [Authorize]
 | 
						|
    public async Task<ActionResult<SnWalletOrder>> 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);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
}
 |