Files
meezi/src/Meezi.API/Services/CouponService.cs
T

189 lines
7.2 KiB
C#
Raw Normal View History

2026-05-27 21:33:48 +03:30
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<IReadOnlyList<CouponDto>> GetAllAsync(string cafeId, CancellationToken cancellationToken = default);
Task<CouponDto?> GetAsync(string cafeId, string id, CancellationToken cancellationToken = default);
Task<CouponDto?> CreateAsync(string cafeId, CreateCouponRequest request, CancellationToken cancellationToken = default);
Task<CouponDto?> UpdateAsync(string cafeId, string id, UpdateCouponRequest request, CancellationToken cancellationToken = default);
Task<bool> 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<IReadOnlyList<CouponDto>> 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<CouponDto?> 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<CouponDto?> 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<CouponDto?> 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<bool> 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);
}