using Meezi.API.Models.Crm; using Meezi.Core.Entities; using Meezi.Core.Enums; using Meezi.Infrastructure.Data; using Meezi.Shared; using Microsoft.EntityFrameworkCore; namespace Meezi.API.Services; public interface ICouponService { Task> GetAllAsync(string cafeId, CancellationToken cancellationToken = default); Task GetAsync(string cafeId, string id, CancellationToken cancellationToken = default); Task CreateAsync(string cafeId, CreateCouponRequest request, CancellationToken cancellationToken = default); Task UpdateAsync(string cafeId, string id, UpdateCouponRequest request, CancellationToken cancellationToken = default); Task DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default); Task<(ValidateCouponResult? Data, ApiError? Error)> ValidateAsync( string cafeId, ValidateCouponRequest request, CancellationToken cancellationToken = default); } public class CouponService : ICouponService { private readonly AppDbContext _db; public CouponService(AppDbContext db) { _db = db; } public async Task> GetAllAsync(string cafeId, CancellationToken cancellationToken = default) { var list = await _db.Coupons .Where(c => c.CafeId == cafeId) .OrderByDescending(c => c.CreatedAt) .ToListAsync(cancellationToken); return list.Select(ToDto).ToList(); } public async Task GetAsync(string cafeId, string id, CancellationToken cancellationToken = default) { var entity = await _db.Coupons .FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken); return entity is null ? null : ToDto(entity); } public async Task CreateAsync( string cafeId, CreateCouponRequest request, CancellationToken cancellationToken = default) { var codeExists = await _db.Coupons.AnyAsync( c => c.CafeId == cafeId && c.Code == request.Code.ToUpperInvariant(), cancellationToken); if (codeExists) return null; var entity = new Coupon { CafeId = cafeId, Code = request.Code.ToUpperInvariant(), Type = request.Type, Value = request.Value, MinOrderAmount = request.MinOrderAmount, MaxDiscount = request.MaxDiscount, UsageLimit = request.UsageLimit, TargetGroup = request.TargetGroup, StartsAt = request.StartsAt, ExpiresAt = request.ExpiresAt, IsActive = request.IsActive }; _db.Coupons.Add(entity); await _db.SaveChangesAsync(cancellationToken); return ToDto(entity); } public async Task UpdateAsync( string cafeId, string id, UpdateCouponRequest request, CancellationToken cancellationToken = default) { var entity = await _db.Coupons .FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken); if (entity is null) return null; if (request.Code is not null) entity.Code = request.Code.ToUpperInvariant(); if (request.Type.HasValue) entity.Type = request.Type.Value; if (request.Value.HasValue) entity.Value = request.Value.Value; if (request.MinOrderAmount.HasValue) entity.MinOrderAmount = request.MinOrderAmount; if (request.MaxDiscount.HasValue) entity.MaxDiscount = request.MaxDiscount; if (request.UsageLimit.HasValue) entity.UsageLimit = request.UsageLimit; if (request.TargetGroup.HasValue) entity.TargetGroup = request.TargetGroup; if (request.StartsAt.HasValue) entity.StartsAt = request.StartsAt; if (request.ExpiresAt.HasValue) entity.ExpiresAt = request.ExpiresAt; if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value; await _db.SaveChangesAsync(cancellationToken); return ToDto(entity); } public async Task DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default) { var entity = await _db.Coupons .FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken); if (entity is null) return false; entity.DeletedAt = DateTime.UtcNow; entity.IsActive = false; await _db.SaveChangesAsync(cancellationToken); return true; } public async Task<(ValidateCouponResult? Data, ApiError? Error)> ValidateAsync( string cafeId, ValidateCouponRequest request, CancellationToken cancellationToken = default) { var code = request.Code.Trim().ToUpperInvariant(); if (string.IsNullOrEmpty(code)) return (null, new ApiError("COUPON_REQUIRED", "Coupon code is required.")); if (request.Subtotal <= 0) return (null, new ApiError("CART_EMPTY", "Add items before applying a coupon.")); var coupon = await _db.Coupons.FirstOrDefaultAsync( c => c.CafeId == cafeId && c.Code == code, cancellationToken); if (coupon is null) return (null, new ApiError("COUPON_NOT_FOUND", "Coupon code is invalid.")); if (!coupon.IsActive || coupon.DeletedAt is not null) return (null, new ApiError("COUPON_INACTIVE", "This coupon is not active.")); var now = DateTime.UtcNow; if (coupon.StartsAt is not null && coupon.StartsAt > now) return (null, new ApiError("COUPON_NOT_STARTED", "This coupon is not valid yet.")); if (coupon.ExpiresAt is not null && coupon.ExpiresAt < now) return (null, new ApiError("COUPON_EXPIRED", "This coupon has expired.")); if (coupon.UsageLimit is not null && coupon.UsedCount >= coupon.UsageLimit) return (null, new ApiError("COUPON_LIMIT_REACHED", "This coupon has reached its usage limit.")); if (coupon.MinOrderAmount is not null && request.Subtotal < coupon.MinOrderAmount) return (null, new ApiError( "COUPON_MIN_ORDER", $"Minimum order amount is {coupon.MinOrderAmount:N0} T.")); var discount = CalculateDiscount(coupon, request.Subtotal); if (discount <= 0) return (null, new ApiError("COUPON_NO_DISCOUNT", "This coupon does not apply to this order.")); return (new ValidateCouponResult( coupon.Id, coupon.Code, coupon.Type, coupon.Value, discount), null); } internal static decimal CalculateDiscount(Coupon coupon, decimal subtotal) { return coupon.Type switch { CouponType.Percentage => Math.Min( Math.Round(subtotal * coupon.Value / 100m, 0), coupon.MaxDiscount ?? subtotal), CouponType.FixedAmount => Math.Min(coupon.Value, subtotal), _ => 0m }; } private static CouponDto ToDto(Coupon c) => new( c.Id, c.Code, c.Type, c.Value, c.MinOrderAmount, c.MaxDiscount, c.UsageLimit, c.UsedCount, c.TargetGroup, c.StartsAt, c.ExpiresAt, c.IsActive); }