Files
Holzleitner-Lieferservice-App/test/payment_step_test.dart
Dennis Nemec dd7fe9a8eb feat(zahlung): eigener Step "Zahlung" vor der Uebersicht
- Neuer Workflow-Step "Zahlung" zeigt ausgelieferte Artikel mit Menge und
  Preis, Betragsaufstellung, Zahlungsstatus und einen grossen Button
  "Zahlung abwickeln" (fest unten)
- Zahlungs-Modal: grosser offener Betrag, Methoden als Cards aus den
  Backend-Stammdaten, bei EC-Karte gross die Kundennummer; schliesst nur
  ueber Abbrechen, "Zahlung erhalten" oder "Lieferung abbrechen" (mit Grund)
- Bestaetigung wird am Server protokolliert (RecordDeliveryPayment); bis
  dahin sind "Weiter" und die Uebersicht gesperrt, ausser es ist nichts
  offen (bereits bezahlt). Aendert sich der Betrag danach, wird die Zahlung
  als veraltet markiert und muss erneut abgewickelt werden
- Uebersicht zeigt die abgewickelte Zahlung statt eigener Methodenauswahl;
  Unterschrift-Flow ohne doppelte Inkasso-Stufe; Abschluss sendet Methode
  und Inkasso-Flag der gueltigen Zahlung
- DeliveryPaymentStatus als einzige Quelle fuer offenen Betrag
  (gleiche Formel wie Backend); Artikelliste/Betragskarte als geteilte
  Widgets; Begruendungs-Dialog geteilt mit Info-Step
- API-Client aus aktueller Backend-Spec neu generiert
- Tests fuer Zahlungsstatus und Modal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:04:36 +02:00

