@stamhoofd/backend 2.122.0 → 2.122.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.122.0",
3
+ "version": "2.122.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -57,20 +57,20 @@
57
57
  "@simonbackx/simple-endpoints": "1.21.1",
58
58
  "@simonbackx/simple-errors": "1.5.0",
59
59
  "@simonbackx/simple-logging": "1.0.1",
60
- "@stamhoofd/backend-env": "2.122.0",
61
- "@stamhoofd/backend-i18n": "2.122.0",
62
- "@stamhoofd/backend-middleware": "2.122.0",
63
- "@stamhoofd/crons": "2.122.0",
64
- "@stamhoofd/email": "2.122.0",
65
- "@stamhoofd/excel-writer": "2.122.0",
66
- "@stamhoofd/logging": "2.122.0",
67
- "@stamhoofd/models": "2.122.0",
68
- "@stamhoofd/object-differ": "2.122.0",
69
- "@stamhoofd/queues": "2.122.0",
70
- "@stamhoofd/sql": "2.122.0",
71
- "@stamhoofd/structures": "2.122.0",
72
- "@stamhoofd/types": "2.122.0",
73
- "@stamhoofd/utility": "2.122.0",
60
+ "@stamhoofd/backend-env": "2.122.2",
61
+ "@stamhoofd/backend-i18n": "2.122.2",
62
+ "@stamhoofd/backend-middleware": "2.122.2",
63
+ "@stamhoofd/crons": "2.122.2",
64
+ "@stamhoofd/email": "2.122.2",
65
+ "@stamhoofd/excel-writer": "2.122.2",
66
+ "@stamhoofd/logging": "2.122.2",
67
+ "@stamhoofd/models": "2.122.2",
68
+ "@stamhoofd/object-differ": "2.122.2",
69
+ "@stamhoofd/queues": "2.122.2",
70
+ "@stamhoofd/sql": "2.122.2",
71
+ "@stamhoofd/structures": "2.122.2",
72
+ "@stamhoofd/types": "2.122.2",
73
+ "@stamhoofd/utility": "2.122.2",
74
74
  "archiver": "7.0.1",
75
75
  "axios": "1.16.0",
76
76
  "base-x": "3.0.11",
@@ -91,7 +91,7 @@
91
91
  "stripe": "16.12.0"
92
92
  },
93
93
  "devDependencies": {
94
- "@stamhoofd/test-utils": "2.122.0",
94
+ "@stamhoofd/test-utils": "2.122.2",
95
95
  "@types/cookie": "0.6.0",
96
96
  "@types/luxon": "3.7.1",
97
97
  "@types/mailparser": "3.4.6",
@@ -107,5 +107,5 @@
107
107
  "publishConfig": {
108
108
  "access": "public"
109
109
  },
110
- "gitHead": "3c022b7d7801fb105acfaa401432a8d029abda61"
110
+ "gitHead": "e7ddea427a321328588b839e475266a6364152e3"
111
111
  }
@@ -46,7 +46,8 @@ async function start(dryRun: boolean) {
46
46
  // sort groups by start date
47
47
  groups.sort((a, b) => a.settings.startDate.getTime() - b.settings.startDate.getTime());
48
48
 
49
- const bestCurrentPeriodSpan = await calculateBestCurrentPeriodSpan(groups);
49
+ const activeGroups = groups.filter(g => g.status !== GroupStatus.Archived && g.deletedAt === null);
50
+ const bestCurrentPeriodSpan = await calculateBestCurrentPeriodSpan(activeGroups);
50
51
 
51
52
  const allGroups: Group[] = await migrateGroups({ groups, organization, periodSpan: bestCurrentPeriodSpan }, dryRun);
52
53
 
@@ -435,6 +436,7 @@ async function migrateRegistrations({ organization, period, originalGroup, newGr
435
436
  newWaitingList.periodId = period.id;
436
437
  newWaitingList.settings = GroupSettings.create({
437
438
  name: TranslatedString.create($t(`%yh`) + ' ' + newGroup.settings.name.toString()),
439
+ period: period.getBaseStructure(),
438
440
  });
439
441
 
440
442
  const migrationData = new V1WaitingListMigrationData();
@@ -679,34 +681,9 @@ async function calculateBestCurrentPeriodSpan(groups: Group[]): Promise<{ startD
679
681
  }
680
682
  periodSpanCounts.set(bestPeriod, groups);
681
683
  }
682
- // #endregion
683
-
684
- // #region find the best year
685
- let topYear = defaultYear;
686
- const groupsInTopPeriodSpan = periodSpanCounts.get(topPeriodSpan) ?? [];
687
-
688
- // calculate the number of groups in each year
689
- const yearMap = new Map<number, Group[]>();
690
- for (const { group, year } of groupsInTopPeriodSpan) {
691
- const groups = yearMap.get(year) ?? [];
692
- groups.push(group);
693
- yearMap.set(year, groups);
694
- }
695
-
696
- // get the year with the most groups
697
- let topYearCount = 0;
698
- for (const [year, groups] of yearMap.entries()) {
699
- if (groups.length > topYearCount) {
700
- topYearCount = groups.length;
701
- topYear = year;
702
- }
703
- }
704
- // #endregion
705
-
706
- const startDate = new Date(topYear, topPeriodSpan.startMonth, 1);
707
- const endDate = new Date((new Date(topYear, topPeriodSpan.startMonth + topPeriodSpan.span)).getTime() - 1);
708
684
 
709
- return { startDate, endDate };
685
+ const topYear = calculateTopYear(topPeriodSpan, groups);
686
+ return getTimeSpan(topYear, topPeriodSpan);
710
687
  }
711
688
 
712
689
  // #region helpers
@@ -831,9 +808,151 @@ function createPreviousGroup({ originalGroup, period, cycleInformation, index }:
831
808
  startDate,
832
809
  endDate,
833
810
  hasCustomDates: true,
811
+ period: period.getBaseStructure(),
834
812
  });
