feat(admin): discount edit/delete + project-scoped scene/color editor
Identity (discounts):
- DiscountsController: PUT /v1/discounts/{id}, DELETE /v1/discounts/{id}
- DiscountService.UpdateAsync (partial update, code-clash guard) + DeleteAsync
- UpdateDiscountRequest record (all fields optional incl. is_active)
- Frontend discountsConfig: canEdit + canDelete + is_active field
Content (scenes/colors — UI for existing CRUD endpoints):
- New SceneColorEditor.tsx: 3-tab modal (scenes / shared-colors / color-presets),
project-scoped, full add/edit/delete per tab, colour pickers + palette item editor
- Wired into TemplatesAdmin: "صحنهها و رنگها" button per template variant row
- Routes through the generic admin proxy with ?project_id=
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,52 @@ public class DiscountService(IdentityDbContext db) : IDiscountService
|
||||
return MapResponse(discount);
|
||||
}
|
||||
|
||||
public async Task<DiscountResponse?> UpdateAsync(Guid tenantId, Guid id, UpdateDiscountRequest request)
|
||||
{
|
||||
var discount = await db.Discounts.FirstOrDefaultAsync(d => d.Id == id && d.TenantId == tenantId);
|
||||
if (discount == null) return null;
|
||||
|
||||
if (request.Name != null) discount.Name = request.Name;
|
||||
if (request.Code != null)
|
||||
{
|
||||
var newCode = request.Code.ToUpper();
|
||||
if (newCode != discount.Code)
|
||||
{
|
||||
var clash = await db.Discounts.AnyAsync(d =>
|
||||
d.TenantId == tenantId && d.Code == newCode && d.Id != id);
|
||||
if (clash) throw new InvalidOperationException("Discount code already exists");
|
||||
discount.Code = newCode;
|
||||
}
|
||||
}
|
||||
if (request.Kind != null)
|
||||
{
|
||||
if (!Enum.TryParse<DiscountKind>(request.Kind, true, out var kind))
|
||||
throw new ArgumentException("Invalid discount kind");
|
||||
discount.Kind = kind;
|
||||
}
|
||||
if (request.Value.HasValue) discount.Value = request.Value.Value;
|
||||
if (request.OwnerUserId.HasValue) discount.OwnerUserId = request.OwnerUserId;
|
||||
if (request.OwnerProfitPercentage.HasValue) discount.OwnerProfitPercentage = request.OwnerProfitPercentage.Value;
|
||||
if (request.MaxUseCount.HasValue) discount.MaxUseCount = request.MaxUseCount;
|
||||
if (request.AppliesToPlanIds != null) discount.AppliesToPlanIds = request.AppliesToPlanIds;
|
||||
if (request.StartsAt.HasValue) discount.StartsAt = request.StartsAt;
|
||||
if (request.ExpiresAt.HasValue) discount.ExpiresAt = request.ExpiresAt;
|
||||
if (request.IsActive.HasValue) discount.IsActive = request.IsActive.Value;
|
||||
discount.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return MapResponse(discount);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid tenantId, Guid id)
|
||||
{
|
||||
var discount = await db.Discounts.FirstOrDefaultAsync(d => d.Id == id && d.TenantId == tenantId);
|
||||
if (discount == null) return false;
|
||||
db.Discounts.Remove(discount);
|
||||
await db.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static DiscountResponse MapResponse(Discount d) => new(
|
||||
d.Id, d.Name, d.Code, d.Kind.ToString(), d.Value,
|
||||
d.UsedCount, d.MaxUseCount, d.IsActive, d.ExpiresAt, d.CreatedAt
|
||||
|
||||
+2
@@ -8,4 +8,6 @@ public interface IDiscountService
|
||||
Task<DiscountValidateResponse> ValidateAsync(Guid tenantId, string code, Guid? planId);
|
||||
Task<PagedResponse<DiscountResponse>> ListAsync(Guid tenantId, int page, int pageSize);
|
||||
Task<DiscountResponse> CreateAsync(Guid tenantId, CreateDiscountRequest request);
|
||||
Task<DiscountResponse?> UpdateAsync(Guid tenantId, Guid id, UpdateDiscountRequest request);
|
||||
Task<bool> DeleteAsync(Guid tenantId, Guid id);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,22 @@ public class DiscountsController(IDiscountService discountService) : ControllerB
|
||||
return StatusCode(201, result);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[ProducesResponseType(typeof(DiscountResponse), 200)]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateDiscountRequest request)
|
||||
{
|
||||
var result = await discountService.UpdateAsync(GetTenantId(), id, request);
|
||||
return result == null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[ProducesResponseType(204)]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var ok = await discountService.DeleteAsync(GetTenantId(), id);
|
||||
return ok ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
private Guid GetTenantId() => Guid.Parse(User.FindFirst("tenant_id")?.Value
|
||||
?? throw new UnauthorizedAccessException());
|
||||
|
||||
|
||||
@@ -23,6 +23,21 @@ public record CreateDiscountRequest(
|
||||
DateTime? ExpiresAt = null
|
||||
);
|
||||
|
||||
// Partial update — every field optional; only non-null values are applied.
|
||||
public record UpdateDiscountRequest(
|
||||
string? Name = null,
|
||||
string? Code = null,
|
||||
string? Kind = null,
|
||||
decimal? Value = null,
|
||||
Guid? OwnerUserId = null,
|
||||
decimal? OwnerProfitPercentage = null,
|
||||
int? MaxUseCount = null,
|
||||
Guid[]? AppliesToPlanIds = null,
|
||||
DateTime? StartsAt = null,
|
||||
DateTime? ExpiresAt = null,
|
||||
bool? IsActive = null
|
||||
);
|
||||
|
||||
public record IssueRefundRequest(
|
||||
[Required] Guid PaymentId,
|
||||
long? AmountMinor,
|
||||
|
||||
Reference in New Issue
Block a user