Legacy-Characterization-Lab

15 Java-Dateien. 7 zentrale Dateien werden direkt mit echtem Quellcode und ihrem Zusammenspiel erklärt.

Zurück zu Code-Labs

Kapitel 11 · Legacy Characterization

Was dieses Lab zeigt

Sichert das reale Verhalten eines Legacy-Prozessors mit Characterization Tests und Szenariokatalog. Erst das beobachtete Verhalten wird stabilisiert, bevor Refactoring beginnt.

Lernziele

  • Ist-Verhalten erfassen
  • Szenarien reproduzierbar machen
  • Refactoring durch Tests absichern

Technik und Schwerpunkte

JDK-Lab15 Java-Dateien1 Tests/RunnerCharacterization TestsLegacySafety Net
Echter Quellcode aus diesem Lab

Geführter Codepfad

Vor jeder Refaktorierung wird das Verhalten des Legacy-Prozessors eingefroren. Der Pfad zeigt deterministische Testdaten, Szenariokatalog, Golden Master und die daraus abgeleiteten Refactoring-Seams.

LegacyOrderProcessorDemoLegacyOrderProcessorLegacyTestFixtureScenarioCatalogGoldenMasterSnapshotRefactoringSeamInventoryLegacyCharacterizationTestRunner
Lesereihenfolge der zentralen Klassen. Die Pfeile zeigen den didaktischen Weg durch den realen Quellcode, nicht zwingend jeden Laufzeitaufruf.
1. LegacyOrderProcessorDemoFührt typische Legacy-Szenarien aus und zeigt die beobachtbaren Ergebnisse.
2. LegacyOrderProcessorEnthält das unveränderte Legacy-Verhalten, das durch Charakterisierungstests abgesichert wird.
3. LegacyTestFixtureErzeugt deterministische Abhängigkeiten und Testdaten für reproduzierbare Beobachtungen.
4. ScenarioCatalogDefiniert die fachlich relevanten Fälle, die vor einer Änderung abgedeckt sein müssen.
5. GoldenMasterSnapshotSpeichert die beobachtete Ausgabe in einer vergleichbaren Form.
6. RefactoringSeamInventoryDokumentiert Stellen, an denen Abhängigkeiten später sicher getrennt werden können.
7. LegacyCharacterizationTestRunnerVergleicht aktuelle Ergebnisse mit dem Golden Master und schützt das bestehende Verhalten.

1. LegacyOrderProcessorDemo

src/main/java/com/example/legacy/run7a/LegacyOrderProcessorDemo.java
Java-Datei öffnen
Rolle im Ablauf

Führt typische Legacy-Szenarien aus und zeigt die beobachtbaren Ergebnisse.

Im Lesepfad folgt LegacyOrderProcessor: Enthält das unveränderte Legacy-Verhalten, das durch Charakterisierungstests abgesichert wird.

Typ
class LegacyOrderProcessorDemo
Verwendet
LegacyTestFixture, ScenarioCatalog, GoldenMasterSnapshot
Verwendet von
Einstiege
main(String[] args)
package com.example.legacy.run7a;

public final class LegacyOrderProcessorDemo {
    public static void main(String[] args) {
        DeterministicLegacyGateway gateway = LegacyTestFixture.gateway();
        LegacyInvoiceResult result = LegacyTestFixture.processor(gateway).process(ScenarioCatalog.platinumHappyPath());
        System.out.println(GoldenMasterSnapshot.from(result));
    }
}

2. LegacyOrderProcessor

src/main/java/com/example/legacy/run7a/LegacyOrderProcessor.java
Java-Datei öffnen
Rolle im Ablauf

Enthält das unveränderte Legacy-Verhalten, das durch Charakterisierungstests abgesichert wird.

Im Lesepfad folgt LegacyTestFixture: Erzeugt deterministische Abhängigkeiten und Testdaten für reproduzierbare Beobachtungen.

Typ
class LegacyOrderProcessor
Verwendet
Verwendet von
LegacyTestFixture, LegacyCharacterizationTestRunner
Einstiege
process(LegacyOrderRequest request)
Legacy Facade (absichtlich)
package com.example.legacy.run7a;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.DayOfWeek;
import java.time.ZoneOffset;
import java.util.*;