835
813
  newGroup.type = originalGroup.type;
836
814
 
837
815
  return newGroup;
838
816
  }
839
817
  // #endregion
818
+
819
+ function calculateTopYear(periodSpan: PeriodSpan, groups: Group[]): number {
820
+ let topYear = periodSpan.startMonth > 6 ? 2025 : 2026;
821
+
822
+ if (groups.length === 0) {
823
+ return topYear;
824
+ }
825
+
826
+ const calculateTopYearHelper = (groups: Group[]): number => {
827
+ // most recent groups first (in case of ties)
828
+ const sortedGroups = [...groups].sort((a, b) => b.settings.startDate.getTime() - a.settings.startDate.getTime());
829
+
830
+ for (const group of sortedGroups) {
831
+ const year = getBestGroupYear(periodSpan, group);
832
+ if (year === null) {
833
+ continue;
834
+ }
835
+ const count = yearCount.get(year) ?? 0;
836
+ yearCount.set(year, count + 1);
837
+ }
838
+
839
+ let topYearCount = 0;
840
+
841
+ for (const [year, count] of yearCount.entries()) {
842
+ if (count > topYearCount) {
843
+ topYearCount = count;
844
+ topYear = year;
845
+ }
846
+ }
847
+
848
+ return topYear;
849
+ };
850
+
851
+ const yearCount = new Map<number, number>();
852
+
853
+ const areAllGroupsPossibleEvents: boolean = groups.every(group => isGroupShorterThanMonth(group));
854
+
855
+ if (areAllGroupsPossibleEvents) {
856
+ return calculateTopYearHelper(groups);
857
+ }
858
+
859
+ const ifEventsExcluded = calculateTopYearHelper(groups.filter(g => !isGroupShorterThanMonth(g)));
860
+ const ifEventsIncluded = calculateTopYearHelper(groups);
861
+ const difference = Math.abs(ifEventsIncluded - ifEventsExcluded);
862
+
863
+ // the difference should never be greater than 1
864
+ if (difference > 1) {
865
+ const max = Math.max(ifEventsIncluded, ifEventsExcluded);
866
+ // year cannot be bigger than 2026
867
+ if (max > 2026) {
868
+ return Math.min(ifEventsIncluded, ifEventsExcluded, 2026);
869
+ }
870
+ return max;
871
+ }
872
+
873
+ // year cannot be bigger than 2026
874
+ if (ifEventsExcluded > 2026 && ifEventsIncluded < 2026) {
875
+ return ifEventsIncluded;
876
+ }
877
+
878
+ return Math.min(ifEventsExcluded, 2026);
879
+ }
880
+
881
+ function isGroupShorterThanMonth(group: Group) {
882
+ const diffMs = group.settings.endDate.getTime() - group.settings.startDate.getTime();
883
+ const msPerDay = 24 * 60 * 60 * 1000;
884
+ const diffDays = diffMs / msPerDay;
885
+
886
+ return diffDays < 30;
887
+ }
888
+
889
+ function getTimeSpan(year: number, periodSpan: PeriodSpan): { startDate: Date; endDate: Date } {
890
+ const startDate = new Date(year, periodSpan.startMonth, 1);
891
+ const endDate = new Date((new Date(year, periodSpan.startMonth + periodSpan.span)).getTime() - 1);
892
+ return { startDate, endDate };
893
+ }
894
+
895
+ function getBestGroupYear(periodSpan: PeriodSpan, group: Group): number | null {
896
+ const startDate = group.settings.startDate;
897
+ const endDate = group.settings.endDate;
898
+ const groupTimeSpan = { startDate, endDate };
899
+
900
+ // ignore groups longer than 2 years
901
+ if (!isYearDifferenceLessThanTwo(startDate, endDate)) {
902
+ return null;
903
+ }
904
+
905
+ const startYear = startDate.getFullYear();
906
+
907
+ const possibleYears: number[] = [startYear + 1, startYear, startYear - 1];
908
+ let topDaysOverlap = 0;
909
+ let topYear = periodSpan.startMonth > 6 ? 2025 : 2026;
910
+
911
+ for (const possibleYear of possibleYears) {
912
+ const possibleTimeSpan = getTimeSpan(possibleYear, periodSpan);
913
+ const result = getDaysMissingAndOverlap(possibleTimeSpan, groupTimeSpan);
914
+ if (result.daysOverlapping > topDaysOverlap) {
915
+ topDaysOverlap = result.daysOverlapping;
916
+ topYear = possibleYear;
917
+ }
918
+ }
919
+
920
+ return topYear;
921
+ }
922
+
923
+ /**
924
+ * Checks if the difference in year between two dates is less than 2.
925
+ * @param {Date} date1
926
+ * @param {Date} date2
927
+ * @returns {boolean}
928
+ */
929
+ function isYearDifferenceLessThanTwo(date1: Date, date2: Date): boolean {
930
+ const d1 = new Date(date1);
931
+ const d2 = new Date(date2);
932
+
933
+ // Get the base year difference
934
+ const yearDiff = d2.getFullYear() - d1.getFullYear();
935
+
936
+ // If the absolute difference is 0 or 1, it's definitely less than 2
937
+ if (Math.abs(yearDiff) < 2) {
938
+ return true;
939
+ }
940
+
941
+ // If the difference is exactly 2, we need to check the exact time elapsed.
942
+ // Example: Jan 1, 2022 to Jan 1, 2024 is exactly 2 years (should be false).
943
+ // Example: Jan 1, 2022 to Dec 31, 2023 is almost 2 years (true, handled above by diff=1 logic? No, 2023-2022=1).
944
+
945
+ // Actually, let's re-evaluate:
946
+ // If d1 = 2022-06-01, d2 = 2024-05-31 -> Year diff = 2. But actual time < 2 years.
947
+ // If d1 = 2022-06-01, d2 = 2024-06-01 -> Year diff = 2. Actual time = 2 years (not less than).
948
+
949
+ // The simplest way to check "less than 2 years" accurately including leap years and days:
950
+ // Calculate the difference in milliseconds and compare against 2 years worth of ms.
951
+
952
+ const MS_PER_YEAR = 365.25 * 24 * 60 * 60 * 1000; // Approximate average year length
953
+ const LIMIT = 2 * MS_PER_YEAR;
954
+
955
+ const diffTime = Math.abs(d2.getTime() - d1.getTime());
956
+
957
+ return diffTime < LIMIT;
958
+ }
@@ -0,0 +1,51 @@
1
+ import { Migration } from '@simonbackx/simple-database';
2
+ import { Group, RegistrationPeriod } from '@stamhoofd/models';
3
+ import { SeedTools } from '../helpers/SeedTools.js';
4
+
5
+ export default new Migration(async () => {
6
+ if (STAMHOOFD.environment === 'test') {
7
+ console.log('skipped in tests');
8
+ return;
9
+ }
10
+
11
+ if (STAMHOOFD.platformName.toLowerCase() !== 'stamhoofd') {
12
+ console.log('skipped for platform (only runs for Stamhoofd): ' + STAMHOOFD.platformName);
13
+ return;
14
+ }
15
+
16
+ const dryRun = false;
17
+ await start(dryRun);
18
+
19
+ if (dryRun) {
20
+ throw new Error('Migration did not finish because of dryRun');
21
+ }
22
+ });
23
+
24
+ /**
25
+ * Groups in v1 have no cached period. This migration adds cached periods to groups that have no cached period yet.
26
+ * @param dryRun
27
+ */
28
+ async function start(dryRun: boolean) {
29
+ await SeedTools.loop({
30
+ query: Group.select(),
31
+ batchSize: 50,
32
+ useTransactionPerBatch: true,
33
+ action: async (group: Group) => {
34
+ if (group.settings.period !== null) {
35
+ return;
36
+ }
37
+
38
+ const period = await RegistrationPeriod.getByID(group.periodId);
39
+ if (period) {
40
+ group.settings.period = period.getBaseStructure();
41
+ if (!dryRun) {
42
+ await group.save();
43
+ }
44
+ return;
45
+ }
46
+
47
+ throw new Error(`No period found for group ${group.id} (periodId: ${group.periodId})`);
48
+ },
49
+
50
+ });
51
+ }