feat: implement VAT management service and integrate VAT calculations across components

This commit is contained in:
masoodafar-web
2025-12-19 00:30:33 +03:30
parent 6b457d0ce6
commit 9ee464b8b4
25 changed files with 751 additions and 268 deletions
+39 -11
View File
@@ -141,11 +141,22 @@
</MudHidden>
@if (DeviceDetector.IsMobile())
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"
Class="mobile-actions-stack px-5">
<MudText Typo="Typo.subtitle2"> تخفیف کل: <b>@FormatPrice(CartData.TotalDiscount)</b></MudText>
<MudText Typo="Typo.h5"> مبلغ کل: <b>@FormatPrice(CartData.Total)</b></MudText>
<MudStack Spacing="1" Class="mobile-actions-stack px-5">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">جمع کالاها:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(CartData.Total) تومان</MudText>
</MudStack>
@if (VAT.IsEnabled)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">مالیات (@VAT.VatPercentage%):</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(VATAmount) تومان</MudText>
</MudStack>
}
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Style="font-weight:bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary" Style="font-weight:bold;">@FormatPrice(TotalWithVAT) تومان</MudText>
</MudStack>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" Spacing="2" Class="mobile-actions-stack">
@@ -159,12 +170,29 @@
}
else
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"
Class="mobile-actions-stack px-5">
<MudText Typo="Typo.subtitle2"> تخفیف کل: <b>@FormatPrice(CartData.TotalDiscount)</b></MudText>
<MudText Typo="Typo.h5"> مبلغ کل: <b>@FormatPrice(CartData.Total)</b></MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
<MudPaper Class="pa-4 rounded-lg" Style="background-color: var(--mud-palette-background-grey);">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کالاها:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(CartData.Total) تومان</MudText>
</MudStack>
@if (VAT.IsEnabled)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage%):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(VATAmount) تومان</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight:bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight:bold;">@FormatPrice(TotalWithVAT) تومان</MudText>
</MudStack>
</MudStack>
</MudPaper>
<MudStack Row="true" Justify="Justify.FlexEnd" Spacing="2" Class="mt-3">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Products)">افزودن محصول
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Success" OnClick="ProceedCheckout"
+12 -2
View File
@@ -7,12 +7,16 @@ namespace FrontOffice.Main.Pages.Store;
public partial class Cart : ComponentBase, IDisposable
{
[Inject] private CartService CartService { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
// Navigation and Snackbar are available via _Imports.razor
private CartService CartData => CartService;
protected override void OnInitialized()
protected override async Task OnInitializedAsync()
{
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
await CartService.EnsureInitializedAsync();
CartService.OnChange += StateHasChanged;
await VAT.LoadAsync();
}
private async Task IncrementQty(long productId)
@@ -44,7 +48,13 @@ public partial class Cart : ComponentBase, IDisposable
Navigation.NavigateTo(RouteConstants.Store.CheckoutSummary);
}
private static string FormatPrice(long price) => string.Format("{0:N0} ", price);
private string FormatPrice(long price) => $"{price:N0} ";
private string FormatPriceWithVAT(long price) => $"{VAT.AddVAT(price):N0} ";
private long TotalWithVAT => VAT.AddVAT(CartData.Total);
private long VATAmount => VAT.CalculateVAT(CartData.Total);
private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl;
@@ -108,14 +108,17 @@
<MudText Typo="Typo.body2">جمع کالاها:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(Cart.Total)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (۹%):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(CalculateVAT())</MudText>
</MudStack>
@if (VAT.IsEnabled)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage%):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(CalculateVAT())</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(Cart.Total + CalculateVAT())</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(VAT.AddVAT(Cart.Total))</MudText>
</MudStack>
</MudStack>
@@ -10,6 +10,7 @@ public partial class CheckoutSummary : ComponentBase
{
[Inject] private CartService Cart { get; set; } = default!;
[Inject] private OrderService OrderService { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
// Snackbar and Navigation are injected via _Imports.razor
@@ -25,8 +26,11 @@ public partial class CheckoutSummary : ComponentBase
protected override async Task OnInitializedAsync()
{
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
await Cart.EnsureInitializedAsync();
await LoadAddresses();
await LoadWalletBalance();
await VAT.LoadAsync();
}
private async Task LoadWalletBalance()
@@ -92,9 +96,9 @@ public partial class CheckoutSummary : ComponentBase
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
/// <summary>
/// محاسبه مالیات بر ارزش افزوده (۹%)
/// محاسبه مالیات بر ارزش افزوده
/// </summary>
private long CalculateVAT() => (long)(Cart.Total * 0.09);
private long CalculateVAT() => VAT.CalculateVAT(Cart.Total);
private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl;
@@ -78,8 +78,6 @@ else
@* نمایش جزئیات مالی *@
@{
var subtotal = _order.FactorDetails.Sum(s => s.UnitPrice.Value * s.Count.Value);
var vatAmount = (long)(subtotal * 0.09);
var totalWithVat = subtotal + vatAmount;
}
<MudStack Spacing="1" Class="pa-2" Style="background-color: var(--mud-palette-background-grey); border-radius: 8px;">
<MudStack Row="true" Justify="Justify.SpaceBetween">
@@ -87,15 +85,26 @@ else
<MudText Typo="Typo.body2">@FormatPrice(subtotal)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (۹%):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(vatAmount)</MudText>
</MudStack>
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(totalWithVat)</MudText>
</MudStack>
@if (_order.VatInfo is not null)
{
var vatPercent = (int)(_order.VatInfo.VatRate * 100);
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@vatPercent%):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(_order.VatInfo.VatAmount)</MudText>
</MudStack>
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(_order.VatInfo.TotalAmount)</MudText>
</MudStack>
}
else
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(subtotal)</MudText>
</MudStack>
}
</MudStack>
</MudStack>
</MudPaper>
@@ -9,6 +9,7 @@ namespace FrontOffice.Main.Pages.Store;
public partial class Orders : ComponentBase
{
[Inject] private OrderService OrderService { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
private List<GetUserOrderResponse> _orders = new();
private bool _loading;
@@ -18,6 +19,7 @@ public partial class Orders : ComponentBase
_loading = true;
try
{
await VAT.LoadAsync();
_orders = await OrderService.GetOrdersAsync();
}
catch (Exception ex)
@@ -77,18 +77,51 @@ else
@* </MudStack> *@
@* } *@
<MudDivider Class="my-2"/>
<MudText Typo="Typo.h5" Color="Color.Primary">@FormatPrice(_product.Price)</MudText>
<MudStack Spacing="1">
<MudText Typo="Typo.h5" Color="Color.Primary">@FormatPrice(_product.Price)</MudText>
@if (VAT.IsEnabled)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
(شامل @VAT.VatPercentage% مالیات بر ارزش افزوده)
</MudText>
}
</MudStack>
<!-- نمایش وضعیت موجودی -->
@if (IsInStock)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" Class="me-1" />
موجود در انبار (@_product.RemainingCount عدد)
</MudChip>
}
else
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">
<MudIcon Icon="@Icons.Material.Filled.Cancel" Size="Size.Small" Class="me-1" />
ناموجود
</MudChip>
}
<!-- Desktop/tablet actions -->
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudNumericField T="int" @bind-Value="_qty" Min="@MinQty" Max="@MaxQty" Immediate="true"
HideSpinButtons="true" Style="max-width:120px"/>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.AddShoppingCart" OnClick="AddToCart">افزودن به
سبد
</MudButton>
</MudStack>
@if (IsInStock)
{
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudNumericField T="int" @bind-Value="_qty" Min="@MinQty" Max="@MaxQty" Immediate="true"
HideSpinButtons="true" Style="max-width:120px"/>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.AddShoppingCart" OnClick="AddToCart">افزودن به
سبد
</MudButton>
</MudStack>
}
else
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
این محصول در حال حاضر موجود نیست.
</MudAlert>
}
</MudHidden>
<!-- Mobile actions use sticky bar rendered below -->
@@ -103,51 +136,58 @@ else
<MudPaper Class="mud-width-full mud-elevation-3 pa-3"
Style="position:sticky;bottom:0;left:0;z-index:9;border-top-left-radius:1rem;border-top-right-radius:1rem;">
<MudGrid Class="align-center" Justify="Justify.SpaceBetween">
<MudItem xs="6">
<MudStack Spacing="1">
@if (HasDiscount && OriginalPrice is not null)
{
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudText Typo="Typo.caption"
Class="mud-text-secondary mud-line-through">@FormatPrice(OriginalPrice.Value)</MudText>
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
Label="true">@($"٪{_product!.Discount}")</MudChip>
</MudStack>
}
<MudText Typo="Typo.h6" Color="Color.Primary">@FormatPrice(TotalPrice)</MudText>
</MudStack>
</MudItem>
@if (IsInCart)
{
<MudItem xs="5" Class="">
<MudPaper Class="mud-width-full d-flex align-center justify-space-between px-2"
Elevation="1" Style="border-radius:1rem;">
<MudIconButton
Icon="@(CurrentCartQuantity == 1 ? Icons.Material.Filled.Delete : Icons.Material.Filled.Remove)"
Color="Color.Error" Variant="Variant.Text" OnClick="RemoveFromCart"/>
<MudText Typo="Typo.subtitle2" Class="mud-font-weight-bold">@CurrentCartQuantity</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Add" Color="Color.Primary"
Variant="Variant.Text" Disabled="@(CurrentCartQuantity >= MaxQty)"
OnClick="AddToCart"/>
</MudPaper>
</MudItem>
}
else
{
<MudItem xs="6" >
@if (IsInStock)
{
<MudGrid Class="align-center" Justify="Justify.SpaceBetween">
<MudItem xs="6">
<MudStack Spacing="1">
<MudButton Variant="Variant.Filled" Size="Size.Large" Color="Color.Error" Class="mud-width-full"
StartIcon="@Icons.Material.Filled.AddShoppingCart" OnClick="AddToCart">
افزودن به سبد
</MudButton>
@if (HasDiscount && OriginalPrice is not null)
{
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudText Typo="Typo.caption"
Class="mud-text-secondary mud-line-through">@FormatPrice(OriginalPrice.Value)</MudText>
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
Label="true">@($"٪{_product!.Discount}")</MudChip>
</MudStack>
}
<MudText Typo="Typo.h6" Color="Color.Primary">@FormatPrice(TotalPrice)</MudText>
</MudStack>
</MudItem>
}
</MudGrid>
@if (IsInCart)
{
<MudItem xs="5" Class="">
<MudPaper Class="mud-width-full d-flex align-center justify-space-between px-2"
Elevation="1" Style="border-radius:1rem;">
<MudIconButton
Icon="@(CurrentCartQuantity == 1 ? Icons.Material.Filled.Delete : Icons.Material.Filled.Remove)"
Color="Color.Error" Variant="Variant.Text" OnClick="RemoveFromCart"/>
<MudText Typo="Typo.subtitle2" Class="mud-font-weight-bold">@CurrentCartQuantity</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Add" Color="Color.Primary"
Variant="Variant.Text" Disabled="@(CurrentCartQuantity >= MaxQty)"
OnClick="AddToCart"/>
</MudPaper>
</MudItem>
}
else
{
<MudItem xs="6" >
<MudStack Spacing="1">
<MudButton Variant="Variant.Filled" Size="Size.Large" Color="Color.Error" Class="mud-width-full"
StartIcon="@Icons.Material.Filled.AddShoppingCart" OnClick="AddToCart">
افزودن به سبد
</MudButton>
</MudStack>
</MudItem>
}
</MudGrid>
}
else
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Filled" Dense="true" Class="mb-0">
این محصول ناموجود است
</MudAlert>
}
</MudPaper>
</MudHidden>
@@ -11,6 +11,7 @@ public partial class ProductDetail : ComponentBase, IDisposable
{
[Inject] private ProductService ProductService { get; set; } = default!;
[Inject] private CartService Cart { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
[Parameter] public long id { get; set; }
@@ -18,12 +19,18 @@ public partial class ProductDetail : ComponentBase, IDisposable
private bool _loading;
private int _qty = 1;
private const int MinQty = 1;
private const int MaxQty = 20;
// حداکثر تعداد قابل خرید بر اساس موجودی انبار
private int MaxQty => _product?.RemainingCount > 0 ? _product.RemainingCount : 0;
// آیا محصول موجود است
private bool IsInStock => _product is not null && _product.RemainingCount > 0;
private IReadOnlyList<ProductGalleryImage> _galleryItems = Array.Empty<ProductGalleryImage>();
private IReadOnlyList<ProductCategoryPathInfo> _categoryPaths = Array.Empty<ProductCategoryPathInfo>();
private ProductGalleryImage? _selectedGalleryImage;
private readonly List<BreadcrumbItem> _breadcrumbItems = new();
private long TotalPrice => (_product?.Price ?? 0) * _qty;
private long TotalPrice => VAT.AddVAT((_product?.Price ?? 0) * _qty);
private bool HasDiscount => _product is { Discount: > 0 and < 100 };
private long? OriginalPrice => HasDiscount && _product is not null
@@ -44,8 +51,13 @@ public partial class ProductDetail : ComponentBase, IDisposable
protected override async Task OnInitializedAsync()
{
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
await Cart.EnsureInitializedAsync();
Cart.OnChange += HandleCartChanged;
// بارگذاری نرخ VAT
await VAT.LoadAsync();
_loading = true;
_product = await ProductService.GetByIdAsync(id);
_loading = false;
@@ -127,7 +139,9 @@ public partial class ProductDetail : ComponentBase, IDisposable
Cart.OnChange -= HandleCartChanged;
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
private string FormatPriceWithoutVAT(long price) => $"{price:N0} تومان";
private static IReadOnlyList<ProductGalleryImage> BuildGalleryItems(Product product)
{
@@ -59,8 +59,20 @@
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<div style="height: 60%;background-image: url('@p.ImageUrl');background-size: cover; background-position: center;border-radius: 0.5rem">
<div style="height: 60%;background-image: url('@p.ImageUrl');background-size: cover; background-position: center;border-radius: 0.5rem; position: relative;">
@if (p.RemainingCount <= 0)
{
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
</div>
}
else if (p.RemainingCount <= 5)
{
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;right:8px;">
فقط @p.RemainingCount عدد
</MudChip>
}
</div>
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
<MudText Typo="Typo.subtitle1">@p.Title</MudText>
@@ -73,7 +85,9 @@
@* Href="@($"{RouteConstants.Store.ProductDetail}{p.Id}")">جزئیات *@
@* </MudButton> *@
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="() => AddToCart(p)"
StartIcon="@Icons.Material.Filled.AddShoppingCart">افزودن
StartIcon="@Icons.Material.Filled.AddShoppingCart"
Disabled="@(p.RemainingCount <= 0)">
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن")
</MudButton>
</MudCardActions>
</MudCard>
@@ -12,6 +12,7 @@ public partial class Products : ComponentBase, IDisposable
[Inject] private ProductService ProductService { get; set; } = default!;
[Inject] private CategoryService CategoryService { get; set; } = default!;
[Inject] private CartService Cart { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
private string _query = string.Empty;
private bool _loading;
@@ -21,8 +22,11 @@ public partial class Products : ComponentBase, IDisposable
protected override async Task OnInitializedAsync()
{
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
await Cart.EnsureInitializedAsync();
Cart.OnChange += StateHasChanged;
Navigation.LocationChanged += HandleLocationChanged;
await VAT.LoadAsync();
await Load();
}
@@ -47,7 +51,7 @@ public partial class Products : ComponentBase, IDisposable
await Cart.Add(p, 1);
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
public void Dispose()
{