// Pattern: Legacy Facade (absichtlich) - diese Klasse zeigt eine Monster-Methode als Lernobjekt.
// Sie ist NICHT das Zielbild. Run 7A nutzt sie, um Verhalten abzusichern, bevor Refactoring beginnt.
public final class LegacyOrderProcessor {
    private final LegacyIntegrationGateway gateway;

    public LegacyOrderProcessor(LegacyIntegrationGateway gateway) {
        this.gateway = Objects.requireNonNull(gateway);
    }

    // Anti-Pattern: Long Method + Mixed Responsibilities + Hidden Transaction Script.
    // Zweck in diesem Run: vorhandenes Verhalten sichtbar und testbar machen.
    public LegacyInvoiceResult process(LegacyOrderRequest request) {
        List<String> localEvents = new ArrayList<>();
        List<String> localAudit = new ArrayList<>();
        String orderId = request == null ? "<null>" : request.orderId();

        try {
            localAudit.add("start process order=" + orderId);

            if (request == null) {
                localAudit.add("request is null");
                return LegacyInvoiceResult.failed("<null>", "INVALID_REQUEST", "request must not be null", localEvents, localAudit);
            }
            if (blank(request.orderId())) {
                localAudit.add("orderId missing");
                return LegacyInvoiceResult.failed("<missing>", "INVALID_ORDER_ID", "orderId is required", localEvents, localAudit);
            }
            if (blank(request.customerId())) {
                localAudit.add("customerId missing");
                return LegacyInvoiceResult.failed(request.orderId(), "INVALID_CUSTOMER", "customerId is required", localEvents, localAudit);
            }
            if (request.lines() == null || request.lines().isEmpty()) {
                localAudit.add("no lines");
                return LegacyInvoiceResult.failed(request.orderId(), "EMPTY_ORDER", "at least one line is required", localEvents, localAudit);
            }
            if (request.riskyAccount()) {
                localAudit.add("risky account blocked customer=" + request.customerId());
                localEvents.add("OrderRejected:risky-account");
                gateway.audit("risky account rejected " + request.customerId());
                return LegacyInvoiceResult.failed(request.orderId(), "CUSTOMER_RISK_BLOCK", "customer is blocked by risk checks", localEvents, merge(localAudit, gateway));
            }

            BigDecimal subtotal = BigDecimal.ZERO;
            BigDecimal digitalSubtotal = BigDecimal.ZERO;
            BigDecimal physicalSubtotal = BigDecimal.ZERO;
            int physicalQuantity = 0;
            int totalQuantity = 0;
            Set<String> skusSeen = new LinkedHashSet<>();
            boolean containsTaxReduced = false;
            boolean containsHazardous = false;
            boolean containsGiftEligible = false;

            for (int i = 0; i < request.lines().size(); i++) {
                OrderLineRequest line = request.lines().get(i);
                if (line == null) {
                    return LegacyInvoiceResult.failed(request.orderId(), "INVALID_LINE", "line " + i + " is null", localEvents, localAudit);
                }
                if (blank(line.sku())) {
                    return LegacyInvoiceResult.failed(request.orderId(), "INVALID_SKU", "line " + i + " has no sku", localEvents, localAudit);
                }
                if (line.quantity() <= 0) {
                    return LegacyInvoiceResult.failed(request.orderId(), "INVALID_QUANTITY", "quantity must be positive for " + line.sku(), localEvents, localAudit);
                }
                if (line.unitPrice() == null || line.unitPrice().signum() < 0) {
                    return LegacyInvoiceResult.failed(request.orderId(), "INVALID_PRICE", "price invalid for " + line.sku(), localEvents, localAudit);
                }
                if (!skusSeen.add(line.sku())) {
                    localAudit.add("duplicate sku merged logically=" + line.sku());
                }

                BigDecimal lineTotal = line.unitPrice().multiply(BigDecimal.valueOf(line.quantity())).setScale(2, RoundingMode.HALF_UP);
                subtotal = subtotal.add(lineTotal);
                totalQuantity += line.quantity();
                if (line.digital()) {
                    digitalSubtotal = digitalSubtotal.add(lineTotal);
                } else {
                    physicalSubtotal = physicalSubtotal.add(lineTotal);
                    physicalQuantity += line.quantity();
                    int available = gateway.availableStock(line.sku());
                    localAudit.add("stock check sku=" + line.sku() + " requested=" + line.quantity() + " available=" + available);
                    if (available < line.quantity()) {
                        localEvents.add("OrderRejected:stock-unavailable:" + line.sku());
                        return LegacyInvoiceResult.failed(request.orderId(), "STOCK_UNAVAILABLE", "not enough stock for " + line.sku(), localEvents, merge(localAudit, gateway));
                    }
                }
                if ("REDUCED".equalsIgnoreCase(line.taxCategory())) containsTaxReduced = true;
                if (line.sku().startsWith("HZ-")) containsHazardous = true;
                if (line.giftEligible()) containsGiftEligible = true;
            }

            if (subtotal.compareTo(new BigDecimal("10000.00")) > 0) {
                localEvents.add("OrderRejected:manual-approval-required");
                return LegacyInvoiceResult.failed(request.orderId(), "MANUAL_APPROVAL_REQUIRED", "subtotal exceeds automatic limit", localEvents, localAudit);
            }
            if (containsHazardous && !"DE".equalsIgnoreCase(request.country())) {
                localEvents.add("OrderRejected:hazardous-cross-border");
                return LegacyInvoiceResult.failed(request.orderId(), "HAZARDOUS_CROSS_BORDER", "hazardous goods only domestic", localEvents, localAudit);
            }

            BigDecimal discount = BigDecimal.ZERO;
            String tier = request.customerTier() == null ? "STANDARD" : request.customerTier().toUpperCase(Locale.ROOT);
            if ("PLATINUM".equals(tier)) {
                discount = discount.add(subtotal.multiply(new BigDecimal("0.10")));
                localAudit.add("tier discount platinum 10%");
            } else if ("GOLD".equals(tier)) {
                discount = discount.add(subtotal.multiply(new BigDecimal("0.07")));
                localAudit.add("tier discount gold 7%");
            } else if ("SILVER".equals(tier)) {
                discount = discount.add(subtotal.multiply(new BigDecimal("0.03")));
                localAudit.add("tier discount silver 3%");
            }

            if (!blank(request.couponCode())) {
                String coupon = request.couponCode().trim().toUpperCase(Locale.ROOT);
                if ("WELCOME10".equals(coupon) && subtotal.compareTo(new BigDecimal("50.00")) >= 0) {
                    discount = discount.add(new BigDecimal("10.00"));
                    localAudit.add("coupon WELCOME10 applied");
                } else if ("FREESHIP".equals(coupon)) {
                    localAudit.add("coupon FREESHIP deferred to shipping");
                } else if ("VIP20".equals(coupon) && ("GOLD".equals(tier) || "PLATINUM".equals(tier))) {
                    discount = discount.add(subtotal.multiply(new BigDecimal("0.20")));
                    localAudit.add("coupon VIP20 applied");
                } else {
                    localEvents.add("OrderRejected:invalid-coupon:" + coupon);
                    return LegacyInvoiceResult.failed(request.orderId(), "INVALID_COUPON", "coupon not applicable: " + coupon, localEvents, localAudit);
                }
            }

            discount = discount.setScale(2, RoundingMode.HALF_UP);
            if (discount.compareTo(subtotal.multiply(new BigDecimal("0.40"))) > 0) {
                localAudit.add("discount capped at 40%");
                discount = subtotal.multiply(new BigDecimal("0.40")).setScale(2, RoundingMode.HALF_UP);
            }

            BigDecimal shipping = BigDecimal.ZERO;
            if (physicalQuantity > 0) {
                shipping = new BigDecimal("4.90");
                if (physicalSubtotal.compareTo(new BigDecimal("100.00")) > 0) shipping = BigDecimal.ZERO;
                if (request.expressShipping()) shipping = shipping.add(new BigDecimal("12.00"));
                if ("FREESHIP".equalsIgnoreCase(String.valueOf(request.couponCode()))) shipping = BigDecimal.ZERO;
                if (!"DE".equalsIgnoreCase(request.country())) shipping = shipping.add(new BigDecimal("9.00"));
            }
            if (request.giftWrap()) {
                if (!containsGiftEligible) {
                    return LegacyInvoiceResult.failed(request.orderId(), "GIFT_WRAP_NOT_ALLOWED", "no gift eligible line", localEvents, localAudit);
                }
                shipping = shipping.add(new BigDecimal("3.50"));
            }
            shipping = shipping.setScale(2, RoundingMode.HALF_UP);

            BigDecimal taxableBase = subtotal.subtract(discount).max(BigDecimal.ZERO);
            BigDecimal taxRate = "DE".equalsIgnoreCase(request.country()) ? new BigDecimal("0.19") : new BigDecimal("0.00");
            if (containsTaxReduced && "DE".equalsIgnoreCase(request.country())) {
                taxRate = new BigDecimal("0.12"); // Legacy bug/behavior: mixed basket gets blended simplified rate.
                localAudit.add("legacy blended reduced tax rate 12%");
            }
            BigDecimal tax = taxableBase.multiply(taxRate).setScale(2, RoundingMode.HALF_UP);
            BigDecimal total = taxableBase.add(shipping).add(tax).setScale(2, RoundingMode.HALF_UP);

            DayOfWeek dow = gateway.now().atZone(ZoneOffset.UTC).getDayOfWeek();
            if (request.expressShipping() && (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY)) {
                localAudit.add("express weekend handling fee");
                total = total.add(new BigDecimal("5.00")).setScale(2, RoundingMode.HALF_UP);
            }

            for (OrderLineRequest line : request.lines()) {
                if (!line.digital()) {
                    boolean reserved = gateway.reserveStock(request.orderId(), line.sku(), line.quantity());
                    if (!reserved) {
                        localEvents.add("OrderRejected:race-stock-unavailable:" + line.sku());
                        return LegacyInvoiceResult.failed(request.orderId(), "STOCK_RACE_LOST", "stock changed for " + line.sku(), localEvents, merge(localAudit, gateway));
                    }
                }
            }

            PaymentDecision payment = gateway.authorizePayment(request.orderId(), request.customerId(), request.paymentMethod(), total);
            if (!payment.approved()) {
                localEvents.add("OrderRejected:payment:" + payment.reason());
                return LegacyInvoiceResult.failed(request.orderId(), "PAYMENT_DECLINED", payment.reason(), localEvents, merge(localAudit, gateway));
            }

            String invoiceId = gateway.nextInvoiceId();
            gateway.saveInvoice(invoiceId, total);
            String invoicePayload = request.orderId() + ":" + invoiceId + ":" + total.toPlainString();
            gateway.publishEvent("InvoiceCreated", invoicePayload);
            gateway.publishEvent("OrderAccepted", request.orderId() + ":" + payment.transactionId());
            localEvents.add("InvoiceCreated:" + invoiceId);
            localEvents.add("OrderAccepted:" + request.orderId());
            localAudit.add("finished success invoice=" + invoiceId + " total=" + total.toPlainString());

            return new LegacyInvoiceResult(true, request.orderId(), invoiceId, subtotal.setScale(2, RoundingMode.HALF_UP), discount,
                    shipping, tax, total, "-", "-", List.copyOf(localEvents), merge(localAudit, gateway));
        } catch (RuntimeException ex) {
            localAudit.add("technical exception " + ex.getClass().getSimpleName() + ":" + ex.getMessage());
            return LegacyInvoiceResult.failed(orderId, "TECHNICAL_FAILURE", ex.getMessage(), localEvents, merge(localAudit, gateway));
        }
    }

