@propriety/court-calendar 1.0.152 → 1.0.153

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.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * End-to-end guard for collections dates reaching the calendar.
3
+ *
4
+ * Collections come back as the raw Court.Collections columns — CollectionID,
5
+ * HearingDate (date-only), HearingTime (HH:mm:ss) — not the CourtDateID/CourtDate
6
+ * shape every other surface reads. A missing mapping is invisible at the API layer
7
+ * and only shows up three stages later as "the date isn't on the calendar":
8
+ *
9
+ * normalizeDateFromApi → CourtDateID undefined, CourtDate undefined
10
+ * filterCourtDates → dropped by the leading `cd.CourtDateID != null`
11
+ * buildCalEvents → dropped by the leading `if (!cd.CourtDate) continue`
12
+ *
13
+ * So this walks the real payload through all three stages rather than asserting on
14
+ * the normalizer alone.
15
+ */
16
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@propriety/court-calendar",
3
3
  "private": false,
4
- "version": "1.0.152",
4
+ "version": "1.0.153",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "./dist/index.mjs",
@@ -0,0 +1,113 @@
1
+ /**
2
+ * End-to-end guard for collections dates reaching the calendar.
3
+ *
4
+ * Collections come back as the raw Court.Collections columns — CollectionID,
5
+ * HearingDate (date-only), HearingTime (HH:mm:ss) — not the CourtDateID/CourtDate
6
+ * shape every other surface reads. A missing mapping is invisible at the API layer
7
+ * and only shows up three stages later as "the date isn't on the calendar":
8
+ *
9
+ * normalizeDateFromApi → CourtDateID undefined, CourtDate undefined
10
+ * filterCourtDates → dropped by the leading `cd.CourtDateID != null`
11
+ * buildCalEvents → dropped by the leading `if (!cd.CourtDate) continue`
12
+ *
13
+ * So this walks the real payload through all three stages rather than asserting on
14
+ * the normalizer alone.
15
+ */
16
+
17
+ import { describe, it, expect, vi, afterEach } from 'vitest';
18
+ import { getAllDates } from '@/helpers/api/courtDates';
19
+ import { buildCalEvents } from '@/helpers/CalEvent';
20
+ import { filterCourtDates } from '@/helpers/filtering';
21
+ import { caseKey } from '@/helpers/courtDates';
22
+ import { DateType } from '@/types';
23
+ import type { Case } from '@/types';
24
+ import { DEFAULT_FILTER_CTX, MOCK_API_KEY } from './fixtures';
25
+
26
+ // Verbatim from GET /court-dates/all?type=Collections on 2026-07-28.
27
+ const RAW_COLLECTIONS = [
28
+ { CollectionID: 1, EntryDate: '2026-07-28T11:48:36-04:00', HearingDate: '2026-07-28', HearingTime: '09:30:00', MuniCode: '' },
29
+ { CollectionID: 2, EntryDate: '2026-07-28T11:48:56-04:00', HearingDate: '2026-07-28', HearingTime: '09:30:00', MuniCode: '' },
30
+ { CollectionID: 3, EntryDate: '2026-07-28T11:50:13-04:00', HearingDate: '2026-07-28', HearingTime: '09:30:00', MuniCode: 'W40' },
31
+ ];
32
+
33
+ function mockFetch(body: unknown) {
34
+ return vi.fn(() => Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(body) }));
35
+ }
36
+
37
+ afterEach(() => {
38
+ vi.unstubAllGlobals();
39
+ });
40
+
41
+ describe('collections reach the calendar end-to-end', () => {
42
+ async function loadCollections() {
43
+ vi.stubGlobal('fetch', mockFetch({ dates: structuredClone(RAW_COLLECTIONS) }));
44
+ return await getAllDates(MOCK_API_KEY, DateType.COLLECTIONS);
45
+ }
46
+
47
+ it('normalizes every row to a usable id and date', async () => {
48
+ const dates = await loadCollections();
49
+ expect(dates).toHaveLength(3);
50
+ for (const d of dates) {
51
+ expect(d.CourtDateID).toEqual(expect.any(Number));
52
+ expect(d.CourtDate).toBeInstanceOf(Date);
53
+ expect(Number.isNaN(new Date(d.CourtDate).getTime())).toBe(false);
54
+ expect(d.DateType).toBe(DateType.COLLECTIONS);
55
+ }
56
+ expect(dates.map((d) => d.CourtDateID)).toEqual([1, 2, 3]);
57
+ });
58
+
59
+ it('keeps the local calendar day — no UTC shift off HearingDate', async () => {
60
+ const [first] = await loadCollections();
61
+ const d = new Date(first.CourtDate);
62
+ // new Date('2026-07-28') would be UTC midnight = Jul 27 20:00 in ET.
63
+ expect([d.getFullYear(), d.getMonth(), d.getDate()]).toEqual([2026, 6, 28]);
64
+ expect([d.getHours(), d.getMinutes()]).toEqual([9, 30]);
65
+ });
66
+
67
+ it('survives filterCourtDates under the default filter context', async () => {
68
+ const dates = await loadCollections();
69
+ const result = filterCourtDates(dates, { ...DEFAULT_FILTER_CTX }, {}, null);
70
+ expect(result).toHaveLength(3);
71
+ });
72
+
73
+ it('survives filterCourtDates on the collections tab', async () => {
74
+ const dates = await loadCollections();
75
+ const result = filterCourtDates(dates, { ...DEFAULT_FILTER_CTX, dateTypeFilter: 'collections' }, {}, null);
76
+ expect(result).toHaveLength(3);
77
+ });
78
+
79
+ it('is not hidden by the default-on unsettled filter despite having no cases', async () => {
80
+ const dates = await loadCollections();
81
+ const result = filterCourtDates(dates, { ...DEFAULT_FILTER_CTX, showOnlyUnsettled: true }, {}, null);
82
+ expect(result).toHaveLength(3);
83
+ });
84
+
85
+ it('produces one CalEvent per hearing once cases resolve to empty', async () => {
86
+ const dates = await loadCollections();
87
+ // UseCaseData's final reconciliation gives every requested key an entry, so an
88
+ // empty bucket reads as "loaded and genuinely empty" rather than "still pending".
89
+ const allCases: Record<string, Case[]> = {};
90
+ for (const d of dates) allCases[caseKey(d)] = [];
91
+
92
+ const events = buildCalEvents(filterCourtDates(dates, { ...DEFAULT_FILTER_CTX }, allCases, null), allCases);
93
+ expect(events).toHaveLength(3);
94
+ for (const e of events) {
95
+ expect(e.dateType).toBe(DateType.COLLECTIONS);
96
+ expect(e.casesLoaded).toBe(true);
97
+ }
98
+ });
99
+
100
+ it('gives each hearing a distinct case key so they do not collapse into one', async () => {
101
+ const dates = await loadCollections();
102
+ expect(new Set(dates.map(caseKey)).size).toBe(3);
103
+ expect(dates.map(caseKey)).toEqual(['col_1', 'col_2', 'col_3']);
104
+ });
105
+
106
+ it('handles a muni-less collections date without breaking village detection', async () => {
107
+ const dates = await loadCollections();
108
+ const noMuni = dates.filter((d) => !d.MuniCode);
109
+ expect(noMuni).toHaveLength(2);
110
+ const events = buildCalEvents(noMuni, {});
111
+ expect(events).toHaveLength(2);
112
+ });
113
+ });