@flusys/nestjs-event-manager 6.0.2 → 6.1.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.
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _modules = require("@flusys/nestjs-shared/modules");
6
+ const _entities = require("../entities");
7
+ const _eventmanagerdatasourceprovider = require("./event-manager-datasource.provider");
8
+ function buildConfigService(overrides = {}) {
9
+ return {
10
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(overrides.isCompanyFeatureEnabled ?? false),
11
+ getDatabaseMode: jest.fn().mockReturnValue('single'),
12
+ isMultiTenant: jest.fn().mockReturnValue(false),
13
+ getDefaultColor: jest.fn(),
14
+ getMaxRecurrenceOccurrences: jest.fn(),
15
+ getOptions: jest.fn().mockReturnValue(overrides.options ?? {
16
+ bootstrapAppConfig: {
17
+ enableCompanyFeature: false,
18
+ databaseMode: 'single'
19
+ },
20
+ config: {
21
+ defaultDatabaseConfig: {
22
+ type: 'mysql',
23
+ host: 'localhost'
24
+ },
25
+ tenantDefaultDatabaseConfig: undefined,
26
+ tenants: []
27
+ }
28
+ })
29
+ };
30
+ }
31
+ describe('EventManagerDataSourceProvider', ()=>{
32
+ afterEach(()=>{
33
+ jest.restoreAllMocks();
34
+ });
35
+ describe('constructor / buildParentOptions', ()=>{
36
+ it('maps module options to the parent IDataSourceServiceOptions shape', ()=>{
37
+ const configService = buildConfigService({
38
+ options: {
39
+ bootstrapAppConfig: {
40
+ enableCompanyFeature: true,
41
+ databaseMode: 'multi-tenant'
42
+ },
43
+ config: {
44
+ defaultDatabaseConfig: {
45
+ type: 'mysql',
46
+ host: 'db-host'
47
+ },
48
+ tenantDefaultDatabaseConfig: {
49
+ type: 'mysql',
50
+ host: 'tenant-host'
51
+ },
52
+ tenants: [
53
+ {
54
+ id: 'tenant-1',
55
+ database: 'tenant_db'
56
+ }
57
+ ]
58
+ }
59
+ }
60
+ });
61
+ const provider = new _eventmanagerdatasourceprovider.EventManagerDataSourceProvider(configService);
62
+ expect(provider.options).toEqual({
63
+ bootstrapAppConfig: {
64
+ enableCompanyFeature: true,
65
+ databaseMode: 'multi-tenant'
66
+ },
67
+ defaultDatabaseConfig: {
68
+ type: 'mysql',
69
+ host: 'db-host'
70
+ },
71
+ tenantDefaultDatabaseConfig: {
72
+ type: 'mysql',
73
+ host: 'tenant-host'
74
+ },
75
+ tenants: [
76
+ {
77
+ id: 'tenant-1',
78
+ database: 'tenant_db'
79
+ }
80
+ ]
81
+ });
82
+ });
83
+ it('builds parent options gracefully when config is not provided', ()=>{
84
+ const configService = buildConfigService({
85
+ options: {
86
+ bootstrapAppConfig: undefined,
87
+ config: undefined
88
+ }
89
+ });
90
+ const provider = new _eventmanagerdatasourceprovider.EventManagerDataSourceProvider(configService);
91
+ expect(provider.options).toEqual({
92
+ bootstrapAppConfig: undefined,
93
+ defaultDatabaseConfig: undefined,
94
+ tenantDefaultDatabaseConfig: undefined,
95
+ tenants: undefined
96
+ });
97
+ });
98
+ });
99
+ describe('getEnableCompanyFeatureForTenant', ()=>{
100
+ it('returns the tenant override when explicitly true', ()=>{
101
+ const configService = buildConfigService({
102
+ isCompanyFeatureEnabled: false
103
+ });
104
+ const provider = new _eventmanagerdatasourceprovider.EventManagerDataSourceProvider(configService);
105
+ const result = provider.getEnableCompanyFeatureForTenant({
106
+ enableCompanyFeature: true
107
+ });
108
+ expect(result).toBe(true);
109
+ });
110
+ it('returns the tenant override when explicitly false', ()=>{
111
+ const configService = buildConfigService({
112
+ isCompanyFeatureEnabled: true
113
+ });
114
+ const provider = new _eventmanagerdatasourceprovider.EventManagerDataSourceProvider(configService);
115
+ const result = provider.getEnableCompanyFeatureForTenant({
116
+ enableCompanyFeature: false
117
+ });
118
+ expect(result).toBe(false);
119
+ });
120
+ it('falls back to the config service when tenant flag is undefined', ()=>{
121
+ const configService = buildConfigService({
122
+ isCompanyFeatureEnabled: true
123
+ });
124
+ const provider = new _eventmanagerdatasourceprovider.EventManagerDataSourceProvider(configService);
125
+ const result = provider.getEnableCompanyFeatureForTenant({});
126
+ expect(result).toBe(true);
127
+ });
128
+ it('falls back to the config service when no tenant is provided', ()=>{
129
+ const configService = buildConfigService({
130
+ isCompanyFeatureEnabled: false
131
+ });
132
+ const provider = new _eventmanagerdatasourceprovider.EventManagerDataSourceProvider(configService);
133
+ const result = provider.getEnableCompanyFeatureForTenant(undefined);
134
+ expect(result).toBe(false);
135
+ });
136
+ });
137
+ describe('createDataSourceFromConfig', ()=>{
138
+ it('loads core event-manager entities when the company feature is disabled', async ()=>{
139
+ const configService = buildConfigService({
140
+ isCompanyFeatureEnabled: false
141
+ });
142
+ const provider = new _eventmanagerdatasourceprovider.EventManagerDataSourceProvider(configService);
143
+ const spy = jest.spyOn(_modules.MultiTenantDataSourceService.prototype, 'createDataSourceFromConfig').mockResolvedValue({
144
+ isInitialized: true
145
+ });
146
+ const config = {
147
+ type: 'mysql',
148
+ host: 'localhost'
149
+ };
150
+ await provider.createDataSourceFromConfig(config);
151
+ expect(spy).toHaveBeenCalledWith(config, expect.arrayContaining([
152
+ _entities.Event,
153
+ _entities.EventParticipant
154
+ ]));
155
+ expect(spy.mock.calls[0][1]).not.toContain(_entities.EventWithCompany);
156
+ });
157
+ it('loads company-scoped event-manager entities when the company feature is enabled', async ()=>{
158
+ const configService = buildConfigService({
159
+ isCompanyFeatureEnabled: true
160
+ });
161
+ const provider = new _eventmanagerdatasourceprovider.EventManagerDataSourceProvider(configService);
162
+ const spy = jest.spyOn(_modules.MultiTenantDataSourceService.prototype, 'createDataSourceFromConfig').mockResolvedValue({
163
+ isInitialized: true
164
+ });
165
+ const config = {
166
+ type: 'mysql',
167
+ host: 'localhost'
168
+ };
169
+ await provider.createDataSourceFromConfig(config);
170
+ expect(spy).toHaveBeenCalledWith(config, expect.arrayContaining([
171
+ _entities.EventWithCompany,
172
+ _entities.EventParticipant
173
+ ]));
174
+ expect(spy.mock.calls[0][1]).not.toContain(_entities.Event);
175
+ });
176
+ it('uses the current tenant override to resolve entities when a request is present', async ()=>{
177
+ const configService = buildConfigService({
178
+ isCompanyFeatureEnabled: false
179
+ });
180
+ const provider = new _eventmanagerdatasourceprovider.EventManagerDataSourceProvider(configService);
181
+ jest.spyOn(provider, 'getCurrentTenant').mockReturnValue({
182
+ enableCompanyFeature: true
183
+ });
184
+ const spy = jest.spyOn(_modules.MultiTenantDataSourceService.prototype, 'createDataSourceFromConfig').mockResolvedValue({
185
+ isInitialized: true
186
+ });
187
+ const config = {
188
+ type: 'mysql',
189
+ host: 'localhost'
190
+ };
191
+ await provider.createDataSourceFromConfig(config);
192
+ expect(spy).toHaveBeenCalledWith(config, expect.arrayContaining([
193
+ _entities.EventWithCompany,
194
+ _entities.EventParticipant
195
+ ]));
196
+ });
197
+ });
198
+ });
@@ -0,0 +1,327 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _common = require("@nestjs/common");
6
+ const _nestjsshared = require("@flusys/nestjs-shared");
7
+ const _repositorymock = require("@test-utils/mocks/repository.mock");
8
+ const _config = require("../config");
9
+ const _entities = require("../entities");
10
+ const _eventmanagerhelperservice = require("./event-manager-helper.service");
11
+ function buildEvent(overrides = {}) {
12
+ return {
13
+ id: 'event-uuid-1',
14
+ title: 'Sprint Planning',
15
+ description: null,
16
+ eventDate: '2026-04-03',
17
+ startTime: '09:00',
18
+ endTime: '10:00',
19
+ isAllDay: false,
20
+ recurrenceType: _nestjsshared.RecurrenceType.NONE,
21
+ recurrenceEndDate: null,
22
+ weekDays: null,
23
+ color: '#3B82F6',
24
+ meetingLink: null,
25
+ isActive: true,
26
+ createdAt: new Date('2026-01-01T00:00:00Z'),
27
+ updatedAt: new Date('2026-01-01T00:00:00Z'),
28
+ deletedAt: null,
29
+ createdById: null,
30
+ updatedById: null,
31
+ deletedById: null,
32
+ ...overrides
33
+ };
34
+ }
35
+ function buildParticipant(overrides = {}) {
36
+ return {
37
+ id: 'participant-uuid-1',
38
+ eventId: 'event-uuid-1',
39
+ userId: 'user-uuid-1',
40
+ status: _nestjsshared.ParticipantStatus.PENDING,
41
+ isOrganizer: false,
42
+ createdAt: new Date('2026-01-01T00:00:00Z'),
43
+ updatedAt: new Date('2026-01-01T00:00:00Z'),
44
+ deletedAt: null,
45
+ createdById: null,
46
+ updatedById: null,
47
+ deletedById: null,
48
+ ...overrides
49
+ };
50
+ }
51
+ describe('EventManagerHelperService', ()=>{
52
+ let service;
53
+ let mockEventRepo;
54
+ let mockParticipantRepo;
55
+ let mockConfigService;
56
+ let mockDataSourceProvider;
57
+ let mockUtilsService;
58
+ // `getRawMany` isn't part of the shared MockQueryBuilder — the participant queries in this
59
+ // service use `.getRawMany()`, so we augment the shared mock query builder instances here.
60
+ let mockEventQb;
61
+ let mockParticipantQb;
62
+ beforeEach(()=>{
63
+ mockEventRepo = (0, _repositorymock.createMockRepository)();
64
+ mockParticipantRepo = (0, _repositorymock.createMockRepository)();
65
+ mockEventQb = mockEventRepo.createQueryBuilder();
66
+ mockEventQb.getRawMany = jest.fn();
67
+ mockParticipantQb = mockParticipantRepo.createQueryBuilder();
68
+ mockParticipantQb.getRawMany = jest.fn();
69
+ mockConfigService = {
70
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(false),
71
+ getDefaultColor: jest.fn().mockReturnValue('#3B82F6')
72
+ };
73
+ const fakeDataSource = {
74
+ getRepository: jest.fn((entity)=>entity === _entities.EventParticipant ? mockParticipantRepo : mockEventRepo)
75
+ };
76
+ mockDataSourceProvider = {
77
+ getDataSource: jest.fn().mockResolvedValue(fakeDataSource)
78
+ };
79
+ mockUtilsService = {
80
+ clearCache: jest.fn()
81
+ };
82
+ service = new _eventmanagerhelperservice.EventManagerHelperService({}, mockUtilsService, mockConfigService, mockDataSourceProvider);
83
+ });
84
+ describe('createEvent', ()=>{
85
+ it('creates a timed event with the given fields and clears the cache', async ()=>{
86
+ const saved = buildEvent();
87
+ mockEventRepo.save.mockResolvedValue(saved);
88
+ mockParticipantRepo.save.mockResolvedValue([]);
89
+ const result = await service.createEvent({
90
+ title: 'Sprint Planning',
91
+ eventDate: '2026-04-03',
92
+ startTime: '09:00',
93
+ endTime: '10:00'
94
+ });
95
+ expect(mockEventRepo.save).toHaveBeenCalledWith(expect.objectContaining({
96
+ title: 'Sprint Planning',
97
+ startTime: '09:00',
98
+ endTime: '10:00',
99
+ isAllDay: false,
100
+ recurrenceType: _nestjsshared.RecurrenceType.NONE,
101
+ color: '#3B82F6'
102
+ }));
103
+ expect(mockUtilsService.clearCache).toHaveBeenCalledWith('event', {});
104
+ expect(result.id).toBe(saved.id);
105
+ expect(result.companyId).toBeNull();
106
+ });
107
+ it('forces symbolic 00:00 / 23:59 markers for all-day events regardless of supplied times', async ()=>{
108
+ const saved = buildEvent({
109
+ isAllDay: true,
110
+ startTime: '00:00',
111
+ endTime: '23:59'
112
+ });
113
+ mockEventRepo.save.mockResolvedValue(saved);
114
+ await service.createEvent({
115
+ title: 'Company Holiday',
116
+ eventDate: '2026-12-25',
117
+ startTime: '09:00',
118
+ endTime: '17:00',
119
+ isAllDay: true
120
+ });
121
+ expect(mockEventRepo.save).toHaveBeenCalledWith(expect.objectContaining({
122
+ isAllDay: true,
123
+ startTime: '00:00',
124
+ endTime: '23:59'
125
+ }));
126
+ });
127
+ it('attaches companyId only when the company feature is enabled and a companyId is supplied', async ()=>{
128
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
129
+ const saved = buildEvent();
130
+ mockEventRepo.save.mockResolvedValue(saved);
131
+ await service.createEvent({
132
+ title: 'Sprint Planning',
133
+ eventDate: '2026-04-03',
134
+ startTime: '09:00',
135
+ endTime: '10:00',
136
+ companyId: 'company-uuid-1'
137
+ });
138
+ expect(mockEventRepo.save).toHaveBeenCalledWith(expect.objectContaining({
139
+ companyId: 'company-uuid-1'
140
+ }));
141
+ });
142
+ it('does not attach companyId when the company feature is disabled, even if supplied', async ()=>{
143
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
144
+ const saved = buildEvent();
145
+ mockEventRepo.save.mockResolvedValue(saved);
146
+ await service.createEvent({
147
+ title: 'Sprint Planning',
148
+ eventDate: '2026-04-03',
149
+ startTime: '09:00',
150
+ endTime: '10:00',
151
+ companyId: 'company-uuid-1'
152
+ });
153
+ const savedArg = mockEventRepo.save.mock.calls[0][0];
154
+ expect(savedArg).not.toHaveProperty('companyId');
155
+ });
156
+ it('adds the organizer and all participants with PENDING status, de-duplicated', async ()=>{
157
+ const saved = buildEvent();
158
+ mockEventRepo.save.mockResolvedValue(saved);
159
+ mockParticipantRepo.save.mockResolvedValue([]);
160
+ await service.createEvent({
161
+ title: 'Sprint Planning',
162
+ eventDate: '2026-04-03',
163
+ startTime: '09:00',
164
+ endTime: '10:00',
165
+ organizerId: 'organizer-uuid-1',
166
+ participantIds: [
167
+ 'organizer-uuid-1',
168
+ 'participant-uuid-2'
169
+ ]
170
+ });
171
+ const savedParticipants = mockParticipantRepo.save.mock.calls[0][0];
172
+ expect(savedParticipants).toHaveLength(2);
173
+ expect(savedParticipants).toEqual(expect.arrayContaining([
174
+ expect.objectContaining({
175
+ userId: 'organizer-uuid-1',
176
+ isOrganizer: true,
177
+ status: _nestjsshared.ParticipantStatus.PENDING
178
+ }),
179
+ expect.objectContaining({
180
+ userId: 'participant-uuid-2',
181
+ isOrganizer: false,
182
+ status: _nestjsshared.ParticipantStatus.PENDING
183
+ })
184
+ ]));
185
+ });
186
+ it('skips participant creation entirely when no organizer or participants are given', async ()=>{
187
+ const saved = buildEvent();
188
+ mockEventRepo.save.mockResolvedValue(saved);
189
+ await service.createEvent({
190
+ title: 'Sprint Planning',
191
+ eventDate: '2026-04-03',
192
+ startTime: '09:00',
193
+ endTime: '10:00'
194
+ });
195
+ expect(mockParticipantRepo.save).not.toHaveBeenCalled();
196
+ });
197
+ });
198
+ describe('updateParticipantStatus', ()=>{
199
+ it('updates the status of an existing participant', async ()=>{
200
+ const participant = buildParticipant({
201
+ status: _nestjsshared.ParticipantStatus.PENDING
202
+ });
203
+ mockParticipantRepo.findOne.mockResolvedValue(participant);
204
+ mockParticipantRepo.save.mockResolvedValue({
205
+ ...participant,
206
+ status: _nestjsshared.ParticipantStatus.ACCEPTED
207
+ });
208
+ await service.updateParticipantStatus('participant-uuid-1', _nestjsshared.ParticipantStatus.ACCEPTED);
209
+ expect(mockParticipantRepo.save).toHaveBeenCalledWith(expect.objectContaining({
210
+ status: _nestjsshared.ParticipantStatus.ACCEPTED
211
+ }));
212
+ });
213
+ it('throws NotFoundException with the correct messageKey when the participant does not exist', async ()=>{
214
+ mockParticipantRepo.findOne.mockResolvedValue(null);
215
+ await expect(service.updateParticipantStatus('missing-uuid', _nestjsshared.ParticipantStatus.ACCEPTED)).rejects.toMatchObject({
216
+ response: expect.objectContaining({
217
+ messageKey: _config.EVENT_PARTICIPANT_MESSAGES.NOT_FOUND
218
+ })
219
+ });
220
+ await expect(service.updateParticipantStatus('missing-uuid', _nestjsshared.ParticipantStatus.ACCEPTED)).rejects.toBeInstanceOf(_common.NotFoundException);
221
+ });
222
+ });
223
+ describe('getEventsForUser', ()=>{
224
+ it('returns an empty list without querying events when the user has no participant rows', async ()=>{
225
+ mockParticipantQb.getRawMany.mockResolvedValue([]);
226
+ const result = await service.getEventsForUser('user-uuid-1', '2026-04-01', '2026-04-30');
227
+ expect(result).toEqual([]);
228
+ expect(mockEventQb.getMany).not.toHaveBeenCalled();
229
+ expect(mockEventQb.where).not.toHaveBeenCalled();
230
+ });
231
+ it('queries events scoped to the participant event ids and date range', async ()=>{
232
+ mockParticipantQb.getRawMany.mockResolvedValue([
233
+ {
234
+ ep_event_id: 'event-uuid-1'
235
+ }
236
+ ]);
237
+ const events = [
238
+ buildEvent()
239
+ ];
240
+ mockEventQb.getMany.mockResolvedValue(events);
241
+ const result = await service.getEventsForUser('user-uuid-1', '2026-04-01', '2026-04-30');
242
+ const eventQb = mockEventQb;
243
+ expect(eventQb.where).toHaveBeenCalledWith('event.id IN (:...eventIds)', {
244
+ eventIds: [
245
+ 'event-uuid-1'
246
+ ]
247
+ });
248
+ expect(eventQb.andWhere).toHaveBeenCalledWith('event.eventDate <= :endDate', {
249
+ endDate: '2026-04-30'
250
+ });
251
+ expect(eventQb.andWhere).toHaveBeenCalledWith('event.eventDate >= :startDate', {
252
+ startDate: '2026-04-01'
253
+ });
254
+ expect(result).toHaveLength(1);
255
+ expect(result[0].id).toBe('event-uuid-1');
256
+ });
257
+ it('handles a zero-length range (startDate === endDate)', async ()=>{
258
+ mockParticipantQb.getRawMany.mockResolvedValue([
259
+ {
260
+ ep_event_id: 'event-uuid-1'
261
+ }
262
+ ]);
263
+ mockEventQb.getMany.mockResolvedValue([
264
+ buildEvent()
265
+ ]);
266
+ const result = await service.getEventsForUser('user-uuid-1', '2026-04-03', '2026-04-03');
267
+ const eventQb = mockEventQb;
268
+ expect(eventQb.andWhere).toHaveBeenCalledWith('event.eventDate <= :endDate', {
269
+ endDate: '2026-04-03'
270
+ });
271
+ expect(eventQb.andWhere).toHaveBeenCalledWith('event.eventDate >= :startDate', {
272
+ startDate: '2026-04-03'
273
+ });
274
+ expect(result).toHaveLength(1);
275
+ });
276
+ it('scopes the query to the company when the company feature is enabled and a companyId is given', async ()=>{
277
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
278
+ mockParticipantQb.getRawMany.mockResolvedValue([
279
+ {
280
+ ep_event_id: 'event-uuid-1'
281
+ }
282
+ ]);
283
+ mockEventQb.getMany.mockResolvedValue([
284
+ buildEvent()
285
+ ]);
286
+ await service.getEventsForUser('user-uuid-1', '2026-04-01', '2026-04-30', 'company-uuid-1');
287
+ const eventQb = mockEventQb;
288
+ expect(eventQb.andWhere).toHaveBeenCalledWith('event.companyId = :companyId', {
289
+ companyId: 'company-uuid-1'
290
+ });
291
+ });
292
+ it('does not scope the query to the company when the company feature is disabled', async ()=>{
293
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
294
+ mockParticipantQb.getRawMany.mockResolvedValue([
295
+ {
296
+ ep_event_id: 'event-uuid-1'
297
+ }
298
+ ]);
299
+ mockEventQb.getMany.mockResolvedValue([
300
+ buildEvent()
301
+ ]);
302
+ await service.getEventsForUser('user-uuid-1', '2026-04-01', '2026-04-30', 'company-uuid-1');
303
+ const eventQb = mockEventQb;
304
+ const companyCalls = eventQb.andWhere.mock.calls.filter((call)=>call[0] === 'event.companyId = :companyId');
305
+ expect(companyCalls).toHaveLength(0);
306
+ });
307
+ it('requests the correct entity for the company feature toggle', async ()=>{
308
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
309
+ mockParticipantQb.getRawMany.mockResolvedValue([]);
310
+ await service.getEventsForUser('user-uuid-1', '2026-04-01', '2026-04-30');
311
+ const fakeDataSource = await mockDataSourceProvider.getDataSource();
312
+ expect(fakeDataSource.getRepository).toHaveBeenCalledWith(_entities.EventWithCompany);
313
+ });
314
+ });
315
+ describe('getEventById', ()=>{
316
+ it('returns the mapped event when found', async ()=>{
317
+ mockEventRepo.findOne.mockResolvedValue(buildEvent());
318
+ const result = await service.getEventById('event-uuid-1');
319
+ expect(result?.id).toBe('event-uuid-1');
320
+ });
321
+ it('returns null when the event does not exist', async ()=>{
322
+ mockEventRepo.findOne.mockResolvedValue(null);
323
+ const result = await service.getEventById('missing-uuid');
324
+ expect(result).toBeNull();
325
+ });
326
+ });
327
+ });
@@ -365,11 +365,16 @@ let EventService = class EventService extends _classes.ApiService {
365
365
  break;
366
366
  case _enums.RecurrenceType.MONTHLY:
367
367
  {
368
+ // Compute the target year/month from the *original* date before any mutation.
369
+ // Mutating `d` first (e.g. via setUTCMonth) can overflow past short months
370
+ // (Jan 31 -> Feb 31 normalizes to Mar 3), which would then compute
371
+ // `lastDayOfMonth` for the wrong (already-overflowed) month and silently
372
+ // skip target months that have fewer days than the original day-of-month.
368
373
  const targetDay = originalDayOfMonth ?? d.getUTCDate();
369
- d.setUTCMonth(d.getUTCMonth() + 1);
370
- const lastDayOfMonth = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)).getUTCDate();
371
- d.setUTCDate(Math.min(targetDay, lastDayOfMonth));
372
- break;
374
+ const targetYear = d.getUTCFullYear();
375
+ const targetMonth = d.getUTCMonth() + 1;
376
+ const lastDayOfMonth = new Date(Date.UTC(targetYear, targetMonth + 1, 0)).getUTCDate();
377
+ return new Date(Date.UTC(targetYear, targetMonth, Math.min(targetDay, lastDayOfMonth))).toISOString().slice(0, 10);
373
378
  }
374
379
  default:
375
380
  break;