@htlkg/data 0.0.19 → 0.0.20
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/dist/client/index.d.ts +257 -1
- package/dist/client/index.js +59 -1
- package/dist/client/index.js.map +1 -1
- package/dist/common-DSxswsZ3.d.ts +40 -0
- package/dist/hooks/index.d.ts +75 -9
- package/dist/hooks/index.js +131 -33
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +351 -13
- package/dist/index.js.map +1 -1
- package/dist/mutations/index.d.ts +4 -2
- package/dist/mutations/index.js +200 -0
- package/dist/mutations/index.js.map +1 -1
- package/dist/{productInstances-BA3cNsYc.d.ts → productInstances-BpQv1oLS.d.ts} +2 -40
- package/dist/queries/index.d.ts +3 -0
- package/dist/queries/index.js +82 -0
- package/dist/queries/index.js.map +1 -1
- package/dist/reservations-C0FNm__0.d.ts +154 -0
- package/dist/reservations-CdDfkcZ_.d.ts +172 -0
- package/package.json +3 -3
- package/src/client/index.ts +18 -0
- package/src/client/reservations.ts +336 -0
- package/src/hooks/createDataHook.test.ts +534 -0
- package/src/hooks/createDataHook.ts +20 -13
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useReservations.ts +145 -0
- package/src/mutations/index.ts +14 -0
- package/src/mutations/productInstances/productInstances.test.ts +3 -3
- package/src/mutations/reservations.test.ts +459 -0
- package/src/mutations/reservations.ts +452 -0
- package/src/queries/index.ts +11 -0
- package/src/queries/reservations.test.ts +374 -0
- package/src/queries/reservations.ts +247 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useReservations Hook
|
|
3
|
+
*
|
|
4
|
+
* Vue composable for fetching and managing reservation data with reactive state.
|
|
5
|
+
* Provides loading states, error handling, pagination, and refetch capabilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Ref, ComputedRef } from "vue";
|
|
9
|
+
import type { Reservation } from "../queries/reservations";
|
|
10
|
+
import { createDataHook, type BaseHookOptions } from "./createDataHook";
|
|
11
|
+
|
|
12
|
+
export interface UseReservationsOptions extends BaseHookOptions {
|
|
13
|
+
/** Filter by brand ID */
|
|
14
|
+
brandId?: string;
|
|
15
|
+
/** Filter by start date (check-in date >= startDate) */
|
|
16
|
+
startDate?: string;
|
|
17
|
+
/** Filter by end date (check-in date <= endDate) */
|
|
18
|
+
endDate?: string;
|
|
19
|
+
/** Filter by reservation status */
|
|
20
|
+
status?: Reservation["status"];
|
|
21
|
+
/** Filter by contact/visit ID */
|
|
22
|
+
contactId?: string;
|
|
23
|
+
/** Pagination token for fetching next page */
|
|
24
|
+
nextToken?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseReservationsReturn {
|
|
28
|
+
/** Reactive array of reservations */
|
|
29
|
+
reservations: Ref<Reservation[]>;
|
|
30
|
+
/** Computed array of confirmed reservations */
|
|
31
|
+
confirmedReservations: ComputedRef<Reservation[]>;
|
|
32
|
+
/** Computed array of active reservations (confirmed or checked_in) */
|
|
33
|
+
activeReservations: ComputedRef<Reservation[]>;
|
|
34
|
+
/** Loading state */
|
|
35
|
+
loading: Ref<boolean>;
|
|
36
|
+
/** Error state */
|
|
37
|
+
error: Ref<Error | null>;
|
|
38
|
+
/** Refetch reservations */
|
|
39
|
+
refetch: () => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build filter from hook options
|
|
44
|
+
*/
|
|
45
|
+
function buildFilter(options: UseReservationsOptions): any {
|
|
46
|
+
const conditions: any[] = [];
|
|
47
|
+
|
|
48
|
+
if (options.brandId) {
|
|
49
|
+
conditions.push({ brandId: { eq: options.brandId } });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (options.status) {
|
|
53
|
+
conditions.push({ status: { eq: options.status } });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.contactId) {
|
|
57
|
+
conditions.push({ visitId: { eq: options.contactId } });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (options.startDate) {
|
|
61
|
+
conditions.push({ checkIn: { ge: options.startDate } });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (options.endDate) {
|
|
65
|
+
conditions.push({ checkIn: { le: options.endDate } });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (options.filter) {
|
|
69
|
+
conditions.push(options.filter);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (conditions.length === 0) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (conditions.length === 1) {
|
|
77
|
+
return conditions[0];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { and: conditions };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Internal hook created by factory
|
|
85
|
+
*/
|
|
86
|
+
const useReservationsInternal = createDataHook<
|
|
87
|
+
Reservation,
|
|
88
|
+
UseReservationsOptions,
|
|
89
|
+
{ confirmedReservations: Reservation[]; activeReservations: Reservation[] }
|
|
90
|
+
>({
|
|
91
|
+
model: "Reservation",
|
|
92
|
+
dataPropertyName: "reservations",
|
|
93
|
+
buildFilter,
|
|
94
|
+
computedProperties: {
|
|
95
|
+
confirmedReservations: (reservations) =>
|
|
96
|
+
reservations.filter((r) => r.status === "confirmed"),
|
|
97
|
+
activeReservations: (reservations) =>
|
|
98
|
+
reservations.filter((r) => r.status === "confirmed" || r.status === "checked_in"),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Composable for fetching and managing reservations
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* import { useReservations } from '@htlkg/data/hooks';
|
|
108
|
+
*
|
|
109
|
+
* const { reservations, loading, error, refetch } = useReservations({
|
|
110
|
+
* brandId: 'brand-123',
|
|
111
|
+
* startDate: '2026-01-01',
|
|
112
|
+
* endDate: '2026-01-31',
|
|
113
|
+
* status: 'confirmed'
|
|
114
|
+
* });
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @example With contact filter
|
|
118
|
+
* ```typescript
|
|
119
|
+
* const { reservations, loading } = useReservations({
|
|
120
|
+
* contactId: 'contact-123',
|
|
121
|
+
* limit: 50
|
|
122
|
+
* });
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @example With computed properties
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const { reservations, activeReservations, confirmedReservations } = useReservations({
|
|
128
|
+
* brandId: 'brand-123'
|
|
129
|
+
* });
|
|
130
|
+
*
|
|
131
|
+
* // activeReservations includes confirmed + checked_in
|
|
132
|
+
* // confirmedReservations includes only confirmed
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export function useReservations(options: UseReservationsOptions = {}): UseReservationsReturn {
|
|
136
|
+
const result = useReservationsInternal(options);
|
|
137
|
+
return {
|
|
138
|
+
reservations: result.reservations as Ref<Reservation[]>,
|
|
139
|
+
confirmedReservations: result.confirmedReservations as ComputedRef<Reservation[]>,
|
|
140
|
+
activeReservations: result.activeReservations as ComputedRef<Reservation[]>,
|
|
141
|
+
loading: result.loading,
|
|
142
|
+
error: result.error,
|
|
143
|
+
refetch: result.refetch,
|
|
144
|
+
};
|
|
145
|
+
}
|
package/src/mutations/index.ts
CHANGED
|
@@ -61,3 +61,17 @@ export {
|
|
|
61
61
|
initializeSystemSettings,
|
|
62
62
|
type UpdateSystemSettingsInput,
|
|
63
63
|
} from "./systemSettings";
|
|
64
|
+
|
|
65
|
+
// Reservation mutations
|
|
66
|
+
export {
|
|
67
|
+
createReservation,
|
|
68
|
+
updateReservation,
|
|
69
|
+
deleteReservation,
|
|
70
|
+
softDeleteReservation,
|
|
71
|
+
restoreReservation,
|
|
72
|
+
updateReservationStatus,
|
|
73
|
+
ReservationValidationError,
|
|
74
|
+
type CreateReservationInput,
|
|
75
|
+
type UpdateReservationInput,
|
|
76
|
+
type ReservationStatus,
|
|
77
|
+
} from "./reservations";
|
|
@@ -132,7 +132,7 @@ describe('ProductInstance Mutations', () => {
|
|
|
132
132
|
accountId: 'account-789',
|
|
133
133
|
enabled: true,
|
|
134
134
|
version: '1.0.0',
|
|
135
|
-
|
|
135
|
+
updatedAt: mockTimestamp,
|
|
136
136
|
updatedBy: mockUserId,
|
|
137
137
|
})
|
|
138
138
|
);
|
|
@@ -183,7 +183,7 @@ describe('ProductInstance Mutations', () => {
|
|
|
183
183
|
await createProductInstance(mockClient, validInput);
|
|
184
184
|
|
|
185
185
|
const callArgs = mockCreate.mock.calls[0][0];
|
|
186
|
-
expect(callArgs.
|
|
186
|
+
expect(callArgs.updatedAt).toBe(mockTimestamp);
|
|
187
187
|
});
|
|
188
188
|
|
|
189
189
|
it('should use provided updatedBy if specified', async () => {
|
|
@@ -358,7 +358,7 @@ describe('ProductInstance Mutations', () => {
|
|
|
358
358
|
await updateProductInstance(mockClient, validInput);
|
|
359
359
|
|
|
360
360
|
const callArgs = mockUpdate.mock.calls[0][0];
|
|
361
|
-
expect(callArgs.
|
|
361
|
+
expect(callArgs.updatedAt).toBe(mockTimestamp);
|
|
362
362
|
expect(callArgs.updatedBy).toBe(mockUserId);
|
|
363
363
|
});
|
|
364
364
|
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reservation Mutation Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for reservation CRUD operations including date validation,
|
|
5
|
+
* status transitions, soft delete, and restoration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
createReservation,
|
|
11
|
+
updateReservation,
|
|
12
|
+
softDeleteReservation,
|
|
13
|
+
restoreReservation,
|
|
14
|
+
deleteReservation,
|
|
15
|
+
updateReservationStatus,
|
|
16
|
+
ReservationValidationError,
|
|
17
|
+
type ReservationStatus,
|
|
18
|
+
} from "./reservations";
|
|
19
|
+
|
|
20
|
+
// Mock the systemSettings query functions
|
|
21
|
+
vi.mock("../queries/systemSettings", () => ({
|
|
22
|
+
checkRestoreEligibility: vi.fn((deletedAt: string | null, retentionDays: number) => {
|
|
23
|
+
if (!deletedAt) {
|
|
24
|
+
return { canRestore: false, daysRemaining: 0, daysExpired: 0 };
|
|
25
|
+
}
|
|
26
|
+
const deletedDate = new Date(deletedAt);
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const diffMs = now.getTime() - deletedDate.getTime();
|
|
29
|
+
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
|
30
|
+
|
|
31
|
+
if (diffDays > retentionDays) {
|
|
32
|
+
return {
|
|
33
|
+
canRestore: false,
|
|
34
|
+
daysRemaining: 0,
|
|
35
|
+
daysExpired: Math.floor(diffDays - retentionDays),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
canRestore: true,
|
|
41
|
+
daysRemaining: Math.ceil(retentionDays - diffDays),
|
|
42
|
+
daysExpired: 0,
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
DEFAULT_SOFT_DELETE_RETENTION_DAYS: 30,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
describe("Reservation Mutations", () => {
|
|
49
|
+
describe("createReservation", () => {
|
|
50
|
+
const validInput = {
|
|
51
|
+
brandId: "brand-123",
|
|
52
|
+
visitId: "visit-456",
|
|
53
|
+
confirmationCode: "ABC123",
|
|
54
|
+
checkIn: "2024-06-01",
|
|
55
|
+
checkOut: "2024-06-05",
|
|
56
|
+
status: "confirmed" as const,
|
|
57
|
+
source: "direct",
|
|
58
|
+
room: "101",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
it("should create a reservation with valid dates", async () => {
|
|
62
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
63
|
+
data: { id: "reservation-001", ...validInput },
|
|
64
|
+
errors: null,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const mockClient = {
|
|
68
|
+
models: {
|
|
69
|
+
Reservation: { create: mockCreate },
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = await createReservation(mockClient, validInput);
|
|
74
|
+
|
|
75
|
+
expect(result).toBeTruthy();
|
|
76
|
+
expect(result?.id).toBe("reservation-001");
|
|
77
|
+
expect(mockCreate).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should calculate nights automatically when not provided", async () => {
|
|
81
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
82
|
+
data: { id: "reservation-001" },
|
|
83
|
+
errors: null,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const mockClient = {
|
|
87
|
+
models: {
|
|
88
|
+
Reservation: { create: mockCreate },
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
await createReservation(mockClient, validInput);
|
|
93
|
+
|
|
94
|
+
const callArgs = mockCreate.mock.calls[0][0];
|
|
95
|
+
expect(callArgs.nights).toBe(4); // June 1 to June 5 = 4 nights
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should throw ReservationValidationError for invalid check-in date", async () => {
|
|
99
|
+
const mockClient = {
|
|
100
|
+
models: {
|
|
101
|
+
Reservation: { create: vi.fn() },
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const invalidInput = {
|
|
106
|
+
...validInput,
|
|
107
|
+
checkIn: "invalid-date",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
await expect(createReservation(mockClient, invalidInput)).rejects.toThrow(
|
|
111
|
+
ReservationValidationError
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should throw ReservationValidationError when check-out is before check-in", async () => {
|
|
116
|
+
const mockClient = {
|
|
117
|
+
models: {
|
|
118
|
+
Reservation: { create: vi.fn() },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const invalidInput = {
|
|
123
|
+
...validInput,
|
|
124
|
+
checkIn: "2024-06-05",
|
|
125
|
+
checkOut: "2024-06-01",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
await expect(createReservation(mockClient, invalidInput)).rejects.toThrow(
|
|
129
|
+
"Check-out date must be after check-in date"
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should throw ReservationValidationError for minimum stay violation", async () => {
|
|
134
|
+
const mockClient = {
|
|
135
|
+
models: {
|
|
136
|
+
Reservation: { create: vi.fn() },
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const invalidInput = {
|
|
141
|
+
...validInput,
|
|
142
|
+
checkIn: "2024-06-01T10:00:00",
|
|
143
|
+
checkOut: "2024-06-01T11:00:00", // Only 1 hour
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
await expect(createReservation(mockClient, invalidInput)).rejects.toThrow(
|
|
147
|
+
"Minimum stay is 4 hours"
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should return null when GraphQL returns errors", async () => {
|
|
152
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
153
|
+
data: null,
|
|
154
|
+
errors: [{ message: "Database error" }],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const mockClient = {
|
|
158
|
+
models: {
|
|
159
|
+
Reservation: { create: mockCreate },
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const result = await createReservation(mockClient, validInput);
|
|
164
|
+
|
|
165
|
+
expect(result).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("updateReservation", () => {
|
|
170
|
+
it("should update a reservation without status change", async () => {
|
|
171
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
172
|
+
data: { id: "reservation-001", room: "202" },
|
|
173
|
+
errors: null,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const mockClient = {
|
|
177
|
+
models: {
|
|
178
|
+
Reservation: { update: mockUpdate },
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const result = await updateReservation(mockClient, {
|
|
183
|
+
id: "reservation-001",
|
|
184
|
+
room: "202",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(result).toBeTruthy();
|
|
188
|
+
expect(mockUpdate).toHaveBeenCalledWith({ id: "reservation-001", room: "202" });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should validate status transition when status is being changed", async () => {
|
|
192
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
193
|
+
data: { id: "reservation-001", status: "checked_out" },
|
|
194
|
+
errors: null,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const mockClient = {
|
|
198
|
+
models: {
|
|
199
|
+
Reservation: {
|
|
200
|
+
get: mockGet,
|
|
201
|
+
update: vi.fn(),
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Try to change from checked_out to confirmed (not allowed)
|
|
207
|
+
await expect(
|
|
208
|
+
updateReservation(mockClient, {
|
|
209
|
+
id: "reservation-001",
|
|
210
|
+
status: "confirmed",
|
|
211
|
+
})
|
|
212
|
+
).rejects.toThrow(ReservationValidationError);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should recalculate nights when dates are updated", async () => {
|
|
216
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
217
|
+
data: { id: "reservation-001" },
|
|
218
|
+
errors: null,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const mockClient = {
|
|
222
|
+
models: {
|
|
223
|
+
Reservation: { update: mockUpdate },
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
await updateReservation(mockClient, {
|
|
228
|
+
id: "reservation-001",
|
|
229
|
+
checkIn: "2024-06-01",
|
|
230
|
+
checkOut: "2024-06-10",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const callArgs = mockUpdate.mock.calls[0][0];
|
|
234
|
+
expect(callArgs.nights).toBe(9); // 9 nights
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("updateReservationStatus", () => {
|
|
239
|
+
const statusTransitionTests: Array<{
|
|
240
|
+
from: ReservationStatus;
|
|
241
|
+
to: ReservationStatus;
|
|
242
|
+
allowed: boolean;
|
|
243
|
+
}> = [
|
|
244
|
+
{ from: "confirmed", to: "checked_in", allowed: true },
|
|
245
|
+
{ from: "confirmed", to: "cancelled", allowed: true },
|
|
246
|
+
{ from: "confirmed", to: "no_show", allowed: true },
|
|
247
|
+
{ from: "confirmed", to: "checked_out", allowed: false },
|
|
248
|
+
{ from: "checked_in", to: "checked_out", allowed: true },
|
|
249
|
+
{ from: "checked_in", to: "cancelled", allowed: true },
|
|
250
|
+
{ from: "checked_in", to: "confirmed", allowed: false },
|
|
251
|
+
{ from: "checked_out", to: "confirmed", allowed: false },
|
|
252
|
+
{ from: "checked_out", to: "checked_in", allowed: false },
|
|
253
|
+
{ from: "cancelled", to: "confirmed", allowed: false },
|
|
254
|
+
{ from: "no_show", to: "confirmed", allowed: false },
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
statusTransitionTests.forEach(({ from, to, allowed }) => {
|
|
258
|
+
it(`should ${allowed ? "allow" : "reject"} transition from '${from}' to '${to}'`, async () => {
|
|
259
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
260
|
+
data: { id: "reservation-001", status: from },
|
|
261
|
+
errors: null,
|
|
262
|
+
});
|
|
263
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
264
|
+
data: { id: "reservation-001", status: to },
|
|
265
|
+
errors: null,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const mockClient = {
|
|
269
|
+
models: {
|
|
270
|
+
Reservation: {
|
|
271
|
+
get: mockGet,
|
|
272
|
+
update: mockUpdate,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (allowed) {
|
|
278
|
+
const result = await updateReservationStatus(mockClient, "reservation-001", to);
|
|
279
|
+
expect(result).toBeTruthy();
|
|
280
|
+
expect(mockUpdate).toHaveBeenCalledWith({ id: "reservation-001", status: to });
|
|
281
|
+
} else {
|
|
282
|
+
await expect(
|
|
283
|
+
updateReservationStatus(mockClient, "reservation-001", to)
|
|
284
|
+
).rejects.toThrow(ReservationValidationError);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("should return null when reservation not found", async () => {
|
|
290
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
291
|
+
data: null,
|
|
292
|
+
errors: [{ message: "Not found" }],
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const mockClient = {
|
|
296
|
+
models: {
|
|
297
|
+
Reservation: { get: mockGet, update: vi.fn() },
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const result = await updateReservationStatus(mockClient, "nonexistent", "checked_in");
|
|
302
|
+
|
|
303
|
+
expect(result).toBeNull();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("softDeleteReservation", () => {
|
|
308
|
+
it("should set deletedAt and deletedBy", async () => {
|
|
309
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
310
|
+
data: { id: "reservation-001" },
|
|
311
|
+
errors: null,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const mockClient = {
|
|
315
|
+
models: {
|
|
316
|
+
Reservation: { update: mockUpdate },
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const result = await softDeleteReservation(
|
|
321
|
+
mockClient,
|
|
322
|
+
"reservation-001",
|
|
323
|
+
"admin@example.com"
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(result).toBe(true);
|
|
327
|
+
const callArgs = mockUpdate.mock.calls[0][0];
|
|
328
|
+
expect(callArgs.id).toBe("reservation-001");
|
|
329
|
+
expect(callArgs.deletedAt).toBeDefined();
|
|
330
|
+
expect(callArgs.deletedBy).toBe("admin@example.com");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("should return false on GraphQL error", async () => {
|
|
334
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
335
|
+
data: null,
|
|
336
|
+
errors: [{ message: "Error" }],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const mockClient = {
|
|
340
|
+
models: {
|
|
341
|
+
Reservation: { update: mockUpdate },
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const result = await softDeleteReservation(
|
|
346
|
+
mockClient,
|
|
347
|
+
"reservation-001",
|
|
348
|
+
"admin@example.com"
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
expect(result).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("restoreReservation", () => {
|
|
356
|
+
it("should restore a recently deleted reservation", async () => {
|
|
357
|
+
const recentDeletedAt = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(); // 5 days ago
|
|
358
|
+
|
|
359
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
360
|
+
data: { id: "reservation-001", deletedAt: recentDeletedAt },
|
|
361
|
+
errors: null,
|
|
362
|
+
});
|
|
363
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
364
|
+
data: { id: "reservation-001" },
|
|
365
|
+
errors: null,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const mockClient = {
|
|
369
|
+
models: {
|
|
370
|
+
Reservation: { get: mockGet, update: mockUpdate },
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const result = await restoreReservation(mockClient, "reservation-001");
|
|
375
|
+
|
|
376
|
+
expect(result.success).toBe(true);
|
|
377
|
+
expect(mockUpdate).toHaveBeenCalledWith({
|
|
378
|
+
id: "reservation-001",
|
|
379
|
+
deletedAt: null,
|
|
380
|
+
deletedBy: null,
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("should reject restoration after retention period expires", async () => {
|
|
385
|
+
const oldDeletedAt = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString(); // 60 days ago
|
|
386
|
+
|
|
387
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
388
|
+
data: { id: "reservation-001", deletedAt: oldDeletedAt },
|
|
389
|
+
errors: null,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const mockClient = {
|
|
393
|
+
models: {
|
|
394
|
+
Reservation: { get: mockGet, update: vi.fn() },
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const result = await restoreReservation(mockClient, "reservation-001", 30);
|
|
399
|
+
|
|
400
|
+
expect(result.success).toBe(false);
|
|
401
|
+
expect(result.error).toContain("Retention period");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("should return error when reservation not found", async () => {
|
|
405
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
406
|
+
data: null,
|
|
407
|
+
errors: [{ message: "Not found" }],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const mockClient = {
|
|
411
|
+
models: {
|
|
412
|
+
Reservation: { get: mockGet, update: vi.fn() },
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const result = await restoreReservation(mockClient, "nonexistent");
|
|
417
|
+
|
|
418
|
+
expect(result.success).toBe(false);
|
|
419
|
+
expect(result.error).toBe("Reservation not found");
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe("deleteReservation", () => {
|
|
424
|
+
it("should permanently delete a reservation", async () => {
|
|
425
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
|
426
|
+
data: { id: "reservation-001" },
|
|
427
|
+
errors: null,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const mockClient = {
|
|
431
|
+
models: {
|
|
432
|
+
Reservation: { delete: mockDelete },
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const result = await deleteReservation(mockClient, "reservation-001");
|
|
437
|
+
|
|
438
|
+
expect(result).toBe(true);
|
|
439
|
+
expect(mockDelete).toHaveBeenCalledWith({ id: "reservation-001" });
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("should return false on GraphQL error", async () => {
|
|
443
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
|
444
|
+
data: null,
|
|
445
|
+
errors: [{ message: "Cannot delete" }],
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const mockClient = {
|
|
449
|
+
models: {
|
|
450
|
+
Reservation: { delete: mockDelete },
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const result = await deleteReservation(mockClient, "reservation-001");
|
|
455
|
+
|
|
456
|
+
expect(result).toBe(false);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
});
|