1515 * and whose next occurrence does not exceed end_date.
1616 *
1717 * Clones event data and related pivot tables (themes, tags, audiences).
18- * Stores reference in `source_ref` as "parent:{event_id}" for traceability.
18+ * Stores reference in `source_ref` as "parent:{event_id}:{Y-m-d}" so each
19+ * occurrence is unique — `events.source_ref` has a unique index, and the old
20+ * bare "parent:{event_id}" form could only ever store one child per parent.
1921 *
2022 * Options:
2123 * --dry-run : simulate generation without DB writes.
@@ -48,6 +50,7 @@ public function handle(): int
4850
4951 if ($ events ->isEmpty ()) {
5052 $ this ->info ('No eligible recurring events found. ' );
53+
5154 return self ::SUCCESS ;
5255 }
5356
@@ -64,30 +67,32 @@ public function handle(): int
6467 // Skip if next start exceeds event's end_date
6568 if ($ nextStart ->gt ($ event ->end_date )) {
6669 $ bar ->advance ();
70+
6771 continue ;
6872 }
6973
70- // Avoid duplicates: check for same parent + same next date
71- $ exists = Event::query ()
72- ->where ('source_ref ' , 'parent: ' . $ event ->id )
73- ->whereDate ('start_date ' , $ nextStart ->toDateString ())
74- ->exists ();
74+ if (! $ dryRun ) {
75+ $ this ->rekeyLegacyChild ($ event );
76+ }
7577
76- if ($ exists ) {
78+ $ sourceRef = $ this ->occurrenceSourceRef ($ event ->id , $ nextStart );
79+
80+ if ($ this ->occurrenceExists ($ event ->id , $ nextStart , $ sourceRef )) {
7781 $ bar ->advance ();
82+
7883 continue ;
7984 }
8085
8186 // Clone event data
8287 $ newEventData = $ event ->replicate ([
83- 'id ' , 'created_at ' , 'updated_at '
88+ 'id ' , 'created_at ' , 'updated_at ' ,
8489 ])->toArray ();
8590
8691 $ durationSeconds = Carbon::parse ($ event ->end_date )->diffInSeconds (Carbon::parse ($ event ->start_date ));
8792
8893 $ newEventData ['start_date ' ] = $ nextStart ;
8994 $ newEventData ['end_date ' ] = $ nextStart ->copy ()->addSeconds ($ durationSeconds );
90- $ newEventData ['source_ref ' ] = ' parent: ' . $ event -> id ;
95+ $ newEventData ['source_ref ' ] = $ sourceRef ;
9196 $ newEventData ['status ' ] = $ event ->status ;
9297
9398 if ($ dryRun ) {
@@ -145,4 +150,42 @@ private function getNextDate(Carbon $startDate, string $recurrence): Carbon
145150
146151 return $ next ;
147152 }
153+
154+ private function occurrenceSourceRef (int $ parentId , Carbon $ start ): string
155+ {
156+ return sprintf ('parent:%d:%s ' , $ parentId , $ start ->toDateString ());
157+ }
158+
159+ private function occurrenceExists (int $ parentId , Carbon $ start , string $ sourceRef ): bool
160+ {
161+ return Event::query ()
162+ ->where (function ($ query ) use ($ parentId , $ sourceRef ) {
163+ $ query ->where ('source_ref ' , $ sourceRef )
164+ ->orWhere ('source_ref ' , 'parent: ' .$ parentId )
165+ ->orWhere ('source_ref ' , 'like ' , 'parent: ' .$ parentId .':% ' );
166+ })
167+ ->whereDate ('start_date ' , $ start ->toDateString ())
168+ ->exists ();
169+ }
170+
171+ /**
172+ * Older runs wrote a single "parent:{id}" child. That value is unique, so it
173+ * blocks every later occurrence. Rewrite it to the dated form once.
174+ */
175+ private function rekeyLegacyChild (Event $ event ): void
176+ {
177+ $ legacy = Event::query ()
178+ ->where ('source_ref ' , 'parent: ' .$ event ->id )
179+ ->first ();
180+
181+ if ($ legacy === null ) {
182+ return ;
183+ }
184+
185+ $ legacy ->source_ref = $ this ->occurrenceSourceRef (
186+ $ event ->id ,
187+ Carbon::parse ($ legacy ->start_date )
188+ );
189+ $ legacy ->save ();
190+ }
148191}
0 commit comments