    private static boolean blank(String text) { return text == null || text.trim().isEmpty(); }

    private static List<String> merge(List<String> local, LegacyIntegrationGateway gateway) {
        List<String> merged = new ArrayList<>(local);
        if (gateway instanceof DeterministicLegacyGateway d) merged.addAll(d.auditTrail());
        return List.copyOf(merged);
    }
}

3. LegacyTestFixture

src/main/java/com/example/legacy/run7a/LegacyTestFixture.java
Java-Datei öffnen
Rolle im Ablauf

Erzeugt deterministische Abhängigkeiten und Testdaten für reproduzierbare Beobachtungen.

Im Lesepfad folgt ScenarioCatalog: Definiert die fachlich relevanten Fälle, die vor einer Änderung abgedeckt sein müssen.

Typ
class LegacyTestFixture
Verwendet
LegacyOrderProcessor
Verwendet von
LegacyOrderProcessorDemo, LegacyCharacterizationTestRunner
Einstiege
gateway(), processor(DeterministicLegacyGateway gateway)
package com.example.legacy.run7a;

import java.time.Instant;

public final class LegacyTestFixture {
    private LegacyTestFixture() {}

    public static DeterministicLegacyGateway gateway() {
        return new DeterministicLegacyGateway(Instant.parse("2026-02-02T10:15:30Z"))
                .withStock("BOOK-1", 20)
                .withStock("CHAIR-9", 4)
                .withStock("RARE-1", 1)
                .withStock("HZ-CLEANER", 8)
                .declinePaymentMethod("CARD_DECLINE");
    }

