Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
-- Follow-up to 20260823000000_add_publishing_campaigns: that backfill collapsed
-- every congregation's legacy `type = 'campaign'` attributions into ONE synthetic
-- ended campaign named 'Campagne'. Real history usually spans several distinct
-- drives, so split each synthetic campaign into one campaign per date cluster:
-- a gap of more than 30 days between an attribution's start and the latest end
-- seen so far opens a new cluster. Synthetic campaigns are recognized by the
-- exact stamps the backfill wrote (name 'Campagne', activatedAt = startDate,
-- endedAt = endDate) — a campaign that went through the real lifecycle never
-- has activation/end timestamps exactly equal to its day-granular bounds.
-- Single-cluster synthetics are left untouched (splitting would be a rename).
-- One data-modifying CTE chain: temp tables don't survive Prisma's statement
-- execution.

WITH synth AS (
SELECT id, "congregationId"
FROM "Campaign"
WHERE name = 'Campagne'
AND "activatedAt" = "startDate"
AND "endedAt" = "endDate"
),
attrs AS (
SELECT a.id,
a."congregationId",
a."campaignId",
a."startDate",
COALESCE(a."endDate", a."startDate") AS eff_end
FROM "Attribution" a
JOIN synth s ON s.id = a."campaignId"
),
marked AS (
SELECT attrs.*,
CASE
WHEN "startDate" > COALESCE(
MAX(eff_end) OVER (
PARTITION BY "campaignId"
ORDER BY "startDate", id
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
),
"startDate"
) + INTERVAL '30 days'
THEN 1 ELSE 0
END AS opens_cluster
FROM attrs
),
clustered AS (
SELECT marked.*,
SUM(opens_cluster) OVER (PARTITION BY "campaignId" ORDER BY "startDate", id) AS cluster
FROM marked
),
-- Only campaigns that actually split into several clusters are rewritten.
bounds AS (
SELECT "campaignId",
"congregationId",
cluster,
MIN("startDate") AS cluster_start,
MAX(eff_end) AS cluster_end
FROM clustered
WHERE "campaignId" IN (
SELECT "campaignId" FROM clustered GROUP BY "campaignId" HAVING COUNT(DISTINCT cluster) > 1
)
GROUP BY "campaignId", "congregationId", cluster
),
-- One already-ended campaign per cluster, named by its period. Cluster bounds
-- within one congregation are disjoint (30-day gaps) and there is only one
-- synthetic campaign per congregation, so (congregationId, startDate, endDate)
-- uniquely identifies the created row for the repointing below.
created AS (
INSERT INTO "Campaign" ("name", "startDate", "endDate", "activatedAt", "endedAt", "congregationId", "updatedAt")
SELECT 'Campagne ' || to_char(b.cluster_start, 'MM/YYYY'),
b.cluster_start,
b.cluster_end,
b.cluster_start,
b.cluster_end,
b."congregationId",
CURRENT_TIMESTAMP
FROM bounds b
RETURNING id, "congregationId", "startDate", "endDate"
),
repointed AS (
UPDATE "Attribution" a
SET "campaignId" = c.id
FROM clustered lc
JOIN bounds b
ON b."campaignId" = lc."campaignId" AND b.cluster = lc.cluster
JOIN created c
ON c."congregationId" = b."congregationId"
AND c."startDate" = b.cluster_start
AND c."endDate" = b.cluster_end
WHERE a.id = lc.id
RETURNING lc."campaignId" AS old_campaign_id
)
-- The split synthetics are now empty (every attribution was repointed; the
-- backfill never paused anything) — drop them.
DELETE FROM "Campaign"
WHERE id IN (SELECT DISTINCT old_campaign_id FROM repointed);
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
-- Eligibility (which roles may fill a slot) moves off the part kind and back
-- onto the parts themselves. Trying the preset feature showed the kind is the
-- wrong granularity: two parts of the same kind — two "Sujet VCM" — can
-- legitimately belong to different populations depending on where they sit in
-- the programme. The kind keeps capability only (reader slot, labels,
-- external-speaker rule, share message).
--
-- Step 1 makes the change neutral *for eligibility*: under the old rule a kind
-- with roles configured for a slot decided that slot and the part's own rows
-- lay dormant. Materialize that effective answer into the part rows before the
-- preset rows disappear, so no existing part's eligibility widens or narrows.
-- Step 2 is not neutral in one respect — see the allowExternalSpeaker note
-- there.

