From 724ee92a2b4685b987c1bc59f909d3491f9e1f98 Mon Sep 17 00:00:00 2001 From: agentd Date: Wed, 5 Aug 2026 00:32:00 +0000 Subject: [PATCH] Preisrechner: Rappenrundung im Backend und Rabatt-Rechner im Web agentd task ad07191c406d4ea496b0b18db826651e --- src/Prices/PriceCalculator.cs | 4 +- tests/Prices.Tests/PriceCalculatorTests.cs | 21 +++ web/src/app/app.html | 1 + web/src/app/app.ts | 3 +- .../discount-calculator.spec.ts | 74 ++++++++++ .../discount-calculator.ts | 135 ++++++++++++++++++ 6 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 web/src/app/discount-calculator/discount-calculator.spec.ts create mode 100644 web/src/app/discount-calculator/discount-calculator.ts diff --git a/src/Prices/PriceCalculator.cs b/src/Prices/PriceCalculator.cs index c2b9cb3..4017df6 100644 --- a/src/Prices/PriceCalculator.cs +++ b/src/Prices/PriceCalculator.cs @@ -16,6 +16,8 @@ public static class PriceCalculator throw new ArgumentOutOfRangeException(nameof(discountPercent), "A discount is between 0 and 100 percent."); } - return price - discountPercent; + var discounted = price * (1 - discountPercent / 100m); + // Swiss rounding (Rappenrundung): round to the nearest 0.05 + return Math.Round(discounted / 0.05m, MidpointRounding.ToEven) * 0.05m; } } diff --git a/tests/Prices.Tests/PriceCalculatorTests.cs b/tests/Prices.Tests/PriceCalculatorTests.cs index c49c29a..f8e6225 100644 --- a/tests/Prices.Tests/PriceCalculatorTests.cs +++ b/tests/Prices.Tests/PriceCalculatorTests.cs @@ -33,4 +33,25 @@ public sealed class PriceCalculatorTests { Assert.Throws(() => PriceCalculator.ApplyDiscount(100m, 101m)); } + + [Fact] + public void ApplyDiscount_99_90_with_10_percent_rounds_to_89_90() + { + // 99.90 * (1 - 10/100) = 89.91 → rounded to nearest 0.05 = 89.90 + Assert.Equal(89.90m, PriceCalculator.ApplyDiscount(99.90m, 10m)); + } + + [Fact] + public void ApplyDiscount_10_with_33_percent_rounds_correctly() + { + // 10 * (1 - 33/100) = 6.70 → 6.70 is already a multiple of 0.05 + Assert.Equal(6.70m, PriceCalculator.ApplyDiscount(10m, 33m)); + } + + [Fact] + public void ApplyDiscount_5_55_with_10_percent_rounds_to_5_00() + { + // 5.55 * 0.9 = 4.995 → rounded to nearest 0.05 = 5.00 + Assert.Equal(5.00m, PriceCalculator.ApplyDiscount(5.55m, 10m)); + } } diff --git a/web/src/app/app.html b/web/src/app/app.html index a1c4296..a80e1dc 100644 --- a/web/src/app/app.html +++ b/web/src/app/app.html @@ -233,6 +233,7 @@

Hello, {{ title() }}

+

Congratulations! Your app is running. 🎉

