Files
CMS/src/CMSMicroservice.WebApi/Services/UserOrderService.cs
T
masoodafar-web 541b0c2ce9
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 14m42s
fix(build): add missing System.Linq import in DiscountOrderService
Fixes CI compile error CS1061 on DbSet.Where; remove duplicate using in UserOrderService.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 02:29:55 +03:30

1060 lines
45 KiB
C#

using CMSMicroservice.Application.Common.Authorization;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrders;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrderHistory;
using CMSMicroservice.Application.OrderManagementCQ.Commands.UpdateOrderStatus;
using CMSMicroservice.Application.OrderManagementCQ.Commands.CancelOrderByAdmin;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Entities.Club;
using CMSMicroservice.Domain.Entities.Order;
using CMSMicroservice.Domain.Enums;
using AppModels = CMSMicroservice.Application.Common.Models;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using System.Collections.Generic;
using System.Linq;
using CMSMicroservice.Protobuf.Protos;
using MediatR;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.WebApi.Services;
public class UserOrderService : UserOrderContract.UserOrderContractBase
{
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
private readonly IInventoryService _inventoryService;
public UserOrderService(ISender sender, IApplicationDbContext context, ICurrentUserService currentUserService, IInventoryService inventoryService)
{
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_inventoryService = inventoryService;
}
[RequiresPermission(PermissionNames.OrdersCreate)]
public override async Task<CreateNewUserOrderResponse> CreateNewUserOrder(CreateNewUserOrderRequest request, ServerCallContext context)
{
var order = new UserOrder
{
Amount = request.Amount,
PackageId = request.PackageId > 0 ? request.PackageId : null,
TransactionId = request.TransactionId,
PaymentStatus = request.HasPaymentStatus
? (Domain.Enums.PaymentStatus)(int)request.PaymentStatus
: Domain.Enums.PaymentStatus.Pending,
PaymentDate = request.PaymentDate?.ToDateTime(),
UserId = request.UserId,
UserAddressId = request.UserAddressId,
PaymentMethod = request.HasPaymentMethod
? (Domain.Enums.PaymentMethod)(int)request.PaymentMethod
: null,
DeliveryStatus = Domain.Enums.DeliveryStatus.Pending
};
_context.UserOrders.Add(order);
await _context.SaveChangesAsync(context.CancellationToken);
return new CreateNewUserOrderResponse { Id = order.Id };
}
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateUserOrder(UpdateUserOrderRequest request, ServerCallContext context)
{
var order = await _context.UserOrders.FindAsync(new object[] { request.Id }, context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
if (request.Amount != null) order.Amount = request.Amount.Value;
if (request.PackageId != null) order.PackageId = request.PackageId.Value;
if (request.TransactionId != null) order.TransactionId = request.TransactionId.Value;
if (request.HasPaymentStatus) order.PaymentStatus = (Domain.Enums.PaymentStatus)(int)request.PaymentStatus;
if (request.PaymentDate != null) order.PaymentDate = request.PaymentDate.ToDateTime();
if (request.UserId != null) order.UserId = request.UserId.Value;
if (request.UserAddressId != null) order.UserAddressId = request.UserAddressId.Value;
if (request.HasPaymentMethod) order.PaymentMethod = (Domain.Enums.PaymentMethod)(int)request.PaymentMethod;
if (request.HasDeliveryStatus) order.DeliveryStatus = (Domain.Enums.DeliveryStatus)(int)request.DeliveryStatus;
if (request.TrackingCode != null) order.TrackingCode = request.TrackingCode;
if (request.DeliveryDescription != null) order.DeliveryDescription = request.DeliveryDescription;
await _context.SaveChangesAsync(context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
[RequiresPermission(PermissionNames.OrdersDelete)]
public override async Task<Google.Protobuf.WellKnownTypes.Empty> DeleteUserOrder(DeleteUserOrderRequest request, ServerCallContext context)
{
var order = await _context.UserOrders.FindAsync(new object[] { request.Id }, context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
order.IsDeleted = true;
await _context.SaveChangesAsync(context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
[RequiresPermission(PermissionNames.OrdersView)]
public override async Task<GetUserOrderResponse> GetUserOrder(GetUserOrderRequest request, ServerCallContext context)
{
var query = new GetCustomerOrderQuery
{
OrderId = request.Id,
UserId = 0 // Admin: no user filter, can view any order by ID
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetUserOrderResponse
{
Id = result.Id,
Amount = result.Amount,
PackageId = result.PackageId ?? 0,
TransactionId = result.TransactionId,
UserId = result.UserId,
UserAddressId = result.UserAddressId,
UserAddressText = result.UserAddressText,
TrackingCode = result.TrackingCode,
DeliveryDescription = result.DeliveryDescription,
UserFullName = result.UserFullName,
UserNationalCode = result.UserNationalCode
};
// VAT Info
if (result.VatAmount > 0)
{
response.VatInfo = new OrderVATInfo
{
VatRate = result.VatPercentage / 100,
BaseAmount = result.Amount - result.VatAmount,
VatAmount = result.VatAmount,
TotalAmount = result.Amount,
IsPaid = result.PaymentStatus == Domain.Enums.PaymentStatus.Success
};
}
response.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)result.PaymentStatus;
if (result.PaymentDate.HasValue)
response.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(result.PaymentDate.Value, DateTimeKind.Utc));
if (result.PaymentMethod.HasValue)
response.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)result.PaymentMethod.Value;
response.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)result.DeliveryStatus;
foreach (var fd in result.FactorDetails)
{
response.FactorDetails.Add(new GetUserOrderResponseFactorDetail
{
ProductId = fd.ProductId,
ProductTitle = fd.ProductTitle,
ProductThumbnailPath = fd.ProductThumbnailPath,
UnitPrice = fd.UnitPrice,
Count = fd.Count,
UnitDiscountPrice = fd.UnitDiscountPrice
});
}
return response;
}
[RequiresPermission(PermissionNames.OrdersView)]
public override async Task<GetAllUserOrderByFilterResponse> GetAllUserOrderByFilter(GetAllUserOrderByFilterRequest request, ServerCallContext context)
{
// Admin API - can view all orders or filter by specific user
var query = new GetCustomerOrdersQuery
{
UserId = request.Filter?.UserId ?? 0, // 0 means all users (admin view)
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
PaymentStatusFilter = request.Filter?.HasPaymentStatus == true
? (int?)request.Filter.PaymentStatus
: null,
DeliveryStatusFilter = request.Filter?.HasDeliveryStatus == true
? (int?)request.Filter.DeliveryStatus
: null,
FromDate = request.Filter?.FromDate?.ToDateTime() ?? request.Filter?.PaymentDate?.ToDateTime(),
ToDate = request.Filter?.ToDate?.ToDateTime()
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetAllUserOrderByFilterResponse
{
MetaData = result.MetaData.Adapt<MetaData>()
};
foreach (var model in result.Models)
{
var orderModel = new GetAllUserOrderByFilterResponseModel
{
Id = model.Id,
Amount = model.Amount,
PackageId = model.PackageId ?? 0,
TransactionId = model.TransactionId,
UserId = model.UserId,
UserAddressId = model.UserAddressId,
UserAddressText = model.UserAddressText,
TrackingCode = model.TrackingCode,
DeliveryDescription = model.DeliveryDescription,
UserFullName = model.UserFullName,
UserNationalCode = model.UserNationalCode,
VatAmount = model.VatAmount,
VatPercentage = model.VatPercentage
};
orderModel.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)model.PaymentStatus;
if (model.PaymentDate.HasValue)
orderModel.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(model.PaymentDate.Value, DateTimeKind.Utc));
if (model.PaymentMethod.HasValue)
orderModel.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)model.PaymentMethod.Value;
orderModel.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)model.DeliveryStatus;
foreach (var fd in model.FactorDetails)
{
orderModel.FactorDetails.Add(new GetAllUserOrderByFilterResponseModelFactorDetail
{
ProductId = fd.ProductId,
ProductTitle = fd.ProductTitle,
ProductThumbnailPath = fd.ProductThumbnailPath,
UnitPrice = fd.UnitPrice,
Count = fd.Count,
UnitDiscountPrice = fd.UnitDiscountPrice
});
}
response.Models.Add(orderModel);
}
return response;
}
public override async Task<SubmitShopBuyOrderResponse> SubmitShopBuyOrder(SubmitShopBuyOrderRequest request, ServerCallContext context)
{
// Get UserId from JWT or request
var userId = !string.IsNullOrEmpty(_currentUserService.UserId)
? long.Parse(_currentUserService.UserId)
: request.UserId;
if (userId == 0)
{
throw new RpcException(new Status(StatusCode.Unauthenticated, "User not authenticated"));
}
// Get user's cart items with product details
var cartItems = await _context.UserCarts
.Include(c => c.Product)
.Where(c => c.UserId == userId && !c.IsDeleted)
.ToListAsync(context.CancellationToken);
if (!cartItems.Any())
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, "سبد خرید خالی است"));
}
// Get user's default address
var defaultAddress = await _context.UserAddresses
.Where(a => a.UserId == userId && a.IsDefault && !a.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (defaultAddress == null)
{
// Check if user has any address at all
var hasAnyAddress = await _context.UserAddresses
.AnyAsync(a => a.UserId == userId && !a.IsDeleted, context.CancellationToken);
throw new RpcException(new Status(StatusCode.FailedPrecondition,
hasAnyAddress
? "لطفاً یک آدرس را به عنوان پیش‌فرض انتخاب کنید."
: "آدرسی ثبت نشده است. لطفاً ابتدا یک آدرس اضافه کنید."));
}
// Calculate amounts
const decimal vatRate = 0.10m;
long baseAmount = cartItems.Sum(c => c.Product.Price * c.Count);
long vatAmount = (long)(baseAmount * vatRate);
long totalAmount = baseAmount + vatAmount;
// Validate total amount
if (request.TotalAmount > 0 && Math.Abs(totalAmount - request.TotalAmount) > 100)
{
throw new RpcException(new Status(StatusCode.InvalidArgument,
$"مبلغ نامعتبر است. محاسبه شده: {totalAmount}, دریافتی: {request.TotalAmount}"));
}
// Validate stock availability for all cart items before any financial operation
foreach (var cartItem in cartItems)
{
var available = await _inventoryService.GetAvailableQuantityAsync(
cartItem.ProductId, ProductType.RegularProduct, ct: context.CancellationToken);
if (available < cartItem.Count)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition,
$"موجودی «{cartItem.Product.Title}» کافی نیست. موجود: {available}، درخواستی: {cartItem.Count}"));
}
}
// Get user's wallet
var wallet = await _context.UserWallets
.Where(w => w.UserId == userId)
.FirstOrDefaultAsync(context.CancellationToken);
if (wallet == null)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, "کیف پول یافت نشد"));
}
// Check wallet balance
if (wallet.Balance < totalAmount)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition,
$"موجودی کیف پول کافی نیست. موجودی: {wallet.Balance:N0} تومان، مورد نیاز: {totalAmount:N0} تومان"));
}
// Create transaction
var transaction = new Transaction
{
Amount = totalAmount,
Description = $"خرید محصولات - سفارش شماره در حال ایجاد",
PaymentStatus = CMSMicroservice.Domain.Enums.PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
Type = CMSMicroservice.Domain.Enums.TransactionType.Buy,
RefId = $"SHOP_{DateTime.UtcNow.Ticks}"
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(context.CancellationToken);
// Deduct from wallet
var oldBalance = wallet.Balance;
wallet.Balance -= totalAmount;
// Create wallet change log
var walletLog = new UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = -totalAmount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = 0,
IsIncrease = false,
RefrenceId = transaction.Id
};
_context.UserWalletHistories.Add(walletLog);
// ═══ Magic Wallet: Entry / Exit Trigger ═══
// Q24: آستانه <= 100,000 تومان به جای == 0 (چون قیمت محصولات متفاوت است)
if (wallet.Balance <= SystemConstants.MagicWalletEntryThreshold)
{
var user = await _context.Users
.Include(u => u.ClubMembership)
.FirstOrDefaultAsync(u => u.Id == userId, context.CancellationToken);
if (wallet.WalletMode == WalletMode.Magic)
{
// بارگذاری پکیج کاربر برای سقف کیف‌پول جادویی
var userCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, context.CancellationToken);
var userPackage = userCycle != null
? await _context.Packages.FirstOrDefaultAsync(p => p.Id == userCycle.PackageId, context.CancellationToken)
: await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, context.CancellationToken);
var magicMaxDeposit = userPackage?.MagicWalletMaxDeposit
?? throw new InvalidOperationException($"Package not found for magic wallet exit check, UserId={userId}");
if (wallet.MagicTotalDeposited >= magicMaxDeposit)
{
// ── EXIT Magic Mode ──
// هر دو شرط: Balance<=Threshold و سقف MagicMaxDeposit پر شده
wallet.WalletMode = WalletMode.Normal;
wallet.MagicCompletedAt = DateTime.UtcNow;
var currentCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle,
context.CancellationToken);
if (currentCycle != null)
{
currentCycle.MagicCompletedAt = DateTime.UtcNow;
currentCycle.IsCurrentCycle = false;
}
// B6 fix: ریست PackagePurchaseMethod تا Guards G1-G3 خرید مجدد را مسدود نکنند
if (user != null)
{
user.PackagePurchaseMethod = PackagePurchaseMethod.None;
}
}
}
else if (wallet.WalletMode == WalletMode.Normal
&& user != null
&& user.PackagePurchaseMethod != PackagePurchaseMethod.None
&& user.ClubMembership?.IsActive == true)
{
// ── ENTER Magic Mode ──
wallet.WalletMode = WalletMode.Magic;
wallet.MagicActivatedAt = DateTime.UtcNow;
wallet.MagicCompletedAt = null;
wallet.MagicTotalDeposited = 0;
wallet.MagicTotalCredited = 0;
var currentCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle,
context.CancellationToken);
if (currentCycle != null)
{
currentCycle.MagicStartedAt = DateTime.UtcNow;
}
}
// ⚠️ اگه Magic باشه و Balance<=Threshold ولی سقف پر نشده → هنوز Magic!
// کاربر میتونه دوباره شارژ کنه
}
// Create order
var order = new UserOrder
{
UserId = userId,
UserAddressId = defaultAddress.Id,
Amount = totalAmount,
TransactionId = transaction.Id,
PaymentStatus = CMSMicroservice.Domain.Enums.PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
PaymentMethod = CMSMicroservice.Domain.Enums.PaymentMethod.Wallet,
DeliveryStatus = CMSMicroservice.Domain.Enums.DeliveryStatus.Pending,
HasVAT = true
};
_context.UserOrders.Add(order);
await _context.SaveChangesAsync(context.CancellationToken);
// Update transaction description with order ID
transaction.Description = $"خرید محصولات - سفارش #{order.Id}";
await _context.SaveChangesAsync(context.CancellationToken);
// Create order VAT record
var orderVat = new OrderVAT
{
OrderId = order.Id,
VATRate = vatRate,
BaseAmount = baseAmount,
VATAmount = vatAmount,
TotalAmount = totalAmount
};
_context.OrderVATs.Add(orderVat);
// Create factor details for each cart item
foreach (var cartItem in cartItems)
{
var factorDetail = new FactorDetails
{
OrderId = order.Id,
ProductId = cartItem.ProductId,
Count = cartItem.Count,
UnitPrice = cartItem.Product.Price,
UnitDiscount = 0,
UnitDiscountPrice = cartItem.Product.Price,
IsChangePrice = false
};
_context.FactorDetails.Add(factorDetail);
}
await _context.SaveChangesAsync(context.CancellationToken);
// Clear user's cart
foreach (var cartItem in cartItems)
{
cartItem.IsDeleted = true;
}
await _context.SaveChangesAsync(context.CancellationToken);
// Deduct inventory and update SaleCount for each purchased item
foreach (var cartItem in cartItems)
{
await _inventoryService.ConfirmSaleAsync(
cartItem.ProductId, ProductType.RegularProduct,
cartItem.Count, order.Id, context.CancellationToken);
cartItem.Product.SaleCount += cartItem.Count;
}
await _context.SaveChangesAsync(context.CancellationToken);
return new SubmitShopBuyOrderResponse
{
Id = order.Id
};
}
[RequiresPermission(PermissionNames.OrdersCancel)]
public override async Task<CancelOrderResponse> CancelOrder(CancelOrderRequest request, ServerCallContext context)
{
var command = new CancelOrderByAdminCommand
{
OrderId = request.OrderId,
CancelReason = request.CancelReason,
RefundToWallet = request.RefundPayment
};
await _sender.Send(command, context.CancellationToken);
return new CancelOrderResponse
{
OrderId = request.OrderId,
Status = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)(int)Domain.Enums.DeliveryStatus.Cancelled,
Message = "سفارش با موفقیت لغو شد",
RefundProcessed = request.RefundPayment
};
}
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<UpdateOrderStatusResponse> UpdateOrderStatus(UpdateOrderStatusRequest request, ServerCallContext context)
{
var order = await _context.UserOrders.FindAsync(new object[] { request.OrderId }, context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
var oldStatus = (int)order.DeliveryStatus;
var command = new Application.OrderManagementCQ.Commands.UpdateOrderStatus.UpdateOrderStatusCommand
{
OrderId = request.OrderId,
NewStatus = (Domain.Enums.DeliveryStatus)request.NewStatus
};
await _sender.Send(command, context.CancellationToken);
return new UpdateOrderStatusResponse
{
Success = true,
Message = "وضعیت سفارش با موفقیت تغییر کرد",
OldStatus = oldStatus,
NewStatus = request.NewStatus
};
}
#region [ARCHIVED] GetOrdersByDateRange تکراری: GetAllUserOrderByFilter همین قابلیت + فیلترهای بیشتر دارد
[RequiresPermission(PermissionNames.ReportsView)]
[Obsolete("ARCHIVED: Use GetAllUserOrderByFilter instead — has date range + more filters")]
public override async Task<GetOrdersByDateRangeResponse> GetOrdersByDateRange(GetOrdersByDateRangeRequest request, ServerCallContext context)
{
var pageNumber = request.PageNumber > 0 ? request.PageNumber : 1;
var pageSize = request.PageSize > 0 ? request.PageSize : 20;
var query = _context.UserOrders
.Include(o => o.User)
.Include(o => o.FactorDetails)
.Where(o => !o.IsDeleted);
if (request.StartDate != null)
query = query.Where(o => o.Created >= request.StartDate.ToDateTime());
if (request.EndDate != null)
query = query.Where(o => o.Created <= request.EndDate.ToDateTime());
if (request.Status != null)
query = query.Where(o => (int)o.DeliveryStatus == request.Status.Value);
if (request.UserId != null)
query = query.Where(o => o.UserId == request.UserId.Value);
var totalCount = await query.CountAsync(context.CancellationToken);
var orders = await query
.OrderByDescending(o => o.Created)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(o => new OrderSummaryDto
{
OrderId = o.Id,
OrderNumber = $"ORD-{o.Id:D6}",
UserId = o.UserId,
UserFullName = o.User != null ? (o.User.FirstName + " " + o.User.LastName) : string.Empty,
TotalAmount = o.Amount,
Status = (int)o.DeliveryStatus,
StatusName = o.DeliveryStatus.ToString(),
ItemsCount = o.FactorDetails.Count,
CreatedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(o.Created, DateTimeKind.Utc))
})
.ToListAsync(context.CancellationToken);
return new GetOrdersByDateRangeResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = pageNumber,
PageSize = pageSize,
TotalCount = totalCount,
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
HasPrevious = pageNumber > 1,
HasNext = pageNumber * pageSize < totalCount
},
Orders = { orders }
};
}
#endregion
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<ApplyDiscountToOrderResponse> ApplyDiscountToOrder(ApplyDiscountToOrderRequest request, ServerCallContext context)
{
var order = await _context.UserOrders.FindAsync(new object[] { request.OrderId }, context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
if (order.PaymentStatus == Domain.Enums.PaymentStatus.Success)
return new ApplyDiscountToOrderResponse
{
Success = false,
Message = "امکان اعمال تخفیف برای سفارش پرداخت شده وجود ندارد"
};
var originalAmount = order.Amount;
var discountAmount = Math.Min(request.DiscountAmount, originalAmount);
order.Amount = originalAmount - discountAmount;
await _context.SaveChangesAsync(context.CancellationToken);
return new ApplyDiscountToOrderResponse
{
Success = true,
Message = $"تخفیف {discountAmount:N0} تومان با موفقیت اعمال شد",
OriginalAmount = originalAmount,
DiscountAmount = discountAmount,
FinalAmount = order.Amount
};
}
// [RequiresPermission(PermissionNames.OrdersView)]
public override async Task<CalculateOrderPVResponse> CalculateOrderPV(CalculateOrderPVRequest request, ServerCallContext context)
{
var order = await _context.UserOrders
.Include(o => o.FactorDetails)
.ThenInclude(fd => fd.Product)
.Where(o => o.Id == request.OrderId && !o.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
var products = new List<ProductPVDto>();
long totalPV = 0;
foreach (var fd in order.FactorDetails.Where(f => !f.IsDeleted))
{
// PV = UnitPrice * Count (simplified - can be customized)
long unitPV = fd.UnitPrice;
long itemPV = unitPV * fd.Count;
totalPV += itemPV;
products.Add(new ProductPVDto
{
ProductId = fd.ProductId,
ProductTitle = fd.Product?.Title ?? string.Empty,
Quantity = fd.Count,
UnitPv = unitPV,
TotalPv = itemPV
});
}
return new CalculateOrderPVResponse
{
OrderId = request.OrderId,
TotalPv = totalPV,
Products = { products }
};
}
// ============= Customer-specific Methods =============
#region [ARCHIVED] CreateNewOrderForCustomer & SubmitOrderForCustomer Stub خالی: مسیر سفارش مشتری از SubmitShopBuyOrder می‌گذرد
[Obsolete("ARCHIVED: Empty stub — customer order flow uses SubmitShopBuyOrder")]
public override async Task<CreateNewUserOrderResponse> CreateNewOrderForCustomer(CreateNewUserOrderRequest request, ServerCallContext context)
{
// [ARCHIVED] Empty stub — مسیر سفارش مشتری از SubmitShopBuyOrder می‌گذرد
return new CreateNewUserOrderResponse();
}
[Obsolete("ARCHIVED: Empty stub — customer order flow uses SubmitShopBuyOrder")]
public override async Task<SubmitShopBuyOrderResponse> SubmitOrderForCustomer(SubmitShopBuyOrderRequest request, ServerCallContext context)
{
// [ARCHIVED] Empty stub — مسیر سفارش مشتری از SubmitShopBuyOrder می‌گذرد
return new SubmitShopBuyOrderResponse();
}
#endregion
public override async Task<GetAllUserOrderByFilterResponse> GetCustomerOrders(GetAllUserOrderByFilterRequest request, ServerCallContext context)
{
// Customer endpoint: ALWAYS resolve userId from JWT (customer can only see own orders)
var customerUserId = long.TryParse(_currentUserService.UserId, out var uid) ? uid : 0;
if (customerUserId == 0)
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
var query = new GetCustomerOrdersQuery
{
UserId = customerUserId,
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
PaymentStatusFilter = request.Filter?.HasPaymentStatus == true
? (int?)request.Filter.PaymentStatus
: null,
DeliveryStatusFilter = request.Filter?.HasDeliveryStatus == true
? (int?)request.Filter.DeliveryStatus
: null,
FromDate = request.Filter?.FromDate?.ToDateTime() ?? request.Filter?.PaymentDate?.ToDateTime(),
ToDate = request.Filter?.ToDate?.ToDateTime()
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetAllUserOrderByFilterResponse
{
MetaData = result.MetaData.Adapt<MetaData>()
};
foreach (var model in result.Models)
{
var orderModel = new GetAllUserOrderByFilterResponseModel
{
Id = model.Id,
Amount = model.Amount,
PackageId = model.PackageId ?? 0,
TransactionId = model.TransactionId,
UserId = model.UserId,
UserAddressId = model.UserAddressId,
UserAddressText = model.UserAddressText,
TrackingCode = model.TrackingCode,
DeliveryDescription = model.DeliveryDescription,
UserFullName = model.UserFullName,
UserNationalCode = model.UserNationalCode,
VatAmount = model.VatAmount,
VatPercentage = model.VatPercentage
};
orderModel.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)model.PaymentStatus;
if (model.PaymentDate.HasValue)
orderModel.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(model.PaymentDate.Value, DateTimeKind.Utc));
if (model.PaymentMethod.HasValue)
orderModel.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)model.PaymentMethod.Value;
orderModel.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)model.DeliveryStatus;
foreach (var fd in model.FactorDetails)
{
orderModel.FactorDetails.Add(new GetAllUserOrderByFilterResponseModelFactorDetail
{
ProductId = fd.ProductId,
ProductTitle = fd.ProductTitle,
ProductThumbnailPath = fd.ProductThumbnailPath,
UnitPrice = fd.UnitPrice,
Count = fd.Count,
UnitDiscountPrice = fd.UnitDiscountPrice
});
}
response.Models.Add(orderModel);
}
return response;
}
public override async Task<GetUserOrderResponse> GetCustomerOrder(GetUserOrderRequest request, ServerCallContext context)
{
// Customer endpoint: ALWAYS resolve userId from JWT (customer can only see own orders)
var customerUserId = long.TryParse(_currentUserService.UserId, out var uid) ? uid : 0;
if (customerUserId == 0)
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
var query = new GetCustomerOrderQuery
{
OrderId = request.Id,
UserId = customerUserId
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetUserOrderResponse
{
Id = result.Id,
Amount = result.Amount,
PackageId = result.PackageId ?? 0,
TransactionId = result.TransactionId,
UserId = result.UserId,
UserAddressId = result.UserAddressId,
UserAddressText = result.UserAddressText,
TrackingCode = result.TrackingCode,
DeliveryDescription = result.DeliveryDescription,
UserFullName = result.UserFullName,
UserNationalCode = result.UserNationalCode
};
// VAT Info
if (result.VatAmount > 0)
{
response.VatInfo = new OrderVATInfo
{
VatRate = result.VatPercentage / 100,
BaseAmount = result.Amount - result.VatAmount,
VatAmount = result.VatAmount,
TotalAmount = result.Amount,
IsPaid = result.PaymentStatus == Domain.Enums.PaymentStatus.Success
};
}
response.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)result.PaymentStatus;
if (result.PaymentDate.HasValue)
response.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(result.PaymentDate.Value, DateTimeKind.Utc));
if (result.PaymentMethod.HasValue)
response.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)result.PaymentMethod.Value;
response.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)result.DeliveryStatus;
foreach (var fd in result.FactorDetails)
{
response.FactorDetails.Add(new GetUserOrderResponseFactorDetail
{
ProductId = fd.ProductId,
ProductTitle = fd.ProductTitle,
ProductThumbnailPath = fd.ProductThumbnailPath,
UnitPrice = fd.UnitPrice,
Count = fd.Count,
UnitDiscountPrice = fd.UnitDiscountPrice
});
}
return response;
}
// ============= Customer-specific Method Implementations =============
public override async Task<CustomerCancelOrderResponse> CustomerCancelOrder(CustomerCancelOrderRequest request, ServerCallContext context)
{
var order = await _context.UserOrders
.Where(o => o.Id == request.OrderId && !o.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (order == null)
return new CustomerCancelOrderResponse { Success = false, Message = "سفارش یافت نشد" };
if (order.DeliveryStatus != Domain.Enums.DeliveryStatus.Pending)
return new CustomerCancelOrderResponse { Success = false, Message = "فقط سفارش‌های در انتظار قابل لغو هستند" };
var refundAmount = order.Amount;
order.DeliveryStatus = Domain.Enums.DeliveryStatus.Cancelled;
// Refund to wallet if payment was from wallet
if (order.PaymentStatus == Domain.Enums.PaymentStatus.Success && order.PaymentMethod == Domain.Enums.PaymentMethod.Wallet)
{
var wallet = await _context.UserWallets
.Where(w => w.UserId == order.UserId)
.FirstOrDefaultAsync(context.CancellationToken);
if (wallet != null)
{
wallet.Balance += refundAmount;
_context.UserWalletHistories.Add(new UserWalletHistory
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = refundAmount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = 0,
IsIncrease = true,
RefrenceId = order.TransactionId ?? 0
});
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerCancelOrderResponse
{
Success = true,
Message = "سفارش شما با موفقیت لغو شد",
RefundAmount = refundAmount,
RefundTransactionId = $"REF{order.Id}"
};
}
public override async Task<GetCustomerOrderHistoryResponse> GetCustomerOrderHistory(GetCustomerOrderHistoryRequest request, ServerCallContext context)
{
var query = new GetCustomerOrderHistoryQuery
{
UserId = request.UserId,
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
StatusFilter = request.StatusFilter != OrderStatusEnum.OrderStatusPending
? (int?)request.StatusFilter
: null,
FromDate = request.FromDate?.ToDateTime(),
ToDate = request.ToDate?.ToDateTime()
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerOrderHistoryResponse
{
MetaData = result.MetaData.Adapt<MetaData>()
};
foreach (var order in result.Orders)
{
response.Orders.Add(new Protobuf.Protos.UserOrder.CustomerOrderModel
{
Id = order.Id,
Amount = order.Amount,
PackageId = order.PackageId ?? 0,
PackageName = order.PackageName,
Status = (OrderStatusEnum)order.Status,
StatusMessage = order.StatusMessage,
OrderDate = Timestamp.FromDateTime(DateTime.SpecifyKind(order.OrderDate, DateTimeKind.Utc)),
DeliveryDate = order.DeliveryDate.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(order.DeliveryDate.Value, DateTimeKind.Utc))
: null,
TrackingCode = order.TrackingCode,
ItemsCount = order.ItemsCount,
CanCancel = order.CanCancel,
CanReorder = order.CanReorder
});
}
return response;
}
public override async Task<CustomerTrackOrderResponse> CustomerTrackOrder(CustomerTrackOrderRequest request, ServerCallContext context)
{
var order = await _context.UserOrders
.Include(o => o.FactorDetails)
.Include(o => o.Package)
.Where(o => o.Id == request.OrderId && !o.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
var orderModel = new Protobuf.Protos.UserOrder.CustomerOrderModel
{
Id = order.Id,
Amount = order.Amount,
PackageId = order.PackageId ?? 0,
PackageName = order.Package?.Title ?? string.Empty,
Status = (OrderStatusEnum)(int)order.DeliveryStatus,
StatusMessage = GetDeliveryStatusPersian(order.DeliveryStatus),
OrderDate = Timestamp.FromDateTime(DateTime.SpecifyKind(order.Created, DateTimeKind.Utc)),
TrackingCode = order.TrackingCode ?? string.Empty,
ItemsCount = order.FactorDetails.Count(f => !f.IsDeleted),
CanCancel = order.DeliveryStatus == Domain.Enums.DeliveryStatus.Pending,
CanReorder = order.DeliveryStatus == Domain.Enums.DeliveryStatus.Delivered
};
var response = new CustomerTrackOrderResponse
{
Order = orderModel,
DeliveryInfo = new DeliveryTrackingInfo
{
TrackingCode = order.TrackingCode ?? string.Empty,
CourierName = "پست پیشتاز",
EstimatedDelivery = order.DeliveryDescription ?? string.Empty,
CurrentLocation = string.Empty
}
};
// Add current status to history
response.StatusHistory.Add(new OrderStatusHistory
{
Status = (OrderStatusEnum)(int)order.DeliveryStatus,
StatusMessage = GetDeliveryStatusPersian(order.DeliveryStatus),
ChangedAt = order.LastModified.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(order.LastModified.Value, DateTimeKind.Utc))
: Timestamp.FromDateTime(DateTime.SpecifyKind(order.Created, DateTimeKind.Utc)),
ChangedBy = "سیستم"
});
return response;
}
public override async Task<CustomerReorderResponse> CustomerReorderPreviousOrder(CustomerReorderRequest request, ServerCallContext context)
{
var originalOrder = await _context.UserOrders
.Include(o => o.FactorDetails)
.ThenInclude(fd => fd.Product)
.Where(o => o.Id == request.OriginalOrderId && !o.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (originalOrder == null)
return new CustomerReorderResponse { Success = false, Message = "سفارش اصلی یافت نشد" };
var userId = originalOrder.UserId;
// Add items from old order to cart
foreach (var fd in originalOrder.FactorDetails.Where(f => !f.IsDeleted))
{
var existingCartItem = await _context.UserCarts
.Where(c => c.UserId == userId && c.ProductId == fd.ProductId && !c.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (existingCartItem != null)
{
existingCartItem.Count += fd.Count;
}
else
{
_context.UserCarts.Add(new UserCart
{
UserId = userId,
ProductId = fd.ProductId,
Count = fd.Count
});
}
}
await _context.SaveChangesAsync(context.CancellationToken);
// Calculate total with current prices
long totalAmount = originalOrder.FactorDetails
.Where(f => !f.IsDeleted)
.Sum(fd => request.UseCurrentPrices && fd.Product != null
? fd.Product.Price * fd.Count
: fd.UnitPrice * fd.Count);
return new CustomerReorderResponse
{
Success = true,
Message = "محصولات به سبد خرید اضافه شدند",
NewOrderId = 0, // Cart items added, no order created yet
TotalAmount = totalAmount
};
}
public override Task<GetVATRateResponse> GetVATRate(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context)
{
// VAT Rate for Iran: 10% (نرخ مالیات بر ارزش افزوده ایران)
return Task.FromResult(new GetVATRateResponse
{
VatRate = 0.10,
VatPercentage = 10,
IsEnabled = true
});
}
// ============= Helper Methods =============
private static string GetDeliveryStatusPersian(Domain.Enums.DeliveryStatus status) => status switch
{
Domain.Enums.DeliveryStatus.None => "نامشخص",
Domain.Enums.DeliveryStatus.Pending => "در انتظار ارسال",
Domain.Enums.DeliveryStatus.InTransit => "در حال ارسال",
Domain.Enums.DeliveryStatus.Delivered => "تحویل داده شده",
Domain.Enums.DeliveryStatus.Returned => "مرجوع شده",
Domain.Enums.DeliveryStatus.Cancelled => "لغو شده",
_ => status.ToString()
};
}