sell method

Future<KasseneckReceipt> sell({
  1. required List<KasseneckItem> items,
  2. required List<KeckPaymentInput> payments,
  3. int? tipCents,
  4. bool tipKeptByEmployee = false,
  5. List<String>? customerLines,
  6. List<String>? legalNotices,
  7. String? drawerId,
  8. List<KeckVoucher>? vouchers,
})

Normalbeleg (Verkauf): der signierte Beleg samt Belegkopf.

drawerId: Lade, in deren Kassenstand der Beleg zaehlt (Schichtbetrieb, Geraete-Einstellung drawerId). Ohne Angabe gilt die Lade des Geraets bzw. die Hauptlade der Kasse. Leer oder nur Leerraum wirft vor dem Senden.

Unter /v3 gehen die Zahlungen immer als payments hinaus (siehe KeckPaymentInput: Karte mit Anbieter und Kennung, Bar mit tenderedCents), nie die Einzelfelder paymentMethod, creditCardProvider, cardPaymentId, cardPaymentData aus 0.x; mixed wird nie gesendet. Den Zahlbetrag rechnet der Server; weicht die Summe ab, kommt payments_sum_mismatch mit expectedCents (paymentsExpectedCents), und es wurde nichts signiert.

tipKeptByEmployee: Bar-Trinkgeld „gleich behalten“ -- die Mitarbeiterin steckt das Trinkgeld beim Verkauf gleich ein. Der Beleg bleibt, wie er ist; im Kassenstand zaehlt der Betrag nicht ins Bar-Soll und gilt sofort als „schon genommen“. Nur mit Trinkgeld; dann geht tip: {cents, keptByEmployee: true} hinaus, sonst bleibt die Nutzlast Byte fuer Byte wie bisher. Der Server nimmt es nur bei Mitarbeiter- Trinkgeld, bar und sofort erhalten. Vorbelegung: posTipKeptByDefault.

vouchers: Gutscheine des Belegs (Altform oder neue Form: Mehrzweck, Einzweck, Leistung, Aktion, Auszahlung). Gepruefte Wuensche gehen roh hinaus (KeckVoucher.toPayload); was der Server abwiese (Serverfelder, Gutscheinzeilen in items, Verkauf und Einloesung zusammen, Grenzen), wirft vor dem Senden. Ein Gutscheinverkauf und eine Auszahlung brauchen keine Positionen; ein Beleg nur aus Auszahlungen nimmt genau eine negative Barzahlung. Den Zahlbetrag rechnet receiptDueCents mit den Pruefungen aus checkVoucher.

Implementation

Future<KasseneckReceipt> sell({
  required List<KasseneckItem> items,
  required List<KeckPaymentInput> payments,
  int? tipCents,
  bool tipKeptByEmployee = false,
  List<String>? customerLines,
  List<String>? legalNotices,
  String? drawerId,
  List<KeckVoucher>? vouchers,
}) async {
  const name = Aufrufe.createReceipt;
  final gutscheine = vouchers ?? const <KeckVoucher>[];
  final zeilenFehler = voucherItemError(items);
  if (zeilenFehler != null) throw KasseneckValidationError(name, zeilenFehler, 'request');
  if (items.isEmpty && !vouchersWithoutItems(gutscheine)) {
    // Ein leerer Verkauf ist kein Verkauf, und der Fehler soll fallen,
    // bevor irgendetwas in die Signaturkette geraet.
    throw const KasseneckValidationError(name, 'Positionen fehlen', 'request');
  }
  if (items.any((p) => !p.isValid)) {
    throw const KasseneckValidationError(name, 'Ungueltige Position uebergeben', 'request');
  }
  if (tipCents != null && tipCents < 0) {
    throw const KasseneckValidationError(name, 'Trinkgeld muss >= 0 sein', 'request');
  }
  final mitTrinkgeld = tipCents != null && tipCents > 0;
  if (mitTrinkgeld && items.isEmpty) {
    throw const KasseneckValidationError(name, 'Trinkgeld: Beleg braucht mindestens eine Position', 'request');
  }
  if (gutscheine.isNotEmpty) {
    final gutscheinFehler = voucherWishError(gutscheine, items, training: false);
    if (gutscheinFehler != null) throw KasseneckValidationError(name, gutscheinFehler, 'request');
  }
  final fehler = paymentsError(payments,
      cancellation: false, payout: isPayoutOnly(gutscheine, items, withTip: mitTrinkgeld));
  if (fehler != null) throw KasseneckValidationError(name, fehler, 'request');
  if (drawerId != null) _ladeKennung(name, drawerId);

  final daten = await _signiertRufen(
    name,
    params: {
      'receiptType': 'standard',
      'items': items.map((p) => p.toJson()).toList(),
      if (gutscheine.isNotEmpty) 'vouchers': [for (final v in gutscheine) v.toPayload()],
      'payments': [for (final z in payments) z.toJson()],
      if (tipCents != null && tipCents > 0) 'tip': tipKeptByEmployee ? {'cents': tipCents, 'keptByEmployee': true} : tipCents,
      if (customerLines != null && customerLines.isNotEmpty) 'customerDetails': customerLines.join('\n'),
      if (legalNotices != null && legalNotices.isNotEmpty) 'legalMessage': legalNotices.join('\n'),
      'drawerId': ?drawerId,
    },
  );
  // Ab hier ist der Beleg signiert: wer ihn nicht lesen kann, bekommt
  // `response_unreadable` (Ausgang unklar) mit der Kennung, nie einen
  // gewoehnlichen Fehler, der zum zweiten Verkauf einluede.
  return readSignedResponse(name, () => _belegAus(daten, name), receiptId: () => _kennungAus(daten));
}