diff --git a/web/src/app/app.ts b/web/src/app/app.ts index 358960b..6a24c96 100644 --- a/web/src/app/app.ts +++ b/web/src/app/app.ts @@ -1,9 +1,10 @@ import { Component, signal } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { DiscountCalculator } from './discount-calculator/discount-calculator'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterOutlet, DiscountCalculator], templateUrl: './app.html', styleUrl: './app.scss' }) diff --git a/web/src/app/discount-calculator/discount-calculator.spec.ts b/web/src/app/discount-calculator/discount-calculator.spec.ts new file mode 100644 index 0000000..a89d83f --- /dev/null +++ b/web/src/app/discount-calculator/discount-calculator.spec.ts @@ -0,0 +1,74 @@ +import { TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { DiscountCalculator } from './discount-calculator'; + +describe('DiscountCalculator', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DiscountCalculator, FormsModule], + }).compileComponents(); + }); + + it('should create the component', () => { + const fixture = TestBed.createComponent(DiscountCalculator); + const component = fixture.componentInstance; + expect(component).toBeTruthy(); + }); + + it('should display "CHF 89.90" for price=99.90 and discount=10', async () => { + const fixture = TestBed.createComponent(DiscountCalculator); + fixture.detectChanges(); + + const priceInput = fixture.nativeElement.querySelector('#price') as HTMLInputElement; + const discountInput = fixture.nativeElement.querySelector('#discount') as HTMLInputElement; + + priceInput.value = '99.90'; + priceInput.dispatchEvent(new Event('input')); + discountInput.value = '10'; + discountInput.dispatchEvent(new Event('input')); + + fixture.detectChanges(); + await fixture.whenStable(); + + const resultEl = fixture.nativeElement.querySelector('.result-value') as HTMLElement; + expect(resultEl.textContent?.trim()).toBe('CHF 89.90'); + }); + + it('should display "CHF 100.00" for price=100 and discount=0', async () => { + const fixture = TestBed.createComponent(DiscountCalculator); + fixture.detectChanges(); + + const priceInput = fixture.nativeElement.querySelector('#price') as HTMLInputElement; + const discountInput = fixture.nativeElement.querySelector('#discount') as HTMLInputElement; + + priceInput.value = '100'; + priceInput.dispatchEvent(new Event('input')); + discountInput.value = '0'; + discountInput.dispatchEvent(new Event('input')); + + fixture.detectChanges(); + await fixture.whenStable(); + + const resultEl = fixture.nativeElement.querySelector('.result-value') as HTMLElement; + expect(resultEl.textContent?.trim()).toBe('CHF 100.00'); + }); + + it('should display "CHF 150.00" for price=200 and discount=25', async () => { + const fixture = TestBed.createComponent(DiscountCalculator); + fixture.detectChanges(); + + const priceInput = fixture.nativeElement.querySelector('#price') as HTMLInputElement; + const discountInput = fixture.nativeElement.querySelector('#discount') as HTMLInputElement; + + priceInput.value = '200'; + priceInput.dispatchEvent(new Event('input')); + discountInput.value = '25'; + discountInput.dispatchEvent(new Event('input')); + + fixture.detectChanges(); + await fixture.whenStable(); + + const resultEl = fixture.nativeElement.querySelector('.result-value') as HTMLElement; + expect(resultEl.textContent?.trim()).toBe('CHF 150.00'); + }); +}); \ No newline at end of file diff --git a/web/src/app/discount-calculator/discount-calculator.ts b/web/src/app/discount-calculator/discount-calculator.ts new file mode 100644 index 0000000..4b5fe3a --- /dev/null +++ b/web/src/app/discount-calculator/discount-calculator.ts @@ -0,0 +1,135 @@ +import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-discount-calculator', + standalone: true, + imports: [FormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+

Rabatt-Rechner

+
+ + +
+
+ + +
+
+ Endpreis + {{ displayValue() }} +
+
+ `, + styles: ` + .card { + max-width: 320px; + margin: 2rem auto; + padding: 1.5rem; + border: 1px solid #d0d0d0; + border-radius: 8px; + background: #fafafa; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + } + + h2 { + margin: 0 0 1rem; + font-size: 1.25rem; + color: #333; + } + + .field { + margin-bottom: 1rem; + display: flex; + flex-direction: column; + } + + label { + font-size: 0.875rem; + color: #555; + margin-bottom: 0.25rem; + } + + input { + padding: 0.5rem 0.75rem; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 1rem; + } + + input:focus { + outline: none; + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15); + } + + .result { + margin-top: 1rem; + padding: 0.75rem; + background: #e9f5e9; + border-radius: 4px; + display: flex; + justify-content: space-between; + align-items: center; + } + + .result-label { + font-size: 0.875rem; + color: #2a6b2a; + font-weight: 600; + } + + .result-value { + font-size: 1.25rem; + font-weight: 700; + color: #1a4a1a; + } + ` +}) +export class DiscountCalculator { + protected readonly priceValue = signal(null); + protected readonly discountValue = signal(null); + + protected readonly displayValue = computed(() => { + const price = this.priceValue(); + const discount = this.discountValue(); + + if (price === null || discount === null) { + return '—'; + } + + if (isNaN(price) || isNaN(discount)) { + return '—'; + } + + if (price <= 0) { + return '—'; + } + + if (discount < 0 || discount > 100) { + return '—'; + } + + const discounted = price * (1 - discount / 100); + const rounded = Math.round(discounted / 0.05) * 0.05; + return `CHF ${rounded.toFixed(2)}`; + }); +} \ No newline at end of file -- 2.49.1