From 8c1147f2a08a2d07f8756b793a7e20e280978997 Mon Sep 17 00:00:00 2001 From: agentd Date: Wed, 5 Aug 2026 01:11:56 +0000 Subject: [PATCH] Preisrechner: Rappenrundung im Backend und Rabatt-Rechner im Web agentd task 332cb3ed956546778f775e25fa01f8c3 --- src/Prices/PriceCalculator.cs | 11 ++- tests/Prices.Tests/PriceCalculatorTests.cs | 23 ++++++- web/src/app/app.html | 2 + web/src/app/app.ts | 5 +- .../discount-calculator.html | 34 ++++++++++ .../discount-calculator.scss | 47 +++++++++++++ .../discount-calculator.spec.ts | 67 +++++++++++++++++++ .../discount-calculator.ts | 36 ++++++++++ 8 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 web/src/app/discount-calculator/discount-calculator.html create mode 100644 web/src/app/discount-calculator/discount-calculator.scss 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..51a4b3a 100644 --- a/src/Prices/PriceCalculator.cs +++ b/src/Prices/PriceCalculator.cs @@ -16,6 +16,13 @@ public static class PriceCalculator throw new ArgumentOutOfRangeException(nameof(discountPercent), "A discount is between 0 and 100 percent."); } - return price - discountPercent; + var raw = price * (100 - discountPercent) / 100m; + return RoundToFiveRappen(raw); } -} + + /// Rounds a decimal to the nearest 0.05 (Swiss Rappenrundung). + private static decimal RoundToFiveRappen(decimal value) + { + return Math.Round(value * 20m, MidpointRounding.AwayFromZero) / 20m; + } +} \ No newline at end of file diff --git a/tests/Prices.Tests/PriceCalculatorTests.cs b/tests/Prices.Tests/PriceCalculatorTests.cs index c49c29a..99c34b8 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 Swiss_rappernrundung_rounds_to_nearest_0_05() + { + // 99.90 * 0.9 = 89.91 → 89.91 * 20 = 1798.2 → round to 1798 → 1798 / 20 = 89.90 + Assert.Equal(89.90m, PriceCalculator.ApplyDiscount(99.90m, 10m)); + } + + [Fact] + public void Rounding_rounds_down_when_cent_remainder_is_below_0_05() + { + // 1.23 * 0.5 = 0.615 → 0.615 * 20 = 12.3 → round to 12 → 12 / 20 = 0.60 + Assert.Equal(0.60m, PriceCalculator.ApplyDiscount(1.23m, 50m)); + } + + [Fact] + public void Rounding_rounds_up_when_cent_remainder_is_at_0_05() + { + // 3.37 * 0.5 = 1.685 → 1.685 * 20 = 33.7 → round to 34 → 34 / 20 = 1.70 + Assert.Equal(1.70m, PriceCalculator.ApplyDiscount(3.37m, 50m)); + } +} \ No newline at end of file diff --git a/web/src/app/app.html b/web/src/app/app.html index a1c4296..40ede54 100644 --- a/web/src/app/app.html +++ b/web/src/app/app.html @@ -341,4 +341,6 @@ + + diff --git a/web/src/app/app.ts b/web/src/app/app.ts index 358960b..36dd692 100644 --- a/web/src/app/app.ts +++ b/web/src/app/app.ts @@ -1,12 +1,13 @@ 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' }) export class App { protected readonly title = signal('web'); -} +} \ No newline at end of file diff --git a/web/src/app/discount-calculator/discount-calculator.html b/web/src/app/discount-calculator/discount-calculator.html new file mode 100644 index 0000000..152f3e7 --- /dev/null +++ b/web/src/app/discount-calculator/discount-calculator.html @@ -0,0 +1,34 @@ +
+

Rabatt-Rechner

