feat: client-side currency conversion with admin-configurable rates
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

- CurrencyRatesService: RUB-based rates, persisted via localStorage
- CurrencyConvertPipe: impure pipe converting item price to selected currency
- Admin settings page: editable currency rates form
- Applied conversion to product-card, product-information, quick-view-dialog,
  delivery-information, compare-table, cart totals + payment payload

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-15 01:45:12 +04:00
parent d8c078ad5a
commit 4510eb769a
20 changed files with 235 additions and 30 deletions

View File

@@ -16,7 +16,7 @@
<span>{{ 'cart.deliveryTime' | translate }}: {{ option.deliveryTime }}</span>
}
</div>
<strong>{{ option.deliveryPrice | number:'1.2-2' }} {{ currency }}</strong>
<strong>{{ option.deliveryPrice | currencyConvert:currency | number:'1.2-2' }} {{ displayCurrency() }}</strong>
</div>
}
</div>

View File

@@ -1,17 +1,22 @@
import { DecimalPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, inject } from '@angular/core';
import { Product } from '../../../../../core/products/models/product-domain.model';
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
import { CurrencyConvertPipe } from '../../../../../pipes/currency-convert.pipe';
import { LanguageService } from '../../../../../services/language.service';
@Component({
selector: 'app-product-delivery-information',
standalone: true,
imports: [DecimalPipe, TranslatePipe],
imports: [DecimalPipe, TranslatePipe, CurrencyConvertPipe],
templateUrl: './delivery-information.component.html',
styleUrls: ['./delivery-information.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductDeliveryInformationComponent {
private readonly languageService = inject(LanguageService);
readonly displayCurrency = this.languageService.currentCurrency;
@Input({ required: true }) product!: Product;
@Input() currency = '';

View File

@@ -19,12 +19,12 @@
<div class="product-price">
@if (product.discount > 0) {
<div class="price-row">
<span class="old-price">{{ price | number:'1.2-2' }} {{ currency }}</span>
<span class="old-price">{{ price | currencyConvert:currency | number:'1.2-2' }} {{ displayCurrency() }}</span>
<span class="discount-badge">-{{ product.discount }}%</span>
</div>
<strong>{{ discountedPrice | number:'1.2-2' }} {{ currency }}</strong>
<strong>{{ discountedPrice | currencyConvert:currency | number:'1.2-2' }} {{ displayCurrency() }}</strong>
} @else {
<strong>{{ price | number:'1.2-2' }} {{ currency }}</strong>
<strong>{{ price | currencyConvert:currency | number:'1.2-2' }} {{ displayCurrency() }}</strong>
}
</div>

View File

@@ -1,18 +1,23 @@
import { DecimalPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject } from '@angular/core';
import { Product } from '../../../../../core/products/models/product-domain.model';
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
import { CurrencyConvertPipe } from '../../../../../pipes/currency-convert.pipe';
import { LanguageService } from '../../../../../services/language.service';
import { getBadgeClass } from '../../../../../utils/item.utils';
@Component({
selector: 'app-product-information',
standalone: true,
imports: [DecimalPipe, TranslatePipe],
imports: [DecimalPipe, TranslatePipe, CurrencyConvertPipe],
templateUrl: './product-information.component.html',
styleUrls: ['./product-information.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductInformationComponent {
private readonly languageService = inject(LanguageService);
readonly displayCurrency = this.languageService.currentCurrency;
@Input({ required: true }) product!: Product;
@Input() title = '';
@Input() price = 0;

View File

@@ -13,10 +13,10 @@
<h3 class="quick-view__title">{{ product.name }}</h3>
<p class="quick-view__price">
@if (hasDiscount) {
<span class="quick-view__price-original">{{ product.price | number:'1.2-2' }} {{ product.currency }}</span>
<span class="quick-view__price-final">{{ discountedPrice | number:'1.2-2' }} {{ product.currency }}</span>
<span class="quick-view__price-original">{{ product.price | currencyConvert:product.currency | number:'1.2-2' }} {{ displayCurrency() }}</span>
<span class="quick-view__price-final">{{ discountedPrice | currencyConvert:product.currency | number:'1.2-2' }} {{ displayCurrency() }}</span>
} @else {
<span class="quick-view__price-final">{{ product.price | number:'1.2-2' }} {{ product.currency }}</span>
<span class="quick-view__price-final">{{ product.price | currencyConvert:product.currency | number:'1.2-2' }} {{ displayCurrency() }}</span>
}
</p>
@if (product.simpleDescription) {

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { Product } from '../../../../../core/products/models/product-domain.model';
@@ -6,17 +6,22 @@ import { DialogComponent } from '../../../../../shared/ui/dialog/dialog.componen
import { ButtonComponent } from '../../../../../shared/ui/button/button.component';
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
import { LangRoutePipe } from '../../../../../pipes/lang-route.pipe';
import { CurrencyConvertPipe } from '../../../../../pipes/currency-convert.pipe';
import { LanguageService } from '../../../../../services/language.service';
import { getDiscountedPrice, getMainImage } from '../../../../../utils/item.utils';
@Component({
selector: 'app-quick-view-dialog',
standalone: true,
imports: [DialogComponent, ButtonComponent, TranslatePipe, LangRoutePipe, DecimalPipe, RouterLink],
imports: [DialogComponent, ButtonComponent, TranslatePipe, LangRoutePipe, DecimalPipe, RouterLink, CurrencyConvertPipe],
templateUrl: './quick-view-dialog.component.html',
styleUrl: './quick-view-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class QuickViewDialogComponent {
private readonly languageService = inject(LanguageService);
readonly displayCurrency = this.languageService.currentCurrency;
@Input() product: Product | null = null;
@Input() loading = false;

View File

@@ -3,6 +3,7 @@ import { Product } from '../../../../../core/products/models/product-domain.mode
import { TranslateService } from '../../../../../i18n/translate.service';
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
import { LanguageService } from '../../../../../services/language.service';
import { CurrencyRatesService } from '../../../../../services/currency-rates.service';
import { getTranslatedField } from '../../../../../utils/item.utils';
interface CompareRow {
@@ -30,6 +31,7 @@ const STOCK_LABEL_KEYS: Record<string, string> = {
export class CompareTableComponent {
private readonly i18n = inject(TranslateService);
private readonly languageService = inject(LanguageService);
private readonly currencyRates = inject(CurrencyRatesService);
@Input() products: Product[] = [];
@Input() hideIdentical = false;
@@ -41,8 +43,12 @@ export class CompareTableComponent {
return [];
}
const targetCurrency = this.languageService.currentCurrency();
const baseRows: CompareRow[] = [
this.toRow('price', this.i18n.t('ux.comparePrice'), products.map(product => `${product.price.toFixed(2)} ${product.currency}`)),
this.toRow('price', this.i18n.t('ux.comparePrice'), products.map(product =>
`${this.currencyRates.convert(product.price, product.currency, targetCurrency).toFixed(2)} ${targetCurrency}`
)),
this.toRow('rating', this.i18n.t('ux.compareRating'), products.map(product => `${(product.rating ?? 0).toFixed(1)}`)),
this.toRow('stock', this.i18n.t('ux.compareStock'), products.map(product => this.stockLabel(product.remainings))),
this.toRow('discount', this.i18n.t('ux.compareDiscount'), products.map(product => `${product.discount ?? 0}%`)),