    public static LegacyOrderProcessor processor(DeterministicLegacyGateway gateway) {
        return new LegacyOrderProcessor(gateway);
    }
}

4. ScenarioCatalog

src/main/java/com/example/legacy/run7a/ScenarioCatalog.java
Java-Datei öffnen
Rolle im Ablauf

Definiert die fachlich relevanten Fälle, die vor einer Änderung abgedeckt sein müssen.

Im Lesepfad folgt GoldenMasterSnapshot: Speichert die beobachtete Ausgabe in einer vergleichbaren Form.

Typ
class ScenarioCatalog
Verwendet
Verwendet von
LegacyOrderProcessorDemo, LegacyCharacterizationTestRunner
Einstiege
platinumHappyPath(), insufficientStock(), invalidCoupon(), digitalOnlyEuCustomer(), paymentDeclined()
Test Data Builder / Object Mother
package com.example.legacy.run7a;

import java.math.BigDecimal;
import java.util.List;

// Pattern: Test Data Builder / Object Mother - zentrale, lesbare fachliche Szenarien statt kopierter Testdaten.
public final class ScenarioCatalog {
    private ScenarioCatalog() {}

    public static LegacyOrderRequest platinumHappyPath() {
        return new LegacyOrderRequest("ORD-1001", "C-77", "PLATINUM", "DE", "VIP20",
                true, true, false, "CARD_OK", List.of(
                new OrderLineRequest("BOOK-1", 2, new BigDecimal("29.90"), false, true, "REDUCED"),
                new OrderLineRequest("CHAIR-9", 1, new BigDecimal("149.00"), false, true, "STANDARD")));
    }

