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>
This commit is contained in:
278
test/payment_step_test.dart
Normal file
278
test/payment_step_test.dart
Normal file
@ -0,0 +1,278 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user