+ +
+ + +
+ +
+ + +
+ + @if (discountedPrice !== null) { +
+ Endpreis: {{ formatPrice(discountedPrice) }} CHF +
+ } +
\ No newline at end of file diff --git a/web/src/app/discount-calculator/discount-calculator.scss b/web/src/app/discount-calculator/discount-calculator.scss new file mode 100644 index 0000000..560f55c --- /dev/null +++ b/web/src/app/discount-calculator/discount-calculator.scss @@ -0,0 +1,47 @@ +:host { + display: block; + max-width: 400px; + margin: 2rem auto; + padding: 1.5rem; + border: 1px solid #ddd; + border-radius: 8px; + font-family: sans-serif; +} + +.calculator { + h2 { + margin: 0 0 1rem; + font-size: 1.25rem; + } +} + +.field { + margin-bottom: 1rem; + + label { + display: block; + margin-bottom: 0.25rem; + font-weight: 500; + } + + input { + width: 100%; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 1rem; + box-sizing: border-box; + } +} + +.result { + margin-top: 1rem; + padding: 0.75rem; + background: #e8f5e9; + border-radius: 4px; + font-size: 1.1rem; + + strong { + color: #2e7d32; + } +} \ No newline at end of file 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..a788a7f --- /dev/null +++ b/web/src/app/discount-calculator/discount-calculator.spec.ts @@ -0,0 +1,67 @@ +import { TestBed, type ComponentFixture } from '@angular/core/testing'; +import { DiscountCalculator } from './discount-calculator'; + +describe('DiscountCalculator', () => { + async function createFixture(): Promise> { + await TestBed.configureTestingModule({ + imports: [DiscountCalculator], + }).compileComponents(); + return TestBed.createComponent(DiscountCalculator); + } + + it('should create the component', async () => { + const fixture = await createFixture(); + const component = fixture.componentInstance; + expect(component).toBeTruthy(); + }); + + it('should calculate 100 CHF with 10% discount to 90.00', async () => { + const fixture = await createFixture(); + const component = fixture.componentInstance; + + component['price'].set(100); + component['discountPercent'].set(10); + fixture.detectChanges(); + + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.result')?.textContent).toContain('90.00 CHF'); + }); + + it('should round 99.90 CHF with 10% discount to 89.90 (Swiss 0.05 rounding)', async () => { + const fixture = await createFixture(); + const component = fixture.componentInstance; + + component['price'].set(99.90); + component['discountPercent'].set(10); + fixture.detectChanges(); + + // raw = 99.90 * 90 / 100 = 89.91 + // Math.round(89.91 * 20) / 20 = Math.round(1798.2) / 20 = 1798 / 20 = 89.90 + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.result')?.textContent).toContain('89.90 CHF'); + }); + + it('should not show result when price is negative', async () => { + const fixture = await createFixture(); + const component = fixture.componentInstance; + + component['price'].set(-5); + component['discountPercent'].set(10); + fixture.detectChanges(); + + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.result')).toBeNull(); + }); + + it('should not show result when discount is negative', async () => { + const fixture = await createFixture(); + const component = fixture.componentInstance; + + component['price'].set(100); + component['discountPercent'].set(-1); + fixture.detectChanges(); + + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.result')).toBeNull(); + }); +}); \ 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..2a1475d --- /dev/null +++ b/web/src/app/discount-calculator/discount-calculator.ts @@ -0,0 +1,36 @@ +import { Component, signal } from '@angular/core'; + +@Component({ + selector: 'app-discount-calculator', + imports: [], + templateUrl: './discount-calculator.html', + styleUrl: './discount-calculator.scss' +}) +export class DiscountCalculator { + protected readonly price = signal(null); + protected readonly discountPercent = signal(null); + + protected onPriceInput(event: Event): void { + const value = (event.target as HTMLInputElement).value; + this.price.set(value === '' ? null : Number(value)); + } + + protected onDiscountInput(event: Event): void { + const value = (event.target as HTMLInputElement).value; + this.discountPercent.set(value === '' ? null : Number(value)); + } + + protected get discountedPrice(): number | null { + const p = this.price(); + const d = this.discountPercent(); + if (p === null || d === null || p < 0 || d < 0) { + return null; + } + const raw = p * (100 - d) / 100; + return Math.round(raw * 20) / 20; + } + + protected formatPrice(value: number): string { + return value.toFixed(2); + } +} \ No newline at end of file -- 2.49.1