DELETE FROM "TemplatePartAllowedRole" tpar
USING "TemplatePart" tp
WHERE tpar."partId" = tp."id"
AND EXISTS (
SELECT 1 FROM "PartPresetAllowedRole" ppar
WHERE ppar."presetId" = tp."presetId" AND ppar."asKind" = tpar."asKind"
);

INSERT INTO "TemplatePartAllowedRole" ("partId", "roleId", "asKind", "congregationId")
SELECT tp."id", ppar."roleId", ppar."asKind", tp."congregationId"
FROM "TemplatePart" tp
JOIN "PartPresetAllowedRole" ppar ON ppar."presetId" = tp."presetId"
ON CONFLICT DO NOTHING;

DELETE FROM "EventPartAllowedRole" epar
USING "EventPart" ep
WHERE epar."eventPartId" = ep."id"
AND EXISTS (
SELECT 1 FROM "PartPresetAllowedRole" ppar
WHERE ppar."presetId" = ep."presetId" AND ppar."asKind" = epar."asKind"
);

INSERT INTO "EventPartAllowedRole" ("eventPartId", "roleId", "asKind", "congregationId")
SELECT ep."id", ppar."roleId", ppar."asKind", ep."congregationId"
FROM "EventPart" ep
JOIN "PartPresetAllowedRole" ppar ON ppar."presetId" = ep."presetId"
ON CONFLICT DO NOTHING;

DROP TABLE "PartPresetAllowedRole";

-- Step 2: with eligibility gone, nothing distinguishes the three seeded
-- midweek talk kinds (Joyaux, Perles, Vie chrétienne) any more — merge them
-- into one "Sujet VCM" kind. Custom wording stored on the old rows (a rename
-- or an edited share message) is dropped; the merged kind starts on the
-- catalogue defaults.
--
-- One thing does change for existing parts, and the neutrality claim above
-- does not cover it: the three kinds disagreed on allowExternalSpeaker
-- (Joyaux and Perles said no, Vie chrétienne said yes), and one merged kind
-- can only hold one answer. It keeps yes, so parts that were Joyaux or Perles
-- start offering the external-speaker option.
--
-- Widening deliberately rather than narrowing: the flag only decides whether
-- the picker offers an external speaker, and nothing enforces it at assignment
-- time. Answering no would have withdrawn the option from parts that were Vie
-- chrétienne and hidden anyone already assigned that way.

INSERT INTO "PartPreset" ("key", "scope", "hasReaderSlot", "allowExternalSpeaker", "isSystem", "congregationId", "updatedAt")
SELECT 'midweek-talk', 'part', false, true, true, c."id", CURRENT_TIMESTAMP
FROM "Congregation" c
ON CONFLICT ("key", "congregationId") DO NOTHING;

UPDATE "TemplatePart" tp
SET "presetId" = np."id"
FROM "PartPreset" op, "PartPreset" np
WHERE tp."presetId" = op."id"
AND op."isSystem" = true
AND op."key" IN ('spiritual-gems', 'spiritual-pearls', 'christian-life-talk')
AND np."key" = 'midweek-talk'
AND np."congregationId" = op."congregationId";

UPDATE "EventPart" ep
SET "presetId" = np."id"
FROM "PartPreset" op, "PartPreset" np
WHERE ep."presetId" = op."id"
AND op."isSystem" = true
AND op."key" IN ('spiritual-gems', 'spiritual-pearls', 'christian-life-talk')
AND np."key" = 'midweek-talk'
AND np."congregationId" = op."congregationId";

DELETE FROM "PartPreset"
WHERE "isSystem" = true
AND "key" IN ('spiritual-gems', 'spiritual-pearls', 'christian-life-talk');
Loading