using FlatRender.IdentitySvc.Application.Services.Interfaces; using FlatRender.IdentitySvc.Domain.Entities; using FlatRender.IdentitySvc.Domain.Enums; using FlatRender.IdentitySvc.Infrastructure.Data; using FlatRender.IdentitySvc.Models.Requests; using FlatRender.IdentitySvc.Models.Responses; using Microsoft.EntityFrameworkCore; namespace FlatRender.IdentitySvc.Application.Services; public class PlanService(IdentityDbContext db) : IPlanService { public async Task> ListAsync(Guid? tenantId, string? scope) { var query = db.Plans.Where(p => p.IsActive && p.DeletedAt == null && (p.TenantId == null || p.TenantId == tenantId) && (p.AvailableFrom == null || p.AvailableFrom <= DateTime.UtcNow) && (p.AvailableUntil == null || p.AvailableUntil >= DateTime.UtcNow) ); if (!string.IsNullOrEmpty(scope) && Enum.TryParse(scope, true, out var s)) query = query.Where(p => p.Scope == s); var plans = await query.OrderBy(p => p.Sort).ToListAsync(); return plans.Select(MapPlanResponse).ToList(); } public async Task GetByIdAsync(Guid planId) { var plan = await db.Plans.FindAsync(planId) ?? throw new KeyNotFoundException("Plan not found"); return MapPlanResponse(plan); } public async Task GetCurrentPlanAsync(Guid userId) { var plan = await db.UserPlans .Where(up => up.UserId == userId && up.CancelledAt == null && up.ExpiresAt > DateTime.UtcNow) .OrderByDescending(up => up.StartsAt) .FirstOrDefaultAsync(); if (plan == null) return null; return new UserPlanResponse( plan.Id, plan.PlanId, plan.PlanCode, plan.PlanName, plan.InitialSecondsCharge, plan.RemainChargeSec, plan.MonthlyRendersUsed, plan.StartsAt, plan.ExpiresAt, plan.CancelledAt, plan.AutoRenew ); } public async Task PurchasePlanAsync(Guid userId, Guid tenantId, PurchasePlanRequest request) { var plan = await db.Plans.FindAsync(request.PlanId) ?? throw new KeyNotFoundException("Plan not found"); if (!plan.IsActive || plan.DeletedAt != null) throw new InvalidOperationException("Plan is not available"); var gateway = string.IsNullOrEmpty(request.Gateway) ? PaymentGateway.ZarinPal : Enum.Parse(request.Gateway, true); long discountAmount = 0; if (!string.IsNullOrEmpty(request.DiscountCode)) { var discount = await db.Discounts.FirstOrDefaultAsync(d => d.TenantId == tenantId && d.Code == request.DiscountCode && d.IsActive && (d.ExpiresAt == null || d.ExpiresAt > DateTime.UtcNow)); if (discount != null) { discountAmount = discount.Kind == DiscountKind.Percentage ? (long)(plan.PriceMinor * (double)discount.Value / 100) : (long)discount.Value; } } var amountDue = Math.Max(0, plan.PriceMinor - discountAmount); // ── Balance: deduct immediately and activate ─────────────────────────── if (gateway == PaymentGateway.Balance) { var user = await db.Users.FindAsync(userId) ?? throw new KeyNotFoundException("User not found"); if (user.BalanceMinor < amountDue) throw new InvalidOperationException("موجودی کافی نیست"); user.BalanceMinor -= amountDue; var balancePayment = new Payment { TenantId = tenantId, UserId = userId, Gateway = PaymentGateway.Balance, Action = PaymentAction.PlanPurchase, Status = PaymentStatus.Succeeded, AmountMinor = amountDue, Currency = plan.Currency, DiscountValueMinor = discountAmount, PlanId = plan.Id, Title = $"خرید {plan.Name}", Description = $"Purchase plan: {plan.Code}", ConfirmedAt = DateTime.UtcNow, }; db.Payments.Add(balancePayment); await ActivatePlanForPaymentAsync(balancePayment, plan); await db.SaveChangesAsync(); return new PurchasePlanResponse(balancePayment.Id, "/dashboard?plan_activated=true"); } // ── External gateway: create pending payment, return redirect ───────── var payment = new Payment { TenantId = tenantId, UserId = userId, Gateway = gateway, Action = PaymentAction.PlanPurchase, AmountMinor = amountDue, Currency = plan.Currency, DiscountValueMinor = discountAmount, PlanId = plan.Id, Title = $"خرید {plan.Name}", Description = $"Purchase plan: {plan.Code}", }; db.Payments.Add(payment); await db.SaveChangesAsync(); var redirectUrl = $"/v1/payments/gateway/{gateway.ToString().ToLower()}?payment_id={payment.Id}"; return new PurchasePlanResponse(payment.Id, redirectUrl); } private async Task ActivatePlanForPaymentAsync(Payment payment, Plan plan) { var durationMonths = plan.MonthsDuration ?? plan.BillingPeriod switch { BillingPeriod.Monthly => 1, BillingPeriod.Quarterly => 3, BillingPeriod.SemiAnnual => 6, BillingPeriod.Annual => 12, BillingPeriod.Lifetime => 1200, BillingPeriod.OneTime => 1, _ => 1, }; var now = DateTime.UtcNow; db.UserPlans.Add(new UserPlan { UserId = payment.UserId, TenantId = payment.TenantId, PlanId = plan.Id, PlanCode = plan.Code, PlanName = plan.Name, PriceMinorPaid = payment.AmountMinor, Currency = payment.Currency, InitialSecondsCharge = plan.SecondsCharge, RemainChargeSec = plan.SecondsCharge, StartsAt = now, ExpiresAt = now.AddMonths(durationMonths), AutoRenew = false, PaymentId = payment.Id, }); await Task.CompletedTask; // placeholder for future async work } public async Task CancelPlanAsync(Guid userId) { var userPlan = await db.UserPlans .Where(up => up.UserId == userId && up.CancelledAt == null && up.ExpiresAt > DateTime.UtcNow) .OrderByDescending(up => up.StartsAt) .FirstOrDefaultAsync() ?? throw new KeyNotFoundException("No active plan to cancel"); userPlan.CancelledAt = DateTime.UtcNow; userPlan.AutoRenew = false; await db.SaveChangesAsync(); } private static PlanResponse MapPlanResponse(Plan p) => new( p.Id, p.Code, p.Name, p.Description, p.PriceMinor, p.BeforePriceMinor, p.Currency, p.BillingPeriod.ToString(), p.SecondsCharge, p.MonthlyRendersQuota, p.StorageGb, p.ParallelRenders, p.MaxResolution, p.RenderSpeedFactor, p.Icon, p.IsFeatured, p.Features ); }