    public static LegacyOrderRequest insufficientStock() {
        return new LegacyOrderRequest("ORD-1002", "C-88", "STANDARD", "DE", null,
                false, false, false, "CARD_OK", List.of(
                new OrderLineRequest("RARE-1", 5, new BigDecimal("12.00"), false, false, "STANDARD")));
    }

    public static LegacyOrderRequest invalidCoupon() {
        return new LegacyOrderRequest("ORD-1003", "C-99", "SILVER", "DE", "NOPE",
                false, false, false, "CARD_OK", List.of(
                new OrderLineRequest("BOOK-1", 1, new BigDecimal("20.00"), false, true, "REDUCED")));
    }

    public static LegacyOrderRequest digitalOnlyEuCustomer() {
        return new LegacyOrderRequest("ORD-1004", "C-11", "GOLD", "AT", "WELCOME10",
                false, false, false, "CARD_OK", List.of(
                new OrderLineRequest("EBOOK-7", 3, new BigDecimal("19.99"), true, false, "DIGITAL")));
    }

    public static LegacyOrderRequest paymentDeclined() {
        return new LegacyOrderRequest("ORD-1005", "C-55", "STANDARD", "DE", null,
                false, false, false, "CARD_DECLINE", List.of(
                new OrderLineRequest("CHAIR-9", 1, new BigDecimal("149.00"), false, true, "STANDARD")));
    }

