cancelReceipt method

Future<CancelReceiptResult> cancelReceipt({
  1. required String originalReceiptId,
  2. required String reason,
  3. List<CancellationItem>? items,
  4. String? returnDisposition,
  5. Map<int, String>? itemReturnDispositions,
  6. String? note,
  7. List<KeckPaymentInput>? payments,
  8. KasseneckReceipt? original,
  9. String? drawerId,
  10. bool? refundTip,
  11. String? tipRefundSource,
})

Storno-Beleg zu einem bestehenden Beleg — voll oder in Teilen.

Ohne items ist es ein Vollstorno. Eine leere Positionsliste ist dagegen ein Fehler: sonst wuerde aus einem missglueckten Teilstorno still ein Vollstorno.

Die Restmengen und die Reichweite des Rechts haelt der Server; die Kasse bietet nur an, was sie fuer moeglich haelt.

reason ist ein Code aus cancellationReasons (input_error …, wie unter /v3); ein unbekannter wirft, bevor etwas hinausgeht.

payments sind die Rueckzahlungen je Zahlung (Betraege negativ, refundOf = id der Originalzahlung). Ohne Angabe spiegelt der Server die Restbetraege jeder Originalzahlung; ein Teilstorno eines Belegs mit mehreren Zahlungen braucht sie (cancellation_payments_required). Eine Einzel-Zahlungsart am Storno gibt es unter /v3 nicht mehr.

Karten-Storno: provider, providerPaymentId und providerData der Zahlung beschreiben die ERSTATTUNG am Terminal. Eine Karten-Rueckbuchung ueber einen Anbieter braucht einen Bezug: ihre eigene providerPaymentId oder, mit original, die Kennung der erstatteten Kartenzahlung dort (cardRefundReference). Fehlt beides, wirft der Aufruf vor dem Senden.

returnDisposition sagt, wohin die Ware der stornierten Artikelzeilen geht (Lager): restock, defective oder disposed aus returnDispositions, fuer alle Positionen. itemReturnDispositions waehlt je Position abweichend (Schluessel = Index im Original, wie in items; nur fuer Positionen in items). Fehlt beides, bucht der Server restock; Zeilen ohne articleId bucht er nie. Ein unbekannter Wert wirft, bevor etwas hinausgeht. Ohne Wahl ist die Nutzlast dieselbe wie vor 10.2.

drawerId: Lade, in deren Kassenstand der Storno zaehlt; wie bei sell.

refundTip ist die Antwort auf „Trinkgeld auch zurueckgeben?" (Schichten-Spec, Nachtrag 5): false ohne das Mitarbeiter-Trinkgeld (ohne items = alle Restmengen ausser dem Trinkgeld), true mit items nimmt das offene Trinkgeld dazu; fehlt = wie bisher. tipRefundSource (drawer | employee, aus tipRefundSources) nur bei Barrueckgabe (asksTipRefundSource), nie zusammen mit refundTip: false; refundTip: true ohne Quelle heisst bei Barrueckgabe: aus der Lade. Die Positionen dazu rechnet cancellationItemsForTip. An einem Beleg mit Wertgutschein gibt es false nicht (tip_keep_not_possible_with_voucher, asksRefundTip).

Implementation

Future<CancelReceiptResult> cancelReceipt({
  required String originalReceiptId,
  required String reason,
  List<CancellationItem>? items,
  String? returnDisposition,
  Map<int, String>? itemReturnDispositions,
  String? note,
  List<KeckPaymentInput>? payments,
  KasseneckReceipt? original,
  String? drawerId,
  bool? refundTip,
  String? tipRefundSource,
}) async {
  const name = Aufrufe.cancelReceipt;
  if (originalReceiptId.trim().isEmpty) {
    throw const KasseneckValidationError(name, 'originalReceiptId fehlt', 'request');
  }
  if (!cancellationReasons.containsKey(reason)) {
    throw const KasseneckValidationError(name, 'Storno-Grund fehlt oder ist unbekannt', 'request');
  }
  if (items != null) {
    if (items.isEmpty) {
      throw const KasseneckValidationError(name, 'positionen muss eine nicht leere Liste sein', 'request');
    }
    if (items.any((p) => p.index < 0 || p.quantity < 1)) {
      throw const KasseneckValidationError(name, 'Storno-Menge muss eine ganze Zahl >= 1 sein', 'request');
    }
  }
  final nutzlast = cancellationPayload(
    name,
    items: items,
    returnDisposition: returnDisposition,
    itemReturnDispositions: itemReturnDispositions,
  );
  if (note != null && note.length > _anmerkungHoechstlaenge) {
    throw const KasseneckValidationError(name, 'Anmerkung ist zu lang', 'request');
  }
  if (payments != null) {
    final fehler = paymentsError(payments, cancellation: true);
    if (fehler != null) throw KasseneckValidationError(name, fehler, 'request');
  }
  if (original != null && original.receiptId != originalReceiptId) {
    throw KasseneckValidationError(
        name, 'original (${original.receiptId}) ist nicht der Beleg originalReceiptId ($originalReceiptId)', 'request');
  }
  if (payments != null) assertCardRefunds(payments, original);
  if (drawerId != null) _ladeKennung(name, drawerId);
  final trinkgeld = tipChoicePayload(name, refundTip: refundTip, tipRefundSource: tipRefundSource);

  final daten = await _signiertRufen(
    name,
    params: {
      'originalReceiptId': originalReceiptId,
      'reason': reason,
      'items': ?nutzlast.items,
      'returnDisposition': ?nutzlast.returnDisposition,
      if (note != null && note.isNotEmpty) 'note': note,
      if (payments != null) 'payments': [for (final z in payments) z.toJson()],
      'drawerId': ?drawerId,
      ...trinkgeld,
    },
  );

  // Ab hier ist der Storno-Beleg ausgestellt und signiert. Scheitert das
  // Lesen, kommt `response_unreadable` (Ausgang unklar) mit der Kennung,
  // sofern die Antwort sie mitbrachte: ohne sie ist der gesetzlich
  // vorgeschriebene Storno-Beleg da, aber fuer die Kasse unerreichbar, und
  // ein zweiter Storno waere eine zweite Ruecknahme.
  final CancelReceiptResult ergebnis = readSignedResponse(
    name,
    () => CancelReceiptResult.fromResponse(name, daten, () => _belegAus(daten, name)),
    receiptId: () => _kennungAus(daten),
  );
  // Die Storno-Antwort trägt weder Layout noch Testkennzeichen. Damit der
  // Storno einer Testkasse nie wie ein gültiger Beleg gedruckt wird, gelten
  // die Kennzeichen des Originals, und eine Test-Umgebung steht für
  // TESTKASSE (wie `KasseneckApi.cancelReceipt` mit `kr_test_`).
  final beleg = ergebnis.receipt;
  if (original?.testCashregister == true || testEnvironment) beleg.testCashregister = true;
  if (original?.testSignature == true && !beleg.testCashregister) beleg.testSignature = true;
  return ergebnis;
}