using System.Linq.Expressions;
using Fake.Application;
using Fake.Domain.Exceptions;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Marketing;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
using Oms.Domain.MarketingAggregate.Consts;
using Oms.Domain.MarketingAggregate.Entities;
using Oms.Domain.MarketingAggregate.Enums;
using Oms.Domain.MarketingAggregate.Repositories;
namespace Oms.Application.Services.Marketing;
///
/// 优惠券模板 API(管理端)。
///
[ApiExplorerSettings(GroupName = "OMS-Marketing")]
public class MarketingCouponTemplateService : ApplicationService
{
private ICouponTemplateRepository TemplateRepository =>
LazyServiceProvider.GetRequiredService();
///
/// 分页查询优惠券模板。
///
[HttpPost("/marketing/coupon-template/list")]
public async Task> GetListAsync(CouponTemplateListInput input)
{
var predicate = BuildListPredicate(input);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await TemplateRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary
{
[nameof(CouponTemplate.UpdateTime)] = false,
[nameof(CouponTemplate.Id)] = false
});
var total = await TemplateRepository.CountAsync(predicate);
return new OmsPageResult
{
Items = entities.Select(MapOutput).ToList(),
Total = total
};
}
///
/// 新增优惠券模板(草稿)。
///
[HttpPost("/marketing/coupon-template/create")]
public async Task CreateAsync(CouponTemplateSaveInput input)
{
await ValidateSaveAsync(input, null);
var entity = MapToEntity(input, new CouponTemplate
{
Status = CouponTemplateStatus.Draft,
IssuedQuantity = 0
});
await TemplateRepository.InsertAsync(entity);
}
///
/// 更新优惠券模板。
///
[HttpPost("/marketing/coupon-template/update")]
public async Task UpdateAsync(CouponTemplateSaveInput input)
{
if (!input.Id.HasValue || input.Id.Value <= 0)
{
throw new BusinessException("Template id is invalid");
}
var entity = await GetTemplateOrThrowAsync(input.Id.Value);
if (entity.Status == CouponTemplateStatus.Ended)
{
throw new BusinessException("Ended template cannot be edited");
}
await ValidateSaveAsync(input, entity.Id);
MapToEntity(input, entity);
await TemplateRepository.UpdateAsync(entity);
}
///
/// 发布优惠券模板(草稿 → 生效)。
///
[HttpPost("/marketing/coupon-template/publish")]
public async Task PublishAsync(IdInput input)
{
var entity = await GetTemplateOrThrowAsync(input.Id);
if (entity.Status != CouponTemplateStatus.Draft)
{
throw new BusinessException("Only draft templates can be published");
}
entity.Status = CouponTemplateStatus.Active;
await TemplateRepository.UpdateAsync(entity);
}
///
/// 结束优惠券模板(生效 → 已结束)。
///
[HttpPost("/marketing/coupon-template/end")]
public async Task EndAsync(IdInput input)
{
var entity = await GetTemplateOrThrowAsync(input.Id);
if (entity.Status != CouponTemplateStatus.Active)
{
throw new BusinessException("Only active templates can be ended");
}
entity.Status = CouponTemplateStatus.Ended;
await TemplateRepository.UpdateAsync(entity);
}
private static Expression> BuildListPredicate(
CouponTemplateListInput input)
{
var keyword = input.Keyword?.Trim();
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
var status = input.Status;
var hasStatus = status.HasValue;
return x =>
(!hasStatus || x.Status == status!.Value) &&
(!hasKeyword ||
x.TemplateCode.Contains(keyword!) ||
x.TemplateName.Contains(keyword!));
}
private async Task ValidateSaveAsync(CouponTemplateSaveInput input, int? excludeId)
{
var templateCode = input.TemplateCode.Trim();
var templateName = input.TemplateName.Trim();
if (string.IsNullOrWhiteSpace(templateCode))
{
throw new BusinessException("Template code is required");
}
if (string.IsNullOrWhiteSpace(templateName))
{
throw new BusinessException("Template name is required");
}
if (string.IsNullOrWhiteSpace(input.CouponType))
{
throw new BusinessException("Coupon type is required");
}
if (input.PerCustomerLimit <= 0)
{
throw new BusinessException("Per customer limit must be positive");
}
if (input.TotalQuantity < 0)
{
throw new BusinessException("Total quantity cannot be negative");
}
if (await TemplateRepository.ExistsTemplateCodeAsync(templateCode, excludeId))
{
throw new BusinessException("Template code already exists");
}
ValidateCouponTypeFields(input);
}
private static void ValidateCouponTypeFields(CouponTemplateSaveInput input)
{
switch (input.CouponType)
{
case MarketingCouponTypes.FixedOff:
case MarketingCouponTypes.DirectOff:
case MarketingCouponTypes.ShippingOff:
if (!input.FaceValue.HasValue || input.FaceValue.Value <= 0)
{
throw new BusinessException("Face value must be positive");
}
break;
case MarketingCouponTypes.PercentOff:
if (!input.DiscountRate.HasValue || input.DiscountRate.Value <= 0 || input.DiscountRate.Value >= 1)
{
throw new BusinessException("Discount rate must be between 0 and 1");
}
break;
default:
throw new BusinessException("Unsupported coupon type");
}
}
private static CouponTemplate MapToEntity(CouponTemplateSaveInput input, CouponTemplate entity)
{
entity.TemplateCode = input.TemplateCode.Trim();
entity.TemplateName = input.TemplateName.Trim();
entity.CouponType = input.CouponType.Trim();
entity.FaceValue = input.FaceValue;
entity.DiscountRate = input.DiscountRate;
entity.MaxDiscountAmount = input.MaxDiscountAmount;
entity.MinOrderAmount = input.MinOrderAmount;
entity.TotalQuantity = input.TotalQuantity;
entity.PerCustomerLimit = input.PerCustomerLimit;
entity.ValidType = input.ValidType;
entity.ValidStartTime = input.ValidStartTime;
entity.ValidEndTime = input.ValidEndTime;
entity.ValidDaysAfterReceive = input.ValidDaysAfterReceive;
entity.ApplyScopeJson = string.IsNullOrWhiteSpace(input.ApplyScopeJson) ? null : input.ApplyScopeJson.Trim();
entity.GrantChannel = string.IsNullOrWhiteSpace(input.GrantChannel) ? "manual" : input.GrantChannel.Trim();
entity.Remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim();
return entity;
}
private static CouponTemplateOutput MapOutput(CouponTemplate entity) => new()
{
Id = entity.Id,
TemplateCode = entity.TemplateCode,
TemplateName = entity.TemplateName,
CouponType = entity.CouponType,
CouponTypeName = MarketingEnumNames.OfCouponType(entity.CouponType),
FaceValue = entity.FaceValue,
DiscountRate = entity.DiscountRate,
MaxDiscountAmount = entity.MaxDiscountAmount,
MinOrderAmount = entity.MinOrderAmount,
TotalQuantity = entity.TotalQuantity,
IssuedQuantity = entity.IssuedQuantity,
PerCustomerLimit = entity.PerCustomerLimit,
ValidType = entity.ValidType,
ValidStartTime = entity.ValidStartTime,
ValidEndTime = entity.ValidEndTime,
ValidDaysAfterReceive = entity.ValidDaysAfterReceive,
ApplyScopeJson = entity.ApplyScopeJson,
GrantChannel = entity.GrantChannel,
Status = entity.Status,
StatusName = MarketingEnumNames.Of(entity.Status),
Remark = entity.Remark,
CreateTime = entity.CreateTime,
UpdateTime = entity.UpdateTime
};
private async Task GetTemplateOrThrowAsync(int id)
{
return await TemplateRepository.FirstOrDefaultAsync(x => x.Id == id)
?? throw new BusinessException("Coupon template not found");
}
}