    public static LegacyOrderRequest hazardousCrossBorder() {
        return new LegacyOrderRequest("ORD-1006", "C-12", "STANDARD", "AT", null,
                false, false, false, "CARD_OK", List.of(
                new OrderLineRequest("HZ-CLEANER", 1, new BigDecimal("9.99"), false, false, "STANDARD")));
    }
}

5. GoldenMasterSnapshot

src/main/java/com/example/legacy/run7a/GoldenMasterSnapshot.java
Java-Datei öffnen
Rolle im Ablauf

Speichert die beobachtete Ausgabe in einer vergleichbaren Form.

Im Lesepfad folgt RefactoringSeamInventory: Dokumentiert Stellen, an denen Abhängigkeiten später sicher getrennt werden können.

Typ
class GoldenMasterSnapshot
Verwendet
Verwendet von
LegacyOrderProcessorDemo, LegacyCharacterizationTestRunner
Einstiege
from(LegacyInvoiceResult result)
Golden Master
package com.example.legacy.run7a;

import java.util.stream.Collectors;

// Pattern: Golden Master - vergleicht beobachtbares Verhalten, nicht interne Struktur.
public final class GoldenMasterSnapshot {
    private GoldenMasterSnapshot() {}

    public static String from(LegacyInvoiceResult result) {
        String events = result.domainEvents().stream().collect(Collectors.joining(","));
        String auditSignal = result.auditTrail().stream()
                .filter(a -> a.contains("stock check") || a.contains("reserved") || a.contains("authorize payment") || a.contains("invoice saved"))
                .collect(Collectors.joining(" | "));
        return "success=" + result.success()
                + ";order=" + result.orderId()
                + ";invoice=" + result.invoiceId()
                + ";subtotal=" + result.subtotal().toPlainString()
                + ";discount=" + result.discount().toPlainString()
                + ";shipping=" + result.shipping().toPlainString()
                + ";tax=" + result.tax().toPlainString()
                + ";total=" + result.total().toPlainString()
                + ";failure=" + result.failureCode()
                + ";events=" + events
                + ";auditSignal=" + auditSignal;
    }
}

6. RefactoringSeamInventory

src/main/java/com/example/legacy/run7a/RefactoringSeamInventory.java
Java-Datei öffnen
Rolle im Ablauf

Dokumentiert Stellen, an denen Abhängigkeiten später sicher getrennt werden können.

Im Lesepfad folgt LegacyCharacterizationTestRunner: Vergleicht aktuelle Ergebnisse mit dem Golden Master und schützt das bestehende Verhalten.

Typ
interface RefactoringSeamInventory
Verwendet
Verwendet von
Einstiege
Refactoring Seam
package com.example.legacy.run7a;

// Pattern: Refactoring Seam - Zielbild für späteren Run 7B, ohne Verhalten in Run 7A zu verändern.
public interface RefactoringSeamInventory {
    boolean canReserve(String sku, int quantity);
    void reserve(String orderId, String sku, int quantity);
}

7. LegacyCharacterizationTestRunner

src/main/java/com/example/legacy/run7a/LegacyCharacterizationTestRunner.java
Java-Datei öffnen
Rolle im Ablauf

Vergleicht aktuelle Ergebnisse mit dem Golden Master und schützt das bestehende Verhalten.

Damit ist der zentrale Pfad abgeschlossen; der Test-/Runner-Code und die vollständige Dateiliste darunter zeigen die übrigen Varianten.

Typ
class LegacyCharacterizationTestRunner
Verwendet
LegacyOrderProcessor, LegacyTestFixture, ScenarioCatalog, GoldenMasterSnapshot
Verwendet von
Einstiege
main(String[] args)
package com.example.legacy.run7a;

import java.util.LinkedHashMap;
import java.util.Map;