279 lines
8.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hl_lieferservice/domain/entity/address.dart';
import 'package:hl_lieferservice/domain/entity/customer.dart';
import 'package:hl_lieferservice/domain/entity/delivery.dart';
import 'package:hl_lieferservice/domain/entity/delivery_credit.dart';
import 'package:hl_lieferservice/domain/entity/delivery_item.dart';
import 'package:hl_lieferservice/domain/entity/delivery_payment.dart';
import 'package:hl_lieferservice/domain/entity/payment_method.dart';
import 'package:hl_lieferservice/domain/entity/scan_progress.dart';
import 'package:hl_lieferservice/domain/entity/tour.dart';
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
import 'package:hl_lieferservice/domain/repository/payment_methods_repository.dart';
import 'package:hl_lieferservice/feature/delivery/detail/model/delivery_payment_status.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_sheet.dart';
import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart';
const _address = Address(
street: 'Konstantinplatz',
houseNumber: '3',
postalCode: '41849',
city: 'Wassenberg',
country: 'DE',
);
final _methods = [
PaymentMethod(
id: 'm-cash',
code: 'cash',
name: 'Bar',
active: true,
createdAt: DateTime(2026),
),
PaymentMethod(
id: 'm-ec',
code: 'ec_card',
name: 'EC-Karte',
active: true,
createdAt: DateTime(2026),
),
PaymentMethod(
id: 'm-inv',
code: 'invoice',
name: 'Rechnung',
active: true,
createdAt: DateTime(2026),
),
];
/// Lieferung: 2 × 50 € Warenwert, 20 € angezahlt ⇒ 80 € offen.
Delivery _delivery({double prepaid = 20}) => Delivery(
id: 'd1',
tourId: 't1',
customerId: 'c1',
contactPersonIds: const [],
deliveryAddressSnapshot: _address,
erpBelegartId: 24,
erpBelegnummer: 'V-1',
state: DeliveryState.active,
sortOrder: 1,
prepaidAmount: prepaid,
paymentMethodId: 'm-cash',
items: [
DeliveryItem(
id: 'i1',
deliveryId: 'd1',
articleId: 'a1',
warehouseId: 'w1',
belegzeilenNr: 1,
requiredQuantity: 2,
unitPrice: 50,
scanProgress: ScanProgress(
status: ScanStatus.done,
scannedQuantity: 2,
lastUpdatedAt: DateTime(2026),
),
),
],
);
TourDetails _details(
Delivery delivery, {
DeliveryPayment? payment,
DeliveryCredit? credit,
}) => TourDetails(
tour: Tour(
id: 't1',
accountId: 414,
date: DateTime(2026, 9, 25),
syncedAt: DateTime(2026),
),
deliveries: [delivery],
customers: const {
'c1': Customer(
id: 'c1',
name: 'Cremer',
erpCustomerId: 7843068,
address: _address,
),
},
contacts: const {},
articles: const {},
warehouses: const {},
notesByDeliveryId: const {},
creditsByDeliveryId: {if (credit != null) 'd1': credit},
paymentsByDeliveryId: {if (payment != null) 'd1': payment},
services: const [],
serviceValuesByDeliveryId: const {},
contactSourcesByDeliveryId: const {},
contactChannelsBySourceId: const {},
);
DeliveryPayment _payment(int cents, {String code = 'ec_card'}) =>
DeliveryPayment(
id: 'p1',
deliveryId: 'd1',
paymentMethodId: code == 'ec_card' ? 'm-ec' : 'm-cash',
paymentMethodCode: code,
amountCents: cents,
recordedAt: DateTime(2026, 9, 25, 13, 5),
);
class _FakePaymentMethods implements PaymentMethodsRepository {
@override
Future<List<PaymentMethod>> list({bool includeInactive = false}) async =>
_methods;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
group('DeliveryPaymentStatus', () {
test('offener Betrag ohne Zahlung ist nicht erledigt', () {
final d = _delivery();
final s = DeliveryPaymentStatus.of(d, _details(d));
expect(s.openCents, 8000);
expect(s.isSettled, isFalse);
expect(s.isStale, isFalse);
});
test('passende Zahlung erledigt den Step', () {
final d = _delivery();
final s = DeliveryPaymentStatus.of(
d,
_details(d, payment: _payment(8000)),
);
expect(s.validPayment?.id, 'p1');
expect(s.isSettled, isTrue);
});
test('Gutschrift nach Zahlung macht sie veraltet', () {
final d = _delivery();
final s = DeliveryPaymentStatus.of(
d,
_details(
d,
payment: _payment(8000),
credit: const DeliveryCredit(
deliveryId: 'd1',
amountCents: 1000,
reason: 'Kratzer',
),
),
);
expect(s.openCents, 7000);
expect(s.isStale, isTrue);
expect(s.isSettled, isFalse);
});
test('bereits bezahlt: nichts offen, Step überspringbar', () {
final d = _delivery(prepaid: 100);
final s = DeliveryPaymentStatus.of(d, _details(d));
expect(s.hasOpenAmount, isFalse);
expect(s.isSettled, isTrue);
});
});
group('Zahlungs-Modal', () {
late PaymentSheetResult? result;
Future<void> openSheet(WidgetTester tester) async {
result = null;
final cubit = PaymentMethodsCubit(repository: _FakePaymentMethods());
await cubit.load();
await tester.pumpWidget(
BlocProvider.value(
value: cubit,
child: MaterialApp(
home: Builder(
builder:
(context) => Scaffold(
body: TextButton(
onPressed: () async {
result = await showPaymentSheet(
context,
openCents: 8000,
customerNumber: 7843068,
preselectedMethodId: 'm-cash',
);
},
child: const Text('open'),
),
),
),
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
}
testWidgets('zeigt Betrag, bei EC die Kundennummer, bestätigt Methode', (
tester,
) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 2.75;
addTearDown(tester.view.reset);
await openSheet(tester);
expect(find.textContaining('80,00'), findsOneWidget);
expect(find.text('Bar'), findsOneWidget);
expect(find.text('EC-Karte'), findsOneWidget);
expect(find.text('Rechnung'), findsOneWidget);
expect(find.text('7843068'), findsNothing);
await tester.tap(find.text('EC-Karte'));
await tester.pumpAndSettle();
expect(find.text('7843068'), findsOneWidget);
await tester.tap(find.text('Zahlung erhalten'));
await tester.pumpAndSettle();
expect(result, isA<PaymentConfirmed>());
expect((result as PaymentConfirmed).method.code, 'ec_card');
});
testWidgets('Rechnung ändert den Bestätigungs-Text', (tester) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 2.75;
addTearDown(tester.view.reset);
await openSheet(tester);
await tester.tap(find.text('Rechnung'));
await tester.pumpAndSettle();
expect(find.text('Auf Rechnung bestätigen'), findsOneWidget);
});
testWidgets('Abbrechen schließt ohne Ergebnis', (tester) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 2.75;
addTearDown(tester.view.reset);
await openSheet(tester);
await tester.tap(find.text('Abbrechen'));
await tester.pumpAndSettle();
expect(result, isNull);
expect(find.text('Zahlung abwickeln'), findsNothing);
});
testWidgets('Lieferung abbrechen fragt Grund ab', (tester) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 2.75;
addTearDown(tester.view.reset);
await openSheet(tester);
await tester.tap(find.text('Lieferung abbrechen'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'Kunde zahlt nicht');
await tester.tap(find.text('Bestätigen'));
await tester.pumpAndSettle();
expect(result, isA<DeliveryCancelRequested>());
expect((result as DeliveryCancelRequested).reason, 'Kunde zahlt nicht');
});
});
}