@fin.cx/compatibility 5.0.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ts/extf/index.ts CHANGED
@@ -518,8 +518,9 @@ export const correctionOf = (
518
518
  export interface ICorrectionBuchungsstapelOptions {
519
519
  /**
520
520
  * The bookings of the period as they were handed over, as a whole: the bookings of its Buchungsstapel as stored
521
- * (`IBuchungsstapelFile.bookings`), or after a correction the period's bookings as that correction left them
522
- * (`bookingsAfter`). Each must state its cost centres: one stored before 4.1 does not, and is refused
521
+ * (`IBuchungsstapelFile.bookings`), after a correction the period's bookings as that correction left them
522
+ * (`bookingsAfter`), and after more than one Stapel the bookings that stand after all of them (`bookingsHeldOf`).
523
+ * Each must state its cost centres: one stored before 4.1 does not, and is refused
523
524
  * (`handed_over_cost_centres_unknown`); pass the rows sent for it. Bookings stored by any version, before or after
524
525
  * writing, are compared as a file states them (`comparableOf`), so a reference the writer changed is no change.
525
526
  */
@@ -529,7 +530,8 @@ export interface ICorrectionBuchungsstapelOptions {
529
530
  * `readExtfBuchungsstapel(bytes).rows`, skr 7.0.1 and later) or the rows given to the writer
530
531
  * (`IBuchungsstapelFile.rows`), taken as the file states them (`bookingsAsWritten`). What DATEV received, cost
531
532
  * centres (fields 37 and 38) included, so a reversal mirrors it exactly. For a Stapel whose bookings were stored
532
- * before 4.1, which record no cost centres.
533
+ * before 4.1, which record no cost centres. The rows of one Stapel: after more than one, pass `handedOver` as
534
+ * `bookingsHeldOf` of the bookings of each file read back.
533
535
  */
534
536
  handedOverRows?: readonly IDatevBuchungsstapelRow[];
535
537
  /** the period's drafts as they stand now */
@@ -583,3 +585,69 @@ export const writeCorrectionBuchungsstapel = (optionsArg: ICorrectionBuchungssta
583
585
  // the rows booked anew as the correction file states them, after the reversals
584
586
  return { ...file, reversed, added, bookingsAfter: [...kept, ...file.bookings.slice(reversed.length)] };
585
587
  };
588
+
589
+ /**
590
+ * The bookings of a period as DATEV holds them after its Stapel, handed over in the order given: the period's
591
+ * Buchungsstapel, the additional Stapel after it and its correction Stapel (`writeCorrectionBuchungsstapel`), each as
592
+ * the bookings it stated (`IBuchungsstapelFile.bookings` as stored, or `bookingsOfRows` of its file read back), in
593
+ * their order. An ordinary booking stands. A general reversal (field 118 "Generalumkehr") takes back one booking that
594
+ * stands before it, in an earlier Stapel or earlier in its own, and does not stand itself: the booking it mirrors says
595
+ * what it says (`differingBookingCount`'s fields, the reversal apart), compared as a file states them
596
+ * (`comparableOf`), so wording and document link do not count. A booking on the cost centres the reversal states is
597
+ * taken first, then one stored before 4.1, which records none; of several, the last, as a correction reverses the
598
+ * last of several bookings it no longer holds. A reversal of no booking that stands before it is refused
599
+ * (`invalid_bookings`, naming its Stapel and place): DATEV would hold a counter-booking of nothing. The bookings that
600
+ * stand, as they were given and in their order: what the next correction is compared against (`handedOver`), and
601
+ * after one correction its `bookingsAfter`. A booking a Buchungsstapel cannot hold is refused (`invalid_bookings`).
602
+ */
603
+ export const bookingsHeldOf = (stapelArg: readonly (readonly IExportBooking[])[]): IExportBooking[] => {
604
+ const given = stapelArg.flat();
605
+ const comparable = comparableOf(given);
606
+ // where each booking stands: its Stapel and its place in it, for a refusal
607
+ const places = stapelArg.flatMap((stapelBookingsArg, stapelIndexArg) => stapelBookingsArg.map((_bookingArg, indexArg) => ({ stapel: stapelIndexArg, index: indexArg })));
608
+ const takenBack = given.map(() => false);
609
+ // the bookings that stand, by what they say: recorded ones on every key field and on the rest, unrecorded ones on the rest
610
+ const recordedByFullKey = new Map<string, number[]>();
611
+ const recordedByBaseKey = new Map<string, number[]>();
612
+ const unrecordedByBaseKey = new Map<string, number[]>();
613
+ const stand = (queuesArg: Map<string, number[]>, keyArg: string, indexArg: number): void => {
614
+ const queue = queuesArg.get(keyArg);
615
+ if (queue) {
616
+ queue.push(indexArg);
617
+ } else {
618
+ queuesArg.set(keyArg, [indexArg]);
619
+ }
620
+ };
621
+ // the last booking of a queue that still stands; a booking taken back through another queue is passed over for good
622
+ const lastStanding = (queuesArg: Map<string, number[]>, keyArg: string): number | undefined => {
623
+ const queue = queuesArg.get(keyArg);
624
+ while (queue && queue.length > 0 && takenBack[queue[queue.length - 1]]) {
625
+ queue.pop();
626
+ }
627
+ return queue?.[queue.length - 1];
628
+ };
629
+ comparable.forEach((bookingArg, indexArg) => {
630
+ if (!bookingArg.generalumkehr) {
631
+ if (recordsCostCentres(bookingArg)) {
632
+ stand(recordedByFullKey, fullKeyOf(bookingArg), indexArg);
633
+ stand(recordedByBaseKey, baseKeyOf(bookingArg), indexArg);
634
+ } else {
635
+ stand(unrecordedByBaseKey, baseKeyOf(bookingArg), indexArg);
636
+ }
637
+ return;
638
+ }
639
+ // the booking the reversal mirrors says what it says
640
+ const { generalumkehr: _generalumkehr, ...mirrored } = bookingArg;
641
+ const baseKey = baseKeyOf(mirrored);
642
+ const unrecorded = lastStanding(unrecordedByBaseKey, baseKey);
643
+ const taken = recordsCostCentres(mirrored)
644
+ ? lastStanding(recordedByFullKey, fullKeyOf(mirrored)) ?? unrecorded
645
+ : Math.max(lastStanding(recordedByBaseKey, baseKey) ?? -1, unrecorded ?? -1);
646
+ if (taken === undefined || taken < 0) {
647
+ const { stapel, index } = places[indexArg];
648
+ throw new CompatibilityError('invalid_bookings', `Stapel ${stapel + 1}, ${bookingNameOf(given[indexArg], index)} is a general reversal of no booking handed over before it`);
649
+ }
650
+ takenBack[taken] = true;
651
+ });
652
+ return given.filter((_bookingArg, indexArg) => !comparable[indexArg].generalumkehr && !takenBack[indexArg]);
653
+ };
package/ts/index.ts CHANGED
@@ -2,3 +2,4 @@ export * from './error.js';
2
2
  export * from './encoding/index.js';
3
3
  export * as extf from './extf/index.js';
4
4
  export * as belegbilder from './belegbilder/index.js';
5
+ export * as buchungsliste from './buchungsliste/index.js';