public final class LegacyCharacterizationTestRunner {
    public static void main(String[] args) {
        Map<String, String> expected = new LinkedHashMap<>();
        expected.put("platinumHappyPath", "success=true;order=ORD-1001;invoice=INV-1001;subtotal=208.80;discount=62.64;shipping=15.50;tax=17.54;total=179.20;failure=-;events=InvoiceCreated:INV-1001,OrderAccepted:ORD-1001;auditSignal=stock check sku=BOOK-1 requested=2 available=20 | stock check sku=CHAIR-9 requested=1 available=4 | 2026-02-02T10:15:30Z reserved 2 of BOOK-1 for ORD-1001 | 2026-02-02T10:15:30Z reserved 1 of CHAIR-9 for ORD-1001 | 2026-02-02T10:15:30Z authorize payment CARD_OK total=179.20 | 2026-02-02T10:15:30Z invoice saved INV-1001 total=179.20");
        expected.put("insufficientStock", "success=false;order=ORD-1002;invoice=-;subtotal=0;discount=0;shipping=0;tax=0;total=0;failure=STOCK_UNAVAILABLE;events=OrderRejected:stock-unavailable:RARE-1;auditSignal=stock check sku=RARE-1 requested=5 available=1");
        expected.put("invalidCoupon", "success=false;order=ORD-1003;invoice=-;subtotal=0;discount=0;shipping=0;tax=0;total=0;failure=INVALID_COUPON;events=OrderRejected:invalid-coupon:NOPE;auditSignal=stock check sku=BOOK-1 requested=1 available=20");
        expected.put("digitalOnlyEuCustomer", "success=true;order=ORD-1004;invoice=INV-1001;subtotal=59.97;discount=14.20;shipping=0.00;tax=0.00;total=45.77;failure=-;events=InvoiceCreated:INV-1001,OrderAccepted:ORD-1004;auditSignal=2026-02-02T10:15:30Z authorize payment CARD_OK total=45.77 | 2026-02-02T10:15:30Z invoice saved INV-1001 total=45.77");
        expected.put("paymentDeclined", "success=false;order=ORD-1005;invoice=-;subtotal=0;discount=0;shipping=0;tax=0;total=0;failure=PAYMENT_DECLINED;events=OrderRejected:payment:PAYMENT_PROVIDER_DECLINED;auditSignal=stock check sku=CHAIR-9 requested=1 available=4 | 2026-02-02T10:15:30Z reserved 1 of CHAIR-9 for ORD-1005 | 2026-02-02T10:15:30Z authorize payment CARD_DECLINE total=177.31");
        expected.put("hazardousCrossBorder", "success=false;order=ORD-1006;invoice=-;subtotal=0;discount=0;shipping=0;tax=0;total=0;failure=HAZARDOUS_CROSS_BORDER;events=OrderRejected:hazardous-cross-border;auditSignal=stock check sku=HZ-CLEANER requested=1 available=8");

        assertSnapshot("platinumHappyPath", expected, ScenarioCatalog.platinumHappyPath());
        assertSnapshot("insufficientStock", expected, ScenarioCatalog.insufficientStock());
        assertSnapshot("invalidCoupon", expected, ScenarioCatalog.invalidCoupon());
        assertSnapshot("digitalOnlyEuCustomer", expected, ScenarioCatalog.digitalOnlyEuCustomer());
        assertSnapshot("paymentDeclined", expected, ScenarioCatalog.paymentDeclined());
        assertSnapshot("hazardousCrossBorder", expected, ScenarioCatalog.hazardousCrossBorder());
        System.out.println("RUN7A_TESTS_OK");
    }

    private static void assertSnapshot(String name, Map<String, String> expected, LegacyOrderRequest request) {
        DeterministicLegacyGateway gateway = LegacyTestFixture.gateway();
        LegacyOrderProcessor processor = LegacyTestFixture.processor(gateway);
        String actual = GoldenMasterSnapshot.from(processor.process(request));
        String wanted = expected.get(name);
        if (!wanted.equals(actual)) {
            throw new AssertionError("Snapshot mismatch for " + name + "\nExpected:\n" + wanted + "\nActual:\n" + actual);
        }
    }
}
Alle Projektdateien öffnen (18 Einträge)
⌂ Cockpit