feat: Implement Discount Shop Completion Plan with Product Image Gallery, Admin APIs, VAT Calculation, and Sales Reports
- Added DiscountProductImage entity and related configurations for product image gallery. - Created commands and queries for managing product images. - Developed GetAllDiscountOrders API for admin order management with various filters. - Implemented VAT calculation service and integrated it into order processing. - Created Sales Reports API with support for daily, weekly, and monthly reports. - Completed gRPC services for BackOffice.BFF to expose new APIs. - Updated Proto files and project references accordingly.
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
# عضویت دستی باشگاه مشتریان - Manual Club Membership
|
||||
|
||||
## 📋 خلاصه نیازمندی
|
||||
|
||||
ادمین بتواند برای یک کاربر **عضویت دستی باشگاه مشتریان** ایجاد کند که:
|
||||
- کیف پول با **56 میلیون (Balance)** + **112 میلیون (DiscountBalance)** شارژ شود
|
||||
- تراکنش و لاگ کیف پول ثبت شود
|
||||
- فیلد `User.PackagePurchaseMethod = DirectPurchase` تنظیم شود
|
||||
- مسیر تصویر فیش واریزی ذخیره شود
|
||||
- بدون نیاز به تایید دو مرحلهای (ادمین ایجاد میکند = تایید شده)
|
||||
|
||||
---
|
||||
|
||||
## 🔢 فرمولهای محاسبه
|
||||
|
||||
```
|
||||
BasePackageAmount = 56,000,000 ریال (SystemConstants)
|
||||
|
||||
Balance (شارژ اصلی) = BasePackageAmount = 56M
|
||||
DiscountBalance (تخفیف) = BasePackageAmount × 2 = 112M
|
||||
|
||||
مجموع شارژ = 56M + 112M = 168M ریال
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 فایلهای مورد نیاز برای تغییر
|
||||
|
||||
| # | فایل | نوع تغییر | اولویت |
|
||||
|---|------|----------|--------|
|
||||
| 1 | `ManualPayment.cs` | اضافه کردن `ImagePath` | بالا |
|
||||
| 2 | `CreateManualPaymentCommand.cs` | اضافه کردن `ImagePath` | بالا |
|
||||
| 3 | `manualpayment.proto` (CMS) | اضافه کردن `image_path` | بالا |
|
||||
| 4 | `manualpayment.proto` (BFF) | اضافه کردن `image_path` | بالا |
|
||||
| 5 | `CreateManualPaymentCommandHandler.cs` (CMS) | بازنویسی کامل | بالا |
|
||||
| 6 | `CreateManualPaymentCommandHandler.cs` (BFF) | اضافه کردن `ImagePath` | متوسط |
|
||||
| 7 | **جدید:** `GetManualMembershipPaymentsQuery` | Query برای لیست | کم |
|
||||
|
||||
---
|
||||
|
||||
## ✅ تسک 1: اضافه کردن ImagePath به Entity
|
||||
|
||||
**فایل:** `CMS/src/CMSMicroservice.Domain/Entities/Payment/ManualPayment.cs`
|
||||
|
||||
**تغییر:** بعد از `ReferenceNumber` اضافه شود:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// مسیر تصویر فیش واریزی (اختیاری)
|
||||
/// </summary>
|
||||
public string? ImagePath { get; set; }
|
||||
```
|
||||
|
||||
**محل دقیق:**
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// شماره مرجع یا شماره فیش (اختیاری)
|
||||
/// </summary>
|
||||
public string? ReferenceNumber { get; set; }
|
||||
|
||||
// ⬇️ اینجا اضافه شود ⬇️
|
||||
/// <summary>
|
||||
/// مسیر تصویر فیش واریزی (اختیاری)
|
||||
/// </summary>
|
||||
public string? ImagePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// وضعیت تایید
|
||||
/// </summary>
|
||||
public ManualPaymentStatus Status { get; set; } = ManualPaymentStatus.Pending;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ تسک 2: اضافه کردن ImagePath به Command
|
||||
|
||||
**فایل:** `CMS/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommand.cs`
|
||||
|
||||
**تغییر:** بعد از `ReferenceNumber` اضافه شود:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// مسیر تصویر فیش واریزی (اختیاری)
|
||||
/// </summary>
|
||||
public string? ImagePath { get; set; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ تسک 3: آپدیت Proto - CMS
|
||||
|
||||
**فایل:** `CMS/src/CMSMicroservice.Protobuf/Protos/manualpayment.proto`
|
||||
|
||||
**تغییر در `CreateManualPaymentRequest`:**
|
||||
|
||||
```protobuf
|
||||
message CreateManualPaymentRequest
|
||||
{
|
||||
int64 user_id = 1;
|
||||
int64 amount = 2;
|
||||
ManualPaymentType type = 3;
|
||||
string description = 4;
|
||||
google.protobuf.StringValue reference_number = 5;
|
||||
google.protobuf.StringValue image_path = 6; // ⬅️ اضافه شود
|
||||
}
|
||||
```
|
||||
|
||||
**تغییر در `ManualPaymentModel`:**
|
||||
|
||||
```protobuf
|
||||
message ManualPaymentModel
|
||||
{
|
||||
// ... existing fields ...
|
||||
google.protobuf.Timestamp created = 19;
|
||||
google.protobuf.StringValue image_path = 20; // ⬅️ اضافه شود
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ تسک 4: آپدیت Proto - BFF
|
||||
|
||||
**فایل:** `BackOffice.BFF/src/Protobufs/BackOffice.BFF.ManualPayment.Protobuf/Protos/manualpayment.proto`
|
||||
|
||||
**همان تغییرات تسک 3**
|
||||
|
||||
---
|
||||
|
||||
## ✅ تسک 5: بازنویسی Handler (CMS) - مهمترین تسک
|
||||
|
||||
**فایل:** `CMS/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs`
|
||||
|
||||
**کد جدید کامل:**
|
||||
|
||||
```csharp
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Common;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.ManualPaymentCQ.Commands.CreateManualPayment;
|
||||
|
||||
public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPaymentCommand, long>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly ILogger<CreateManualPaymentCommandHandler> _logger;
|
||||
|
||||
public CreateManualPaymentCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUser,
|
||||
ILogger<CreateManualPaymentCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<long> Handle(
|
||||
CreateManualPaymentCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Creating manual membership payment for UserId: {UserId}, Type: {Type}",
|
||||
request.UserId,
|
||||
request.Type
|
||||
);
|
||||
|
||||
// 1. بررسی Admin فعلی
|
||||
var currentUserId = _currentUser.UserId;
|
||||
if (string.IsNullOrEmpty(currentUserId))
|
||||
{
|
||||
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
|
||||
}
|
||||
|
||||
if (!long.TryParse(currentUserId, out var adminUserId))
|
||||
{
|
||||
throw new UnauthorizedAccessException("شناسه کاربر نامعتبر است");
|
||||
}
|
||||
|
||||
// 2. بررسی وجود کاربر
|
||||
var user = await _context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found: {UserId}", request.UserId);
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
}
|
||||
|
||||
// 3. پیدا کردن کیف پول
|
||||
var wallet = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken);
|
||||
|
||||
if (wallet == null)
|
||||
{
|
||||
_logger.LogError("Wallet not found for UserId: {UserId}", request.UserId);
|
||||
throw new NotFoundException($"کیف پول کاربر {request.UserId} یافت نشد");
|
||||
}
|
||||
|
||||
// 4. محاسبه مبالغ
|
||||
var balanceAmount = SystemConstants.BasePackageAmount; // 56M
|
||||
var discountBalanceAmount = SystemConstants.BasePackageAmount * 2; // 112M
|
||||
var totalAmount = balanceAmount + discountBalanceAmount; // 168M
|
||||
|
||||
// 5. ثبت تراکنش
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Amount = totalAmount,
|
||||
Description = $"عضویت دستی باشگاه مشتریان - {request.Description} - مرجع: {request.ReferenceNumber}",
|
||||
PaymentStatus = PaymentStatus.Success,
|
||||
PaymentDate = DateTime.Now,
|
||||
RefId = request.ReferenceNumber,
|
||||
Type = TransactionType.DepositExternal1
|
||||
};
|
||||
|
||||
_context.Transactions.Add(transaction);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 6. ایجاد ManualPayment با وضعیت Approved (بدون نیاز به تایید دو مرحلهای)
|
||||
var manualPayment = new ManualPayment
|
||||
{
|
||||
UserId = request.UserId,
|
||||
Amount = totalAmount,
|
||||
Type = request.Type,
|
||||
Description = request.Description,
|
||||
ReferenceNumber = request.ReferenceNumber,
|
||||
ImagePath = request.ImagePath,
|
||||
Status = ManualPaymentStatus.Approved,
|
||||
RequestedBy = adminUserId,
|
||||
ApprovedBy = adminUserId,
|
||||
ApprovedAt = DateTime.Now,
|
||||
TransactionId = transaction.Id
|
||||
};
|
||||
|
||||
_context.ManualPayments.Add(manualPayment);
|
||||
|
||||
// 7. اعمال تغییرات بر کیف پول
|
||||
var oldBalance = wallet.Balance;
|
||||
var oldDiscountBalance = wallet.DiscountBalance;
|
||||
|
||||
wallet.Balance += balanceAmount; // +56M
|
||||
wallet.DiscountBalance += discountBalanceAmount; // +112M
|
||||
|
||||
// 8. ثبت لاگ کیف پول
|
||||
var walletLog = new UserWalletChangeLog
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
ChangeValue = balanceAmount,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
ChangeDiscountValue = discountBalanceAmount,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
};
|
||||
|
||||
await _context.UserWalletChangeLogs.AddAsync(walletLog, cancellationToken);
|
||||
|
||||
// 9. تنظیم روش خرید پکیج
|
||||
user.PackagePurchaseMethod = PackagePurchaseMethod.DirectPurchase;
|
||||
|
||||
// 10. ذخیره همه تغییرات
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Manual membership payment created successfully. " +
|
||||
"ManualPaymentId: {Id}, UserId: {UserId}, TransactionId: {TransactionId}, " +
|
||||
"Balance: {OldBalance} -> {NewBalance}, DiscountBalance: {OldDiscount} -> {NewDiscount}",
|
||||
manualPayment.Id,
|
||||
request.UserId,
|
||||
transaction.Id,
|
||||
oldBalance,
|
||||
wallet.Balance,
|
||||
oldDiscountBalance,
|
||||
wallet.DiscountBalance
|
||||
);
|
||||
|
||||
return manualPayment.Id;
|
||||
}
|
||||
catch (Exception ex) when (ex is not NotFoundException && ex is not UnauthorizedAccessException)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error creating manual membership payment for UserId: {UserId}",
|
||||
request.UserId
|
||||
);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ تسک 6: آپدیت Handler (BFF)
|
||||
|
||||
**فایل:** `BackOffice.BFF/src/BackOffice.BFF.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs`
|
||||
|
||||
**تغییر:** اضافه کردن `ImagePath` به gRPC request:
|
||||
|
||||
```csharp
|
||||
var grpcRequest = new CreateManualPaymentRequest
|
||||
{
|
||||
UserId = request.UserId,
|
||||
Amount = request.Amount,
|
||||
Type = (ManualPaymentType)request.Type,
|
||||
Description = request.Description
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.ReferenceNumber))
|
||||
{
|
||||
grpcRequest.ReferenceNumber = request.ReferenceNumber;
|
||||
}
|
||||
|
||||
// ⬇️ اضافه شود ⬇️
|
||||
if (!string.IsNullOrWhiteSpace(request.ImagePath))
|
||||
{
|
||||
grpcRequest.ImagePath = request.ImagePath;
|
||||
}
|
||||
```
|
||||
|
||||
**همچنین:** فایل `CreateManualPaymentCommand.cs` در BFF هم باید `ImagePath` اضافه شود.
|
||||
|
||||
---
|
||||
|
||||
## ✅ تسک 7: ایجاد Query برای لیست (اختیاری)
|
||||
|
||||
**فایلهای جدید:**
|
||||
- `GetManualMembershipPaymentsQuery.cs`
|
||||
- `GetManualMembershipPaymentsQueryHandler.cs`
|
||||
- `ManualMembershipPaymentDto.cs`
|
||||
|
||||
> این تسک **اختیاری** است چون در حال حاضر `GetAllManualPayments` وجود دارد که میتواند با فیلتر `Type` استفاده شود.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 ترتیب اجرای تسکها
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[1. Entity - ImagePath] --> B[2. Command - ImagePath]
|
||||
B --> C[3. Proto CMS - image_path]
|
||||
C --> D[4. Proto BFF - image_path]
|
||||
D --> E[5. CMS Handler - Full Rewrite]
|
||||
E --> F[6. BFF Handler - ImagePath]
|
||||
F --> G[7. Build & Test]
|
||||
G --> H[8. Query - اختیاری]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 نکات مهم
|
||||
|
||||
### 1. تفاوت با ProcessManualMembershipPayment
|
||||
| معیار | CreateManualPayment (این تسک) | ProcessManualMembershipPayment |
|
||||
|-------|------------------------------|--------------------------------|
|
||||
| کاربرد | ادمین ایجاد میکند | مشتری از طریق درگاه پرداخت میکند |
|
||||
| Amount | از `SystemConstants` (ثابت) | از `request` (متغیر) |
|
||||
| DiscountBalance | `BasePackageAmount × 2` | `Amount` (همان مبلغ) |
|
||||
| ImagePath | ✅ دارد | ❌ ندارد |
|
||||
|
||||
### 2. مقادیر SystemConstants
|
||||
```csharp
|
||||
// فایل: CMSMicroservice.Domain/Common/SystemConstants.cs
|
||||
public const long BasePackageAmount = 56_000_000; // 56 میلیون ریال
|
||||
```
|
||||
|
||||
### 3. ManualPaymentType پیشنهادی
|
||||
برای این کاربرد میتوان از `CashDeposit` یا یک نوع جدید مثل `ClubMembership` استفاده کرد.
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ برآورد زمانی
|
||||
|
||||
| تسک | زمان تقریبی |
|
||||
|-----|-------------|
|
||||
| تسک 1-4 (فیلدها و Proto) | ~15 دقیقه |
|
||||
| تسک 5 (Handler CMS) | ~20 دقیقه |
|
||||
| تسک 6 (Handler BFF) | ~10 دقیقه |
|
||||
| Build & Test | ~10 دقیقه |
|
||||
| **مجموع** | **~55 دقیقه** |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 تست نهایی
|
||||
|
||||
بعد از اتمام تسکها:
|
||||
|
||||
1. **Build:** `dotnet build` در هر دو پروژه
|
||||
2. **Migration:** اگر نیاز بود برای `ImagePath`
|
||||
3. **تست API:** ایجاد یک Manual Payment برای کاربر تست
|
||||
4. **بررسی:** Balance و DiscountBalance کاربر
|
||||
|
||||
---
|
||||
|
||||
**تاریخ ایجاد:** 2026-01-01
|
||||
**نویسنده:** GitHub Copilot
|
||||
**وضعیت:** ⏳ در انتظار اجرا
|
||||
Reference in New Issue
Block a user