Setting the file. One moment.
Notification Mute · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page This file
Number 45.112
Position 112 of 182
Type JavaScript
Size 10 KB
Lines 178 lib/ notification-mute.js
JavaScript · 178 lines · 10 KB
9
const
{
getDecisionValue
}
=
require
(
'./orchestration-decisions.js'
);
10
11 const DECISION_KEY = 'notificationMute' ;
12 const ENV_KEY = 'WIX_MUTE_NOTIFICATIONS' ;
13 const MUTE_REQUIREMENT_ID = 'mute-site-notifications' ;
14 const ANSWERS = Object. freeze ([ 'on' , 'off' ]);
15
16 const NOTIFICATION_MUTE_QUESTION = Object. freeze ({
17 decisionKey: DECISION_KEY ,
18 question: 'This is an existing, live Wix site. Should we mute its notifications before we start writing to it?' ,
19 choices: Object. freeze ([
20 Object. freeze ({ id: 'on' , label: 'Mute' , description: 'Wix stops all site notifications, including emails to your customers, until someone unmutes. Your live business may miss real customer notifications during the import.' }),
21 Object. freeze ({ id: 'off' , label: "Don't mute" , description: 'Your customers and you may get emails for the records we import (orders, bookings, form submissions, RSVPs, and so on).' }),
22 ]),
23 });
24
25 function normalizeAnswer ( value ) {
26 if (value == null ) return null ;
27 const text = String (value). trim (). toLowerCase ();
28 return text === '' ? null : text;
29 }
30
31 // strategy: 'new_site' | 'existing_site' | null. decision/envValue: raw recorded values.
32 function resolveNotificationMute ({ strategy , decision = null , envValue = null , writesStarted = false } = {}) {
33 const answer = normalizeAnswer (decision);
34 const env = normalizeAnswer (envValue);
35 const errors = [];
36 if (answer !== null && ! ANSWERS . includes (answer)) errors. push ({ code: 'invalid_notification_mute' , field: DECISION_KEY , value: decision });
37 if (env !== null && ! ANSWERS . includes (env)) errors. push ({ code: 'invalid_notification_mute' , field: ENV_KEY , value: envValue });
38 if (errors. length ) return { strategy, answer, inEffect: null , source: null , needsAnswer: false , errors };
39 if (strategy === 'new_site' ) {
40 if (answer === 'off' || env === 'off' ) return { strategy, answer, inEffect: null , source: null , needsAnswer: false , errors: [{ code: 'new_site_mute_off' }] };
41 return { strategy, answer, inEffect: true , source: 'new_site' , needsAnswer: false , errors };
42 }
43 if (strategy !== 'existing_site' ) return { strategy, answer, inEffect: null , source: null , needsAnswer: false , errors };
44 if (answer !== null ) {
45 if (env !== null && env !== answer) return { strategy, answer, inEffect: null , source: null , needsAnswer: false , errors: [{ code: 'mute_config_conflict' , decision: answer, env }] };
46 return { strategy, answer, inEffect: answer === 'on' , source: 'operator' , needsAnswer: false , errors };
47 }
48 // A run that already wrote keeps what it ran with: never a mid-import question.
49 if (writesStarted) return { strategy, answer, inEffect: env === 'on' , source: 'legacy_config' , needsAnswer: false , errors };
50 return { strategy, answer, inEffect: null , source: null , needsAnswer: true , errors };
51 }
52
53 async function maybeReadJson ( file ) {
54 try { return JSON . parse ( await fs. readFile (file, 'utf8' )); } catch (error) { if (error.code === 'ENOENT' ) return null ; throw error; }
55 }
56
57 async function readNotificationMute ( projectDir , { decisions } = {}) {
58 const recorded = decisions || ( await maybeReadJson (path. join (projectDir, 'orchestration' , 'decisions.json' ))) || {};
59 let env = {};
60 try { env = await readEnvFile (path. join (projectDir, 'config' , 'wix.env' )); } catch (error) { if (error.code !== 'ENOENT' ) throw error; }
61 let strategy = getDecisionValue (recorded, 'targetSiteStrategy' );
62 if ( ! strategy) strategy = { new: 'new_site' , existing: 'existing_site' }[ String (env. WIX_SITE_STRATEGY || '' ). trim ()] || null ;
63 let writesStarted = true ;
64 try { await fs. access (path. join (projectDir, 'setup' , 'setup-verification.json' )); } catch { writesStarted = false ; }
65 return resolveNotificationMute ({ strategy, decision: getDecisionValue (recorded, DECISION_KEY ), envValue: env[ ENV_KEY ], writesStarted });
66 }
67
68 // The setup plan must mute exactly when mute is in effect.
69 function setupPlanMuteMismatch ( resolved , plan ) {
70 if ( ! resolved || resolved.inEffect === null || resolved.errors. length ) return null ;
71 const steps = Array. isArray (plan?.steps) ? plan.steps : [];
72 const hasMute = steps. some (( step ) => step && step.id === MUTE_REQUIREMENT_ID );
73 if (resolved.inEffect && ! hasMute) return { code: 'setup_plan_mute_missing' };
74 if ( ! resolved.inEffect && hasMute) return { code: 'setup_plan_mute_unwanted' };
75 return null ;
76 }
77
78 function mutedRecorded ( verification ) {
79 const rows = Array. isArray (verification?.requirements) ? verification.requirements : [];
80 return rows. some (( row ) => row && row.id === MUTE_REQUIREMENT_ID && row.status === 'passed' );
81 }
82
83 const UNMUTE_PROMPT = 'Open the migration project. Call unmuteSiteNotifications from rp-target-wix with the project \' s CLI site token (WIX_AUTH_TOKEN), then confirm getSiteMuteState returns muted: false. Tell the owner notifications are back on.' ;
84
85 // The completion report's mute block and its cutover reminder.
86 async function completionReportMute ( projectDir ) {
87 const resolved = await readNotificationMute (projectDir);
88 const verification = await maybeReadJson (path. join (projectDir, 'setup' , 'setup-verification.json' ));
89 const muted = mutedRecorded (verification);
90 const block = { strategy: resolved.strategy, answer: resolved.answer, inEffect: resolved.inEffect, source: resolved.source, mutedRecorded: muted };
91 if ( ! muted) return { block, actions: [] };
92 const existing = resolved.strategy === 'existing_site' ;
93 return {
94 block,
95 actions: [{
96 actionId: 'notifications-muted' ,
97 code: 'notifications-muted' ,
98 severity: 'high' ,
99 role: 'import-operator' ,
100 action: 'Requires Import Operator: all site notifications are muted, including emails to customers and alerts to the owner. They stay muted until someone unmutes them. Unmute the site.' ,
101 deadlineReason: existing
102 ? 'Unmute as soon as the import is finished and checked; this live site is missing real customer notifications while muted.'
103 : 'Unmute at go-live, before or right after the DNS switch; customers get no site emails while muted.' ,
104 affectedCount: null ,
105 importOperatorPrompt: UNMUTE_PROMPT ,
106 artifactRefs: [ 'setup/setup-verification.json' ],
107 resolutionEvidence: 'A getSiteMuteState read shows muted: false.' ,
108 }],
109 };
110 }
111
112 // The one writer of the operator's answer (the decide command and the durable request both
113 // call it): user source only, never off on a new site, never changed once setup wrote to the
114 // site, and mirrored to WIX_MUTE_NOTIFICATIONS so generated code reads the same value.
115 async function recordNotificationMute ( projectDir , value , { source = 'user' , rationale = null } = {}) {
116 const answer = normalizeAnswer (value);
117 if ( ! ANSWERS . includes (answer)) throw new Error ( `notificationMute must be on or off (got: ${ value })` );
118 if (source !== 'user' ) throw new Error ( 'notificationMute is the operator \' s answer; record it with source user (never inferred)' );
119 const { loadArtifacts , updateDecisionObject , writeJsonAtomic } = require ( './orchestration-state.js' );
120 const artifacts = await loadArtifacts (projectDir);
121 if ( getDecisionValue (artifacts.decisions, 'targetSiteStrategy' ) === 'new_site' && answer === 'off' ) {
122 throw new Error ( 'a new site is always muted before the first write; notificationMute=off is not allowed for targetSiteStrategy=new_site' );
123 }
124 const previous = normalizeAnswer ( getDecisionValue (artifacts.decisions, DECISION_KEY ));
125 if (previous && previous !== answer && ( await maybeReadJson (path. join (projectDir, 'setup' , 'setup-verification.json' )))) {
126 throw new Error ( `notificationMute was already answered ${ previous } and setup has written to the site; it cannot change mid-import` );
127 }
128 updateDecisionObject (artifacts.decisions, DECISION_KEY , answer, { source: 'user' , rationale });
129 await writeJsonAtomic (path. join (projectDir, 'orchestration' , 'decisions.json' ), artifacts.decisions);
130 await require ( './config-env.js' ). upsertEnvFile (path. join (projectDir, 'config' , 'wix.env' ), { [ ENV_KEY ]: answer });
131 return answer;
132 }
133
134 // The durable request a supervised run raises for an unanswered existing site.
135 function notificationMuteRequestInput () {
136 return {
137 kind: 'question' ,
138 stage: 'intake' ,
139 title: 'Mute notifications on the existing site?' ,
140 prompt: NOTIFICATION_MUTE_QUESTION .question,
141 response: { mode: 'choice' , required: true },
142 choices: NOTIFICATION_MUTE_QUESTION .choices. map (( choice ) => ({ ... choice })),
143 effect: { kind: 'decision' , key: DECISION_KEY , choices: { on: 'on' , off: 'off' } },
144 };
145 }
146
147 function isValidNotificationMuteEffect ( effect , choices ) {
148 if ( ! effect || effect.kind !== 'decision' || effect.key !== DECISION_KEY ) return false ;
149 const entries = Object. entries (effect.choices || {});
150 return entries. length === ANSWERS . length && entries. every (([ choice , value ]) => choice === value && ANSWERS . includes (choice) && choices. some (( item ) => item.id === choice));
151 }
152
153 // Reuses the pending request, so a resumed run never asks twice.
154 async function ensureNotificationMuteRequest ( projectDir ) {
155 const actions = require ( './user-actions.js' );
156 for ( const row of await actions. listUserActions (projectDir, { status: 'pending' })) {
157 const request = await actions. readUserAction (projectDir, row.requestId);
158 if (request.effect && request.effect.key === DECISION_KEY ) return request;
159 }
160 return actions. createUserAction (projectDir, notificationMuteRequestInput ());
161 }
162
163 module . exports = {
164 ANSWERS,
165 DECISION_KEY,
166 ENV_KEY,
167 MUTE_REQUIREMENT_ID,
168 NOTIFICATION_MUTE_QUESTION,
169 completionReportMute,
170 ensureNotificationMuteRequest,
171 isValidNotificationMuteEffect,
172 notificationMuteRequestInput,
173 recordNotificationMute,
174 mutedRecorded,
175 readNotificationMute,
176 resolveNotificationMute,
177 setupPlanMuteMismatch,
178 };