From 1716e090e015b684b86e3d6b8cfa6118cbad1423 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 22:12:34 +0200 Subject: [PATCH] =?UTF-8?q?feat(timetable):=20AdvanceSectionObserver=20?= =?UTF-8?q?=E2=80=94=20keep=20advancing=5F*=5Fcount=20in=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes ART-OBSERVER-ADVANCE-AGGREGATE. Recomputes artist_engagements.advancing_completed_count + advancing_total_count on every section lifecycle event (created / updated-status-only / deleted). Atomic via DB::transaction + lockForUpdate on both the parent engagement and the sibling section rows; concurrent section- status changes serialise correctly. Counter updates use disableLogging() — counter sync is housekeeping, not audit. The section's own updated event continues to log via LogsActivity on AdvanceSection. Co-Authored-By: Claude Opus 4.7 (1M context) --- api/app/Observers/AdvanceSectionObserver.php | 96 ++++++++++++++++++++ api/app/Providers/AppServiceProvider.php | 3 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 api/app/Observers/AdvanceSectionObserver.php diff --git a/api/app/Observers/AdvanceSectionObserver.php b/api/app/Observers/AdvanceSectionObserver.php new file mode 100644 index 00000000..9ef16028 --- /dev/null +++ b/api/app/Observers/AdvanceSectionObserver.php @@ -0,0 +1,96 @@ +recompute($section); + } + + public function updated(AdvanceSection $section): void + { + if (! $section->wasChanged('submission_status')) { + return; + } + + $this->recompute($section); + } + + public function deleted(AdvanceSection $section): void + { + $this->recompute($section); + } + + private function recompute(AdvanceSection $section): void + { + $engagementId = $section->engagement_id; + + DB::transaction(function () use ($engagementId): void { + $engagement = ArtistEngagement::query() + ->withoutGlobalScope(OrganisationScope::class) + ->whereKey($engagementId) + ->lockForUpdate() + ->first(); + + if ($engagement === null) { + return; + } + + $rows = AdvanceSection::query() + ->withoutGlobalScope(OrganisationScope::class) + ->where('engagement_id', $engagementId) + ->lockForUpdate() + ->get(['submission_status']); + + $total = $rows->count(); + $completed = $rows + ->where('submission_status', AdvanceSectionSubmissionStatus::Approved) + ->count(); + + $engagement->disableLogging(); + $engagement->update([ + 'advancing_completed_count' => $completed, + 'advancing_total_count' => $total, + ]); + }); + } +} diff --git a/api/app/Providers/AppServiceProvider.php b/api/app/Providers/AppServiceProvider.php index 04217e5e..5389abdd 100644 --- a/api/app/Providers/AppServiceProvider.php +++ b/api/app/Providers/AppServiceProvider.php @@ -164,9 +164,10 @@ class AppServiceProvider extends ServiceProvider FormFieldLibrary::observe(FormFieldChildTablesCascadeObserver::class); // RFC-TIMETABLE v0.2 — engagement denorm + cross-tenant guard, - // performance optimistic-lock bump. + // performance optimistic-lock bump, advance-section counter sync. \App\Models\ArtistEngagement::observe(\App\Observers\ArtistEngagementObserver::class); \App\Models\Performance::observe(\App\Observers\PerformanceObserver::class); + \App\Models\AdvanceSection::observe(\App\Observers\AdvanceSectionObserver::class); // RFC-WS-6 v1.3 §Q1 — FormSubmissionSubmitted listener layout. //