@fayz-ai/plugin-getting-started 0.2.0-canary.20261028153856 → 0.2.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.
- package/dist/{chunk-IDU6T6QU.js → chunk-4ATBLO6S.js} +18 -12
- package/dist/chunk-4ATBLO6S.js.map +1 -0
- package/dist/data/mock.d.ts +2 -2
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/host.js +2 -2
- package/dist/index.js +2 -2
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/main.browser.js +2 -1
- package/dist/migrations/main.browser.js.map +1 -1
- package/dist/migrations/main.js +249 -2
- package/dist/migrations/main.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-IDU6T6QU.js.map +0 -1
package/dist/data/mock.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export interface MockGettingStartedOptions {
|
|
|
4
4
|
tenantId?: string;
|
|
5
5
|
/** plg_getting_started_config.trial_days */
|
|
6
6
|
trialDays?: number;
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/** Starts as a tenant whose trial already ran and ended this many days ago. */
|
|
8
|
+
trialEndedDaysAgo?: number;
|
|
9
9
|
minimums?: Partial<Omit<JourneyMinimums, 'ready'>>;
|
|
10
10
|
/** Injectable clock, so a test can walk through the phases without waiting. */
|
|
11
11
|
now?: () => Date;
|
package/dist/data/mock.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock.d.ts","sourceRoot":"","sources":["../../src/data/mock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyB,eAAe,EAAE,YAAY,EAA2C,MAAM,UAAU,CAAA;AAC7H,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAA;AAEzD,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB
|
|
1
|
+
{"version":3,"file":"mock.d.ts","sourceRoot":"","sources":["../../src/data/mock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyB,eAAe,EAAE,YAAY,EAA2C,MAAM,UAAU,CAAA;AAC7H,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAA;AAEzD,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAA;IAClD,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CACjB;AAED,MAAM,WAAW,0BAA2B,SAAQ,0BAA0B;IAC5E,yCAAyC;IACzC,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAAA;IACjE,kEAAkE;IAClE,aAAa,IAAI,IAAI,CAAA;CACtB;AAKD,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,GAAG,EAAE,IAAI,CAAA;IACT,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B,GAAG,YAAY,CAKf;AAED,wBAAgB,gCAAgC,CAAC,OAAO,GAAE,yBAA8B,GAAG,0BAA0B,CA2FpH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAgB,eAAe,EAA4C,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAClI,OAAO,EAAuB,KAAK,0BAA0B,EAAE,MAAM,SAAS,CAAA;AAG9E,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAkBlC,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,eAAe,CAMzD;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAgB,eAAe,EAA4C,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAClI,OAAO,EAAuB,KAAK,0BAA0B,EAAE,MAAM,SAAS,CAAA;AAG9E,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAkBlC,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,eAAe,CAMzD;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAoB5C;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,gBAAgB,CAWxD;AAoBD,wBAAgB,oCAAoC,IAAI,0BAA0B,CA+BjF"}
|
package/dist/host.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { resolveConfig, setActiveConfig, setJourneyOverride, gettingStartedLocales, useJourneyStore, registerGettingStartedToolHandlers, configureStoreSettings, configureStoreProfile, buildStoreLiveProgram, withoutStoreProfileStep, setHostJourneyPrograms, useJourneyTourStore, JOURNEY_TOUR_ID, gettingStartedAiToolsFor, startJourneyTour, withGettingStartedContext, withJourneyDeclaration, TrialSettingsTab, TrialEndBanner, StartTrialAction, StartTrialCta, StoreLiveCard } from './chunk-
|
|
2
|
-
export { DEFAULT_ROUTES, DELIVERY_METHODS, GettingStartedError, JOURNEY_TOUR_ID, OPERATIONS, PROFILE_KEYS, SAVE_PROFILE_TOOL, SERVICE_MODES, CARD_ANCHOR as STORE_LIVE_CARD_ANCHOR, PROGRAM_ID as STORE_LIVE_PROGRAM_ID, STORE_PROFILE_STEP_KEY, TABLE_BANDS, TRIAL_TAB_ANCHOR, buildJourneyClosingStop, buildJourneyTour, buildOwnJourneyTour, buildStoreLiveProgram, createMockGettingStartedProvider, hasOpeningHours, hasPaymentMethods, isForbidden, isMissingClock, journeyTour, resolveConfig, resolveRoutes, storeProfileAnswered, storeProfileFrom, storeProfileFromAnswers, storeProfileToAnswers, storeSetupRouting, withoutStoreProfileStep } from './chunk-
|
|
1
|
+
import { resolveConfig, setActiveConfig, setJourneyOverride, gettingStartedLocales, useJourneyStore, registerGettingStartedToolHandlers, configureStoreSettings, configureStoreProfile, buildStoreLiveProgram, withoutStoreProfileStep, setHostJourneyPrograms, useJourneyTourStore, JOURNEY_TOUR_ID, gettingStartedAiToolsFor, startJourneyTour, withGettingStartedContext, withJourneyDeclaration, TrialSettingsTab, TrialEndBanner, StartTrialAction, StartTrialCta, StoreLiveCard } from './chunk-4ATBLO6S.js';
|
|
2
|
+
export { DEFAULT_ROUTES, DELIVERY_METHODS, GettingStartedError, JOURNEY_TOUR_ID, OPERATIONS, PROFILE_KEYS, SAVE_PROFILE_TOOL, SERVICE_MODES, CARD_ANCHOR as STORE_LIVE_CARD_ANCHOR, PROGRAM_ID as STORE_LIVE_PROGRAM_ID, STORE_PROFILE_STEP_KEY, TABLE_BANDS, TRIAL_TAB_ANCHOR, buildJourneyClosingStop, buildJourneyTour, buildOwnJourneyTour, buildStoreLiveProgram, createMockGettingStartedProvider, hasOpeningHours, hasPaymentMethods, isForbidden, isMissingClock, journeyTour, resolveConfig, resolveRoutes, storeProfileAnswered, storeProfileFrom, storeProfileFromAnswers, storeProfileToAnswers, storeSetupRouting, withoutStoreProfileStep } from './chunk-4ATBLO6S.js';
|
|
3
3
|
import { registerTranslations } from '@fayz-ai/core';
|
|
4
4
|
import { configureActivationBackend, configurePermissions, HostActivationProvider } from '@fayz-ai/admin/host';
|
|
5
5
|
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { resolveConfig, setActiveConfig, setJourneyOverride, gettingStartedLocales, createSupabaseGettingStartedProvider, createMockGettingStartedProvider, useJourneyStore, registerGettingStartedToolHandlers, withGettingStartedContext, withJourneyDeclaration, StartTrialCta, TrialEndBanner, TrialSettingsTab, StoreLiveCard, gettingStartedAiTools, buildOwnJourneyTour, buildStoreLiveProgram } from './chunk-
|
|
2
|
-
export { DEFAULT_ROUTES, GettingStartedError, JOURNEY_TOUR_ID, MINIMUM_IDS, PAYMENT_METHOD_IDS as PAYMENT_METHODS, ProgressRing, RPC, SENSOR_RULED_KEYS, PROGRAM_ID as STORE_LIVE_PROGRAM_ID, STORE_PROFILE_STEP_KEY, STORE_SETTINGS_SAVED_EVENT, StartTrialAction, StartTrialCta, StoreLiveCard, T, TrialEndBanner, TrialSettingsTab, WEEKDAYS, buildJourneyClosingStop, buildJourneyTour, buildOwnJourneyTour, buildStoreLiveProgram, businessProfileStoreProfileProvider, clientJourney, configureStoreProfile, configureStoreSettings, countJourney, countMinimums, createMockGettingStartedProvider, createSupabaseGettingStartedProvider, currentJourney, declareJourney, derivePhase, formatEndTime, formatTrialDate, getActiveConfig, getStoreProfileProvider, getStoreSettingsProvider, gettingStartedAiTools, hasOpeningHours, hasPaymentMethods, isForbidden, isMissingClock, journeyTour, loadStoreSettings, minimumLines, minimumSlotOf, noteTrialDaysDivergence, nowStepKey, readStoreSettings, resolveConfig, resolveRoutes, rowDone, saveStoreSetting, selectJourneySteps, storeProfileAnswered, storeProfileFrom, storeProfileFromAnswers, storeProfileToAnswers, supabaseStoreSettingsProvider, toStepInputs, toTrialSource, trialEndsToday, useJourneyDeclaration, useJourneyStore, useStartTrial, withoutStoreProfileStep } from './chunk-
|
|
1
|
+
import { resolveConfig, setActiveConfig, setJourneyOverride, gettingStartedLocales, createSupabaseGettingStartedProvider, createMockGettingStartedProvider, useJourneyStore, registerGettingStartedToolHandlers, withGettingStartedContext, withJourneyDeclaration, StartTrialCta, TrialEndBanner, TrialSettingsTab, StoreLiveCard, gettingStartedAiTools, buildOwnJourneyTour, buildStoreLiveProgram } from './chunk-4ATBLO6S.js';
|
|
2
|
+
export { DEFAULT_ROUTES, GettingStartedError, JOURNEY_TOUR_ID, MINIMUM_IDS, PAYMENT_METHOD_IDS as PAYMENT_METHODS, ProgressRing, RPC, SENSOR_RULED_KEYS, PROGRAM_ID as STORE_LIVE_PROGRAM_ID, STORE_PROFILE_STEP_KEY, STORE_SETTINGS_SAVED_EVENT, StartTrialAction, StartTrialCta, StoreLiveCard, T, TrialEndBanner, TrialSettingsTab, WEEKDAYS, buildJourneyClosingStop, buildJourneyTour, buildOwnJourneyTour, buildStoreLiveProgram, businessProfileStoreProfileProvider, clientJourney, configureStoreProfile, configureStoreSettings, countJourney, countMinimums, createMockGettingStartedProvider, createSupabaseGettingStartedProvider, currentJourney, declareJourney, derivePhase, formatEndTime, formatTrialDate, getActiveConfig, getStoreProfileProvider, getStoreSettingsProvider, gettingStartedAiTools, hasOpeningHours, hasPaymentMethods, isForbidden, isMissingClock, journeyTour, loadStoreSettings, minimumLines, minimumSlotOf, noteTrialDaysDivergence, nowStepKey, readStoreSettings, resolveConfig, resolveRoutes, rowDone, saveStoreSetting, selectJourneySteps, storeProfileAnswered, storeProfileFrom, storeProfileFromAnswers, storeProfileToAnswers, supabaseStoreSettingsProvider, toStepInputs, toTrialSource, trialEndsToday, useJourneyDeclaration, useJourneyStore, useStartTrial, withoutStoreProfileStep } from './chunk-4ATBLO6S.js';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { registerTranslations, createSafeDataProvider, definePlugin, tl } from '@fayz-ai/core';
|
|
5
5
|
import { defineOnboardingWidget } from '@fayz-ai/ui';
|
|
@@ -5,6 +5,7 @@ export declare const MIGRATION_003_THE_CLOCK_STARTS_ONCE_AND_ONLY_FOR_WHO_MAY =
|
|
|
5
5
|
export declare const MIGRATION_004_LEAD_KEEPS_THE_NAME = "-- 004_lead_keeps_the_name.sql \u2014 001 padded the house name with rpad(name, 2), which also CUTS it to 2\n-- characters: every lead in the Faya CRM was born named \"Bu\". Pad only names shorter than 2.\n\nCREATE OR REPLACE FUNCTION public.plg_getting_started_lead(p_tenant uuid, p_tag text, p_extra jsonb DEFAULT '{}'::jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_crm uuid;\n v_slug text;\n v_name text;\n v_email text;\n v_phone text;\n v_lead uuid;\n v_j record;\nBEGIN\n IF to_regprocedure('public.create_public_lead(uuid,text,text,text,text,text,jsonb,text,jsonb,text[],jsonb,text)') IS NULL THEN\n RETURN jsonb_build_object('skipped', 'plugin-crm absent');\n END IF;\n SELECT c.crm_tenant_slug INTO v_slug FROM public.plg_getting_started_config c;\n SELECT t.id INTO v_crm FROM public.tenants t WHERE t.slug = v_slug;\n IF v_crm IS NULL OR v_crm = p_tenant THEN RETURN jsonb_build_object('skipped', 'crm tenant not found'); END IF;\n\n SELECT t.name, t.slug INTO v_name, v_slug FROM public.tenants t WHERE t.id = p_tenant;\n IF v_name IS NULL THEN RETURN jsonb_build_object('skipped', 'tenant not found'); END IF;\n\n SELECT u.email, pe.phone INTO v_email, v_phone\n FROM app.memberships m\n JOIN auth.users u ON u.id = m.user_id\n LEFT JOIN public.people pe ON pe.tenant_id = m.tenant_id AND lower(pe.email) = lower(u.email)\n WHERE m.tenant_id = p_tenant AND m.active\n ORDER BY m.created_at\n LIMIT 1;\n -- create_public_lead raises on a phone outside 8\u201315 digits; a bad number is dropped, not fatal.\n v_phone := nullif(regexp_replace(coalesce(v_phone, ''), '\\D', '', 'g'), '');\n IF v_phone IS NOT NULL AND length(v_phone) NOT BETWEEN 8 AND 15 THEN v_phone := NULL; END IF;\n IF v_email IS NULL AND v_phone IS NULL THEN RETURN jsonb_build_object('skipped', 'no contact'); END IF;\n\n SELECT * INTO v_j FROM public.v_getting_started_journey v WHERE v.tenant_id = p_tenant;\n\n -- Dedupe by e-mail/phone is create_public_lead's own; the tag says where in the journey this is.\n SELECT l.lead_id INTO v_lead FROM public.create_public_lead(\n v_crm, left(CASE WHEN length(btrim(v_name)) < 2 THEN rpad(btrim(v_name), 2, '.') ELSE btrim(v_name) END, 120), v_phone, v_email,\n 'getting-started', 'Teste gr\u00E1tis \u2014 ' || coalesce(v_slug, ''),\n jsonb_build_object('tenant_id', p_tenant, 'tenant_slug', v_slug, 'phase', v_j.phase,\n 'trial_started_at', v_j.trial_started_at, 'trial_ends_at', v_j.trial_ends_at)\n || coalesce(p_extra, '{}'::jsonb),\n NULL, coalesce(v_j.utm, '{}'::jsonb), ARRAY['trial', p_tag], '[]'::jsonb, NULL) l;\n RETURN jsonb_build_object('lead_id', v_lead, 'tag', p_tag);\nEND $$;\nREVOKE ALL ON FUNCTION public.plg_getting_started_lead(uuid, text, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_getting_started_lead(uuid, text, jsonb) TO service_role;\n\n-- Handlers in the shape consume_event_log runs: public.<handler>(jsonb), where the argument is\n-- to_jsonb(event_log row) \u2014 so `tenant_id` and `payload` are top-level keys.\nCREATE OR REPLACE FUNCTION public.plg_getting_started_on_trial_started(p_event jsonb)\nRETURNS jsonb LANGUAGE sql SECURITY DEFINER SET search_path TO '' AS $$\n SELECT public.plg_getting_started_lead((p_event ->> 'tenant_id')::uuid, 'trial_started', coalesce(p_event -> 'payload', '{}'::jsonb));\n$$;\n\nREVOKE ALL ON FUNCTION public.plg_getting_started_lead(uuid, text, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_getting_started_lead(uuid, text, jsonb) TO service_role;\n";
|
|
6
6
|
export declare const MIGRATION_005_QA_LEADS_GO_TO_QA_PLATFORM = "-- 005_qa_leads_go_to_qa_platform.sql \u2014 a QA signup never lands in the real Faya funnel.\n--\n-- The V2-native e2e (qa+e2e-<ts>@fayalabs.com) created leads in the REAL `controlgroup` CRM because\n-- plg_getting_started_lead always targeted crm_tenant_slug. Now, when the tenant's owner/contact\n-- e-mail matches qa_email_pattern (ILIKE), the lead goes to qa_crm_tenant_slug instead. When that\n-- tenant does not exist the lead is NOT created and the handler result (event_deliveries.result)\n-- says why \u2014 it never falls back to the real CRM. Any other e-mail: unchanged (004 behaviour).\n--\n-- Also restores 003's guarded plg_getting_started_on_trial_started: 004 re-created the unguarded 001\n-- form by accident (any tenant member could emit trial_started and mint a lead without a trial).\n\n-- \u2500\u2500 config: where QA leads go \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nALTER TABLE public.plg_getting_started_config\n ADD COLUMN IF NOT EXISTS qa_email_pattern text NOT NULL DEFAULT 'qa+%@fayalabs.com',\n ADD COLUMN IF NOT EXISTS qa_crm_tenant_slug text NOT NULL DEFAULT 'controlgroup-qa';\n-- 003 narrowed authenticated to (id, trial_days, setup_window_days); the new columns are read by\n-- DEFINER functions only, so no column grant is added here.\n\nCREATE OR REPLACE FUNCTION public.plg_getting_started_lead(p_tenant uuid, p_tag text, p_extra jsonb DEFAULT '{}'::jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_crm uuid;\n v_slug text;\n v_crm_slug text;\n v_qa_pat text;\n v_qa_slug text;\n v_qa boolean := false;\n v_name text;\n v_email text;\n v_phone text;\n v_lead uuid;\n v_j record;\nBEGIN\n IF to_regprocedure('public.create_public_lead(uuid,text,text,text,text,text,jsonb,text,jsonb,text[],jsonb,text)') IS NULL THEN\n RETURN jsonb_build_object('skipped', 'plugin-crm absent');\n END IF;\n SELECT c.crm_tenant_slug, nullif(btrim(c.qa_email_pattern), ''), nullif(btrim(c.qa_crm_tenant_slug), '')\n INTO v_crm_slug, v_qa_pat, v_qa_slug\n FROM public.plg_getting_started_config c;\n\n SELECT t.name, t.slug INTO v_name, v_slug FROM public.tenants t WHERE t.id = p_tenant;\n IF v_name IS NULL THEN RETURN jsonb_build_object('skipped', 'tenant not found'); END IF;\n\n SELECT u.email, pe.phone INTO v_email, v_phone\n FROM app.memberships m\n JOIN auth.users u ON u.id = m.user_id\n LEFT JOIN public.people pe ON pe.tenant_id = m.tenant_id AND lower(pe.email) = lower(u.email)\n WHERE m.tenant_id = p_tenant AND m.active\n ORDER BY m.created_at\n LIMIT 1;\n\n -- QA contact \u2192 the QA platform's CRM, never the real one (not even as a fallback).\n v_qa := v_qa_pat IS NOT NULL AND v_email IS NOT NULL AND v_email ILIKE v_qa_pat;\n IF v_qa THEN\n SELECT t.id INTO v_crm FROM public.tenants t WHERE t.slug = v_qa_slug;\n IF v_crm IS NULL OR v_crm = p_tenant THEN\n RETURN jsonb_build_object('skipped', 'qa crm tenant not found', 'qa', true,\n 'qa_crm_tenant_slug', v_qa_slug, 'tag', p_tag);\n END IF;\n ELSE\n SELECT t.id INTO v_crm FROM public.tenants t WHERE t.slug = v_crm_slug;\n IF v_crm IS NULL OR v_crm = p_tenant THEN RETURN jsonb_build_object('skipped', 'crm tenant not found'); END IF;\n END IF;\n\n -- create_public_lead raises on a phone outside 8\u201315 digits; a bad number is dropped, not fatal.\n v_phone := nullif(regexp_replace(coalesce(v_phone, ''), '\\D', '', 'g'), '');\n IF v_phone IS NOT NULL AND length(v_phone) NOT BETWEEN 8 AND 15 THEN v_phone := NULL; END IF;\n IF v_email IS NULL AND v_phone IS NULL THEN RETURN jsonb_build_object('skipped', 'no contact'); END IF;\n\n SELECT * INTO v_j FROM public.v_getting_started_journey v WHERE v.tenant_id = p_tenant;\n\n -- Dedupe by e-mail/phone is create_public_lead's own; the tag says where in the journey this is.\n SELECT l.lead_id INTO v_lead FROM public.create_public_lead(\n v_crm, left(CASE WHEN length(btrim(v_name)) < 2 THEN rpad(btrim(v_name), 2, '.') ELSE btrim(v_name) END, 120), v_phone, v_email,\n 'getting-started', 'Teste gr\u00E1tis \u2014 ' || coalesce(v_slug, ''),\n jsonb_build_object('tenant_id', p_tenant, 'tenant_slug', v_slug, 'phase', v_j.phase,\n 'trial_started_at', v_j.trial_started_at, 'trial_ends_at', v_j.trial_ends_at)\n || coalesce(p_extra, '{}'::jsonb),\n NULL, coalesce(v_j.utm, '{}'::jsonb), ARRAY['trial', p_tag], '[]'::jsonb, NULL) l;\n RETURN jsonb_build_object('lead_id', v_lead, 'tag', p_tag)\n || CASE WHEN v_qa THEN jsonb_build_object('qa', true, 'crm_tenant_slug', v_qa_slug) ELSE '{}'::jsonb END;\nEND $$;\nREVOKE ALL ON FUNCTION public.plg_getting_started_lead(uuid, text, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_getting_started_lead(uuid, text, jsonb) TO service_role;\n\n-- \u2500\u2500 003's guard, back (004 regressed it to the 001 form) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.plg_getting_started_on_trial_started(p_event jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;\nBEGIN\n IF v_tenant IS NULL THEN\n RETURN jsonb_build_object('skipped', 'no tenant');\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_getting_started_journeys j\n WHERE j.tenant_id = v_tenant AND j.trial_started_at IS NOT NULL)\n OR NOT EXISTS (\n SELECT 1 FROM public.plg_getting_started_events e\n WHERE e.tenant_id = v_tenant AND e.kind = 'trial_started') THEN\n RETURN jsonb_build_object('skipped', 'trial_started was not written by getting_started_start_trial');\n END IF;\n RETURN public.plg_getting_started_lead(v_tenant, 'trial_started', coalesce(p_event -> 'payload', '{}'::jsonb));\nEND $$;\nREVOKE ALL ON FUNCTION public.plg_getting_started_on_trial_started(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_getting_started_on_trial_started(jsonb) TO service_role;\n";
|
|
7
7
|
export declare const MIGRATION_006_THE_LEAD_IS_BORN_AT_SIGNUP = "-- 006_the_lead_is_born_at_signup.sql \u2014 the trial request becomes a lead when the workspace is born,\n-- not only when \"Iniciar teste\" is clicked (TP-08).\n--\n-- Before: a V2 house reached the Faya CRM only through getting_started.trial_started (005). A house\n-- that signed up and never started the trial was invisible to sales.\n--\n-- Now:\n-- * When the getting-started binding becomes active (app.tenant_plugins: create_tenant through the\n-- food vertical defaults, or ensureDefaultModules / tenant_plugin_set right after), a DEFINER\n-- trigger leaves its mark on the timeline (kind 'signed_up', once per tenant, ever) and files\n-- getting_started.signed_up. The trigger runs inside the signup transaction and never fails it.\n-- It does not build the lead there: the owner's membership may not exist yet (the binding is\n-- written by the tenants AFTER INSERT trigger, before create_tenant inserts the membership).\n-- * The handler (async, after commit) accepts only what the trigger wrote \u2014 same rule as 003:\n-- plg_emit_event is open to any member, and a hand-made signed_up must not mint a lead \u2014 and\n-- creates the lead with origem \"Cadastro V2\". create_public_lead puts it on the first open\n-- stage of the CRM's default funnel (crm_lead_to_board).\n-- * When the trial starts, plg_getting_started_lead finds the SAME lead (create_public_lead dedupes\n-- by the owner's e-mail) and moves its deal FORWARD to the stage named by\n-- config.lead_stage_patterns->>'trial_started' (ILIKE, default '%teste%') \u2014 never backwards,\n-- never out of won/lost. No matching stage: nothing moves and the result says why.\n-- * QA rule of 005 kept: qa+\u2026@fayalabs.com \u2192 controlgroup-qa, never controlgroup.\n-- * 003's guarded plg_getting_started_on_trial_started is NOT re-created here (004 lost it once by\n-- re-creating it; 005 restored it). This file only touches the lead function it calls.\n--\n-- Only who is signing up gets a lead: free plan, inside the trial window, not converted, trial not\n-- started, workspace created in the last 48 hours. A binding switched on for an old or paying\n-- tenant (an operator, a backfill) is not a signup.\n\n-- \u2500\u2500 config: where each journey tag moves the deal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nALTER TABLE public.plg_getting_started_config\n ADD COLUMN IF NOT EXISTS lead_stage_patterns jsonb NOT NULL DEFAULT '{\"trial_started\": \"%teste%\"}'::jsonb;\n-- Read by DEFINER functions only (003 narrowed the column grant of authenticated).\n\n-- \u2500\u2500 the lead: origem per tag, and the stage it moves to \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.plg_getting_started_lead(p_tenant uuid, p_tag text, p_extra jsonb DEFAULT '{}'::jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_crm uuid;\n v_slug text;\n v_crm_slug text;\n v_qa_pat text;\n v_qa_slug text;\n v_qa boolean := false;\n v_name text;\n v_email text;\n v_phone text;\n v_lead uuid;\n v_j record;\n v_patterns jsonb;\n v_pattern text;\n v_origin text;\n v_deal record;\n v_target record;\n v_stage jsonb;\nBEGIN\n IF to_regprocedure('public.create_public_lead(uuid,text,text,text,text,text,jsonb,text,jsonb,text[],jsonb,text)') IS NULL THEN\n RETURN jsonb_build_object('skipped', 'plugin-crm absent');\n END IF;\n SELECT c.crm_tenant_slug, nullif(btrim(c.qa_email_pattern), ''), nullif(btrim(c.qa_crm_tenant_slug), ''),\n coalesce(c.lead_stage_patterns, '{}'::jsonb)\n INTO v_crm_slug, v_qa_pat, v_qa_slug, v_patterns\n FROM public.plg_getting_started_config c;\n\n SELECT t.name, t.slug INTO v_name, v_slug FROM public.tenants t WHERE t.id = p_tenant;\n IF v_name IS NULL THEN RETURN jsonb_build_object('skipped', 'tenant not found'); END IF;\n\n SELECT u.email, pe.phone INTO v_email, v_phone\n FROM app.memberships m\n JOIN auth.users u ON u.id = m.user_id\n LEFT JOIN public.people pe ON pe.tenant_id = m.tenant_id AND lower(pe.email) = lower(u.email)\n WHERE m.tenant_id = p_tenant AND m.active\n ORDER BY m.created_at\n LIMIT 1;\n\n -- QA contact \u2192 the QA platform's CRM, never the real one (not even as a fallback). 005.\n v_qa := v_qa_pat IS NOT NULL AND v_email IS NOT NULL AND v_email ILIKE v_qa_pat;\n IF v_qa THEN\n SELECT t.id INTO v_crm FROM public.tenants t WHERE t.slug = v_qa_slug;\n IF v_crm IS NULL OR v_crm = p_tenant THEN\n RETURN jsonb_build_object('skipped', 'qa crm tenant not found', 'qa', true,\n 'qa_crm_tenant_slug', v_qa_slug, 'tag', p_tag);\n END IF;\n ELSE\n SELECT t.id INTO v_crm FROM public.tenants t WHERE t.slug = v_crm_slug;\n IF v_crm IS NULL OR v_crm = p_tenant THEN RETURN jsonb_build_object('skipped', 'crm tenant not found'); END IF;\n END IF;\n\n -- create_public_lead raises on a phone outside 8\u201315 digits; a bad number is dropped, not fatal.\n v_phone := nullif(regexp_replace(coalesce(v_phone, ''), '\\D', '', 'g'), '');\n IF v_phone IS NOT NULL AND length(v_phone) NOT BETWEEN 8 AND 15 THEN v_phone := NULL; END IF;\n IF v_email IS NULL AND v_phone IS NULL THEN RETURN jsonb_build_object('skipped', 'no contact'); END IF;\n\n SELECT * INTO v_j FROM public.v_getting_started_journey v WHERE v.tenant_id = p_tenant;\n\n -- The origem (sourceName) is written when the lead is BORN; a later tag on the same person goes\n -- to its submissions. The signup is the birth now, so it names the origem.\n v_origin := CASE WHEN p_tag = 'cadastro_v2' THEN 'Cadastro V2' ELSE 'Teste gr\u00E1tis \u2014 ' || coalesce(v_slug, '') END;\n\n -- Dedupe by e-mail/phone is create_public_lead's own: the signup lead and the trial lead are one person.\n SELECT l.lead_id INTO v_lead FROM public.create_public_lead(\n v_crm, left(CASE WHEN length(btrim(v_name)) < 2 THEN rpad(btrim(v_name), 2, '.') ELSE btrim(v_name) END, 120), v_phone, v_email,\n 'getting-started', v_origin,\n jsonb_build_object('tenant_id', p_tenant, 'tenant_slug', v_slug, 'phase', v_j.phase,\n 'trial_started_at', v_j.trial_started_at, 'trial_ends_at', v_j.trial_ends_at)\n || coalesce(p_extra, '{}'::jsonb),\n NULL, coalesce(v_j.utm, '{}'::jsonb), ARRAY['trial', p_tag], '[]'::jsonb, NULL) l;\n\n -- \u2500\u2500 the deal moves forward to the stage this tag names \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n v_pattern := nullif(btrim(coalesce(v_patterns ->> p_tag, '')), '');\n IF v_lead IS NOT NULL AND v_pattern IS NOT NULL AND to_regclass('public.plg_crm_deals') IS NOT NULL THEN\n -- The card is born by crm_lead_to_board; a lead without one (trigger failed, hand-made) gets it now.\n IF NOT EXISTS (SELECT 1 FROM public.plg_crm_deals d WHERE d.lead_id = v_lead)\n AND to_regprocedure('public.crm_place_lead_on_board(uuid)') IS NOT NULL THEN\n PERFORM public.crm_place_lead_on_board(v_lead);\n END IF;\n SELECT d.id, d.status, d.pipeline_id, d.stage_id, s.name AS stage_name, s.\"order\" AS stage_order,\n coalesce(s.is_won, false) OR coalesce(s.is_lost, false) AS closed\n INTO v_deal\n FROM public.plg_crm_deals d\n LEFT JOIN public.plg_crm_pipeline_stages s ON s.id = d.stage_id\n WHERE d.lead_id = v_lead AND d.tenant_id = v_crm\n LIMIT 1;\n IF v_deal.id IS NULL THEN\n v_stage := jsonb_build_object('moved', false, 'reason', 'no deal for the lead');\n ELSIF v_deal.status <> 'open' OR v_deal.closed THEN\n v_stage := jsonb_build_object('moved', false, 'reason', 'deal is closed', 'deal_id', v_deal.id);\n ELSE\n SELECT s.id, s.name, s.\"order\", s.probability INTO v_target\n FROM public.plg_crm_pipeline_stages s\n WHERE s.pipeline_id = v_deal.pipeline_id AND NOT coalesce(s.is_won, false) AND NOT coalesce(s.is_lost, false)\n AND s.name ILIKE v_pattern\n ORDER BY s.\"order\", s.created_at\n LIMIT 1;\n IF v_target.id IS NULL THEN\n v_stage := jsonb_build_object('moved', false, 'reason', 'no stage matches', 'pattern', v_pattern, 'deal_id', v_deal.id);\n ELSIF v_deal.stage_id = v_target.id THEN\n v_stage := jsonb_build_object('moved', false, 'reason', 'already there', 'stage', v_target.name, 'deal_id', v_deal.id);\n ELSIF v_deal.stage_order IS NOT NULL AND v_deal.stage_order > v_target.\"order\" THEN\n -- Sales already moved it further (proposal, negotiation): the journey never pulls it back.\n v_stage := jsonb_build_object('moved', false, 'reason', 'deal is ahead', 'stage', v_deal.stage_name, 'deal_id', v_deal.id);\n ELSE\n UPDATE public.plg_crm_deals\n SET stage_id = v_target.id, probability = coalesce(v_target.probability, probability)\n WHERE id = v_deal.id;\n v_stage := jsonb_build_object('moved', true, 'from', v_deal.stage_name, 'to', v_target.name, 'deal_id', v_deal.id);\n END IF;\n END IF;\n END IF;\n\n RETURN jsonb_build_object('lead_id', v_lead, 'tag', p_tag)\n || CASE WHEN v_stage IS NOT NULL THEN jsonb_build_object('stage', v_stage) ELSE '{}'::jsonb END\n || CASE WHEN v_qa THEN jsonb_build_object('qa', true, 'crm_tenant_slug', v_qa_slug) ELSE '{}'::jsonb END;\nEND $$;\nREVOKE ALL ON FUNCTION public.plg_getting_started_lead(uuid, text, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_getting_started_lead(uuid, text, jsonb) TO service_role;\n\n-- \u2500\u2500 the mark: the binding became active for a house that is signing up \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.plg_getting_started_on_binding()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_t record;\n v_j record;\n v_payload jsonb;\nBEGIN\n IF NEW.plugin_id <> 'getting-started' OR NEW.facet <> '' OR NEW.status <> 'active' THEN RETURN NULL; END IF;\n IF TG_OP = 'UPDATE' AND OLD.status = 'active' THEN RETURN NULL; END IF;\n -- Never fails the signup: an account created without its lead is recoverable, a lost signup is not.\n BEGIN\n -- Once per tenant, ever: switching the module off and on is not a second signup.\n IF EXISTS (SELECT 1 FROM public.plg_getting_started_events e\n WHERE e.tenant_id = NEW.tenant_id AND e.kind = 'signed_up') THEN\n RETURN NULL;\n END IF;\n SELECT t.plan, t.trial_ends_at, t.created_at INTO v_t FROM public.tenants t WHERE t.id = NEW.tenant_id;\n IF NOT FOUND\n OR coalesce(nullif(btrim(v_t.plan), ''), 'free') <> 'free'\n OR v_t.trial_ends_at IS NULL\n OR v_t.created_at < now() - interval '48 hours' THEN\n RETURN NULL;\n END IF;\n SELECT j.trial_started_at, j.converted_at INTO v_j\n FROM public.plg_getting_started_journeys j WHERE j.tenant_id = NEW.tenant_id;\n IF v_j.trial_started_at IS NOT NULL OR v_j.converted_at IS NOT NULL THEN\n RETURN NULL;\n END IF;\n\n v_payload := jsonb_build_object('source', 'cadastro_v2', 'binding_source', NEW.source);\n INSERT INTO public.plg_getting_started_events (tenant_id, kind, actor_id, payload)\n VALUES (NEW.tenant_id, 'signed_up', auth.uid(), v_payload)\n ON CONFLICT (tenant_id, kind, day) DO NOTHING;\n -- plg_emit_event refuses a caller who is not yet a member, and inside create_tenant the\n -- membership comes after the binding: file the fact directly (002's helper).\n PERFORM public.plg_getting_started_platform_emit(NEW.tenant_id, 'getting_started.signed_up', v_payload);\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING '[getting-started] signed_up not recorded for tenant %: %', NEW.tenant_id, SQLERRM;\n END;\n RETURN NULL;\nEND $$;\nREVOKE ALL ON FUNCTION public.plg_getting_started_on_binding() FROM PUBLIC, anon, authenticated;\n\nDROP TRIGGER IF EXISTS plg_getting_started_signed_up ON app.tenant_plugins;\nCREATE TRIGGER plg_getting_started_signed_up\n AFTER INSERT OR UPDATE OF status ON app.tenant_plugins\n FOR EACH ROW\n WHEN (NEW.plugin_id = 'getting-started' AND NEW.status = 'active')\n EXECUTE FUNCTION public.plg_getting_started_on_binding();\n\n-- \u2500\u2500 the handler accepts only what the trigger wrote (003's rule) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.plg_getting_started_on_signed_up(p_event jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;\nBEGIN\n IF v_tenant IS NULL THEN\n RETURN jsonb_build_object('skipped', 'no tenant');\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_getting_started_events e\n WHERE e.tenant_id = v_tenant AND e.kind = 'signed_up') THEN\n RETURN jsonb_build_object('skipped', 'signed_up was not written by the getting-started binding');\n END IF;\n RETURN public.plg_getting_started_lead(v_tenant, 'cadastro_v2', coalesce(p_event -> 'payload', '{}'::jsonb));\nEND $$;\nREVOKE ALL ON FUNCTION public.plg_getting_started_on_signed_up(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_getting_started_on_signed_up(jsonb) TO service_role;\n\nSELECT public.register_event_subscription('getting-started', 'getting_started.signed_up', 'plg_getting_started_on_signed_up',\n 'Faya lead when the workspace is created (cadastro V2)', 5);\n";
|
|
8
|
+
export declare const MIGRATION_007_THE_TRIAL_CLOCK_STARTS_AT_THE_CLICK = "-- 007_the_trial_clock_starts_at_the_click.sql \u2014 the trial is 14 days counted from\n-- \"Iniciar teste\"; nothing counts before that. Depends on 000, 003 and 006.\n--\n-- * v_getting_started_journey: `expired` only after a trial that STARTED. A date\n-- left by the old signup clock (db 044) on a tenant still in setup no longer\n-- reads \"Seu teste terminou\" above \"Para iniciar o teste\". No setup deadline.\n-- * getting_started_start_trial: a tenant with no date can start \u2014 that is now\n-- every new tenant. Still once per tenant, never for converted or paying.\n-- * plg_getting_started_on_binding: a tenant born with this plugin drops the\n-- date the spine DEFAULT stamped. Apps without getting-started keep that\n-- DEFAULT (they have no \"Iniciar teste\" to start a clock).\n--\n-- Existing tenants still in setup keep the old date until the one-off cleanup in\n-- docs/checkpoints/FAY-1763-BACKEND-HANDOFF.md runs (it changes live accounts).\n\n-- \u2500\u2500 the phase \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE VIEW public.v_getting_started_journey WITH (security_invoker = true) AS\nSELECT t.id AS tenant_id,\n j.ready_at, j.trial_started_at, j.converted_at, j.wants_contract_at, j.source, j.utm,\n coalesce(j.trial_days, c.trial_days) AS trial_days,\n t.trial_ends_at,\n NULL::timestamptz AS setup_deadline_at,\n CASE\n WHEN j.converted_at IS NOT NULL THEN 'converted'\n WHEN j.trial_started_at IS NULL THEN 'setup'\n WHEN t.trial_ends_at IS NOT NULL AND t.trial_ends_at <= now() THEN 'expired'\n ELSE 'trial'\n END AS phase\n FROM public.tenants t\n CROSS JOIN public.plg_getting_started_config c\n LEFT JOIN public.plg_getting_started_journeys j ON j.tenant_id = t.id;\nGRANT SELECT ON public.v_getting_started_journey TO authenticated;\n\n-- \u2500\u2500 the start: no date is the normal case now \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.getting_started_start_trial(p_tenant uuid DEFAULT NULL)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := coalesce(p_tenant, app.current_tenant_id());\n v_days integer;\n v_min jsonb;\n v_t record;\n v_j record;\n v_started timestamptz;\n v_ends timestamptz;\nBEGIN\n -- Same gate as tenant_setting_set: a session, the tenant in scope, settings.manage.\n IF auth.uid() IS NULL OR v_tenant IS NULL OR v_tenant IS DISTINCT FROM app.current_tenant_id() THEN\n RAISE EXCEPTION 'getting_started_start_trial: no session or tenant out of scope' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('settings.manage') THEN\n RAISE EXCEPTION 'getting_started_start_trial: settings.manage is required' USING ERRCODE = '42501';\n END IF;\n\n -- The lock: a second concurrent call waits here and then sees the start.\n SELECT t.id, t.trial_ends_at, t.plan INTO v_t FROM public.tenants t WHERE t.id = v_tenant FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'getting_started_start_trial: tenant not found' USING ERRCODE = 'P0002';\n END IF;\n SELECT j.trial_started_at, j.converted_at INTO v_j\n FROM public.plg_getting_started_journeys j WHERE j.tenant_id = v_tenant FOR UPDATE;\n\n -- Idempotent: a second call returns the state and leaves the date alone.\n IF v_j.trial_started_at IS NOT NULL THEN\n RETURN jsonb_build_object('ok', true, 'already_started', true, 'trial_started_at', v_j.trial_started_at,\n 'trial_ends_at', v_t.trial_ends_at);\n END IF;\n\n v_min := public.getting_started_minimums(v_tenant);\n\n -- Who is already converted, or already paying, is not offered a clock that\n -- would expire on them. A NULL trial_ends_at no longer means \"no trial\": since\n -- 007 every new tenant is born without a clock and gets it here.\n IF v_j.converted_at IS NOT NULL THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'already_converted', 'minimums', v_min);\n END IF;\n IF coalesce(nullif(btrim(v_t.plan), ''), 'free') <> 'free' THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'already_paid', 'minimums', v_min, 'plan', v_t.plan);\n END IF;\n\n -- Minimums are checked here, not in the button.\n IF NOT coalesce((v_min ->> 'ready')::boolean, false) THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'minimums_missing', 'minimums', v_min);\n END IF;\n\n SELECT c.trial_days INTO v_days FROM public.plg_getting_started_config c;\n v_days := coalesce(v_days, 14);\n v_started := now();\n v_ends := v_started + make_interval(days => v_days);\n\n INSERT INTO public.plg_getting_started_journeys (tenant_id, ready_at, trial_started_at, trial_days)\n VALUES (v_tenant, v_started, v_started, v_days)\n ON CONFLICT (tenant_id) DO UPDATE\n SET ready_at = coalesce(public.plg_getting_started_journeys.ready_at, EXCLUDED.ready_at),\n trial_started_at = EXCLUDED.trial_started_at,\n trial_days = EXCLUDED.trial_days,\n updated_at = now();\n\n -- The real clock. Passes tenants_trial_is_readonly (db 221): this DEFINER\n -- function runs as its owner, which the gate lets through.\n UPDATE public.tenants SET trial_ends_at = v_ends WHERE id = v_tenant;\n\n INSERT INTO public.plg_getting_started_events (tenant_id, kind, actor_id, payload)\n VALUES (v_tenant, 'trial_started', auth.uid(), jsonb_build_object('trial_days', v_days, 'ends_at', v_ends))\n ON CONFLICT (tenant_id, kind, day) DO NOTHING;\n\n PERFORM public.plg_emit_event('getting_started.trial_started',\n jsonb_build_object('trial_days', v_days, 'trial_ends_at', v_ends), 'tenant', v_tenant::text, v_tenant);\n\n RETURN jsonb_build_object('ok', true, 'trial_started_at', v_started, 'trial_ends_at', v_ends, 'trial_days', v_days);\nEND $$;\nREVOKE ALL ON FUNCTION public.getting_started_start_trial(uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.getting_started_start_trial(uuid) TO authenticated, service_role;\n\n-- \u2500\u2500 the platform config: the client reads only what it needs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- The view (000) reads trial_days; the setup window is documentation of the\n-- spine default. crm_tenant_slug and reminder_hour_utc are read inside the\n-- DEFINER functions only. The policy stays (USING true); the GRANT narrows.\nREVOKE SELECT ON public.plg_getting_started_config FROM authenticated;\nGRANT SELECT (id, trial_days, setup_window_days) ON public.plg_getting_started_config TO authenticated;\n\n-- \u2500\u2500 the handlers accept only what the RPCs wrote \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- plg_emit_event is open to any member of the tenant; the journey row and the\n-- timeline are written by DEFINER RPCs only (no write policy). So a lead is\n-- created only when the mark the RPC leaves is there.\nCREATE OR REPLACE FUNCTION public.plg_getting_started_on_trial_started(p_event jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;\nBEGIN\n IF v_tenant IS NULL THEN\n RETURN jsonb_build_object('skipped', 'no tenant');\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_getting_started_journeys j\n WHERE j.tenant_id = v_tenant AND j.trial_started_at IS NOT NULL)\n OR NOT EXISTS (\n SELECT 1 FROM public.plg_getting_started_events e\n WHERE e.tenant_id = v_tenant AND e.kind = 'trial_started') THEN\n RETURN jsonb_build_object('skipped', 'trial_started was not written by getting_started_start_trial');\n END IF;\n RETURN public.plg_getting_started_lead(v_tenant, 'trial_started', coalesce(p_event -> 'payload', '{}'::jsonb));\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plg_getting_started_on_wants_contract(p_event jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;\nBEGIN\n IF v_tenant IS NULL THEN\n RETURN jsonb_build_object('skipped', 'no tenant');\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_getting_started_journeys j\n WHERE j.tenant_id = v_tenant AND j.wants_contract_at IS NOT NULL)\n OR NOT EXISTS (\n SELECT 1 FROM public.plg_getting_started_events e\n WHERE e.tenant_id = v_tenant AND e.kind = 'wants_contract'\n AND e.created_at >= now() - interval '2 days') THEN\n RETURN jsonb_build_object('skipped', 'wants_contract was not written by getting_started_request_contract');\n END IF;\n RETURN public.plg_getting_started_lead(v_tenant, 'quer_contratar', coalesce(p_event -> 'payload', '{}'::jsonb));\nEND $$;\nREVOKE ALL ON FUNCTION public.plg_getting_started_on_trial_started(jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plg_getting_started_on_wants_contract(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_getting_started_on_trial_started(jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plg_getting_started_on_wants_contract(jsonb) TO service_role;\nREVOKE ALL ON FUNCTION public.getting_started_start_trial(uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.getting_started_start_trial(uuid) TO authenticated, service_role;\n\n-- \u2500\u2500 the birth: drop the signup date \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.plg_getting_started_on_binding()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_t record;\n v_j record;\n v_payload jsonb;\nBEGIN\n IF NEW.plugin_id <> 'getting-started' OR NEW.facet <> '' OR NEW.status <> 'active' THEN RETURN NULL; END IF;\n IF TG_OP = 'UPDATE' AND OLD.status = 'active' THEN RETURN NULL; END IF;\n -- Never fails the signup: an account created without its lead is recoverable, a lost signup is not.\n BEGIN\n -- Once per tenant, ever: switching the module off and on is not a second signup.\n IF EXISTS (SELECT 1 FROM public.plg_getting_started_events e\n WHERE e.tenant_id = NEW.tenant_id AND e.kind = 'signed_up') THEN\n RETURN NULL;\n END IF;\n SELECT t.plan, t.trial_ends_at, t.created_at INTO v_t FROM public.tenants t WHERE t.id = NEW.tenant_id;\n IF NOT FOUND\n OR coalesce(nullif(btrim(v_t.plan), ''), 'free') <> 'free'\n OR v_t.trial_ends_at IS NULL\n OR v_t.created_at < now() - interval '48 hours' THEN\n RETURN NULL;\n END IF;\n SELECT j.trial_started_at, j.converted_at INTO v_j\n FROM public.plg_getting_started_journeys j WHERE j.tenant_id = NEW.tenant_id;\n IF v_j.trial_started_at IS NOT NULL OR v_j.converted_at IS NOT NULL THEN\n RETURN NULL;\n END IF;\n\n v_payload := jsonb_build_object('source', 'cadastro_v2', 'binding_source', NEW.source);\n INSERT INTO public.plg_getting_started_events (tenant_id, kind, actor_id, payload)\n VALUES (NEW.tenant_id, 'signed_up', auth.uid(), v_payload)\n ON CONFLICT (tenant_id, kind, day) DO NOTHING;\n -- plg_emit_event refuses a caller who is not yet a member, and inside create_tenant the\n -- membership comes after the binding: file the fact directly (002's helper).\n PERFORM public.plg_getting_started_platform_emit(NEW.tenant_id, 'getting_started.signed_up', v_payload);\n -- The spine DEFAULT (db 044) stamped a 30-day date at signup; the trial is 14\n -- days from \"Iniciar teste\", so this tenant starts with no clock at all.\n UPDATE public.tenants SET trial_ends_at = NULL WHERE id = NEW.tenant_id;\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING '[getting-started] signed_up not recorded for tenant %: %', NEW.tenant_id, SQLERRM;\n END;\n RETURN NULL;\nEND $$;\nREVOKE ALL ON FUNCTION public.plg_getting_started_on_binding() FROM PUBLIC, anon, authenticated;\n\nDROP TRIGGER IF EXISTS plg_getting_started_signed_up ON app.tenant_plugins;\nCREATE TRIGGER plg_getting_started_signed_up\n AFTER INSERT OR UPDATE OF status ON app.tenant_plugins\n FOR EACH ROW\n WHEN (NEW.plugin_id = 'getting-started' AND NEW.status = 'active')\n EXECUTE FUNCTION public.plg_getting_started_on_binding();\n\n-- \u2500\u2500 the handler accepts only what the trigger wrote (003's rule) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.plg_getting_started_on_signed_up(p_event jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;\nBEGIN\n IF v_tenant IS NULL THEN\n RETURN jsonb_build_object('skipped', 'no tenant');\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_getting_started_events e\n WHERE e.tenant_id = v_tenant AND e.kind = 'signed_up') THEN\n RETURN jsonb_build_object('skipped', 'signed_up was not written by the getting-started binding');\n END IF;\n RETURN public.plg_getting_started_lead(v_tenant, 'cadastro_v2', coalesce(p_event -> 'payload', '{}'::jsonb));\nEND $$;\nREVOKE ALL ON FUNCTION public.plg_getting_started_on_signed_up(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_getting_started_on_signed_up(jsonb) TO service_role;\n\nSELECT public.register_event_subscription('getting-started', 'getting_started.signed_up', 'plg_getting_started_on_signed_up',\n 'Faya lead when the workspace is created (cadastro V2)', 5);\nREVOKE ALL ON FUNCTION public.plg_getting_started_on_binding() FROM PUBLIC, anon, authenticated;\n";
|
|
8
9
|
export declare const MIGRATIONS: Array<{
|
|
9
10
|
id: string;
|
|
10
11
|
sql: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,qBAAqB,+uSAmJjC,CAAA;AAED,eAAO,MAAM,yBAAyB,gzlBAwTrC,CAAA;AAED,eAAO,MAAM,+BAA+B,y0fAgQ3C,CAAA;AAED,eAAO,MAAM,wDAAwD,8xVAwMpE,CAAA;AAED,eAAO,MAAM,iCAAiC,glHA4D7C,CAAA;AAED,eAAO,MAAM,wCAAwC,yzMA0GpD,CAAA;AAED,eAAO,MAAM,wCAAwC,6gbAqOpD,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,qBAAqB,+uSAmJjC,CAAA;AAED,eAAO,MAAM,yBAAyB,gzlBAwTrC,CAAA;AAED,eAAO,MAAM,+BAA+B,y0fAgQ3C,CAAA;AAED,eAAO,MAAM,wDAAwD,8xVAwMpE,CAAA;AAED,eAAO,MAAM,iCAAiC,glHA4D7C,CAAA;AAED,eAAO,MAAM,wCAAwC,yzMA0GpD,CAAA;AAED,eAAO,MAAM,wCAAwC,6gbAqOpD,CAAA;AAED,eAAO,MAAM,iDAAiD,0ucAqP7D,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CASzD,CAAA"}
|
|
@@ -6,8 +6,9 @@ var MIGRATION_003_THE_CLOCK_STARTS_ONCE_AND_ONLY_FOR_WHO_MAY = "";
|
|
|
6
6
|
var MIGRATION_004_LEAD_KEEPS_THE_NAME = "";
|
|
7
7
|
var MIGRATION_005_QA_LEADS_GO_TO_QA_PLATFORM = "";
|
|
8
8
|
var MIGRATION_006_THE_LEAD_IS_BORN_AT_SIGNUP = "";
|
|
9
|
+
var MIGRATION_007_THE_TRIAL_CLOCK_STARTS_AT_THE_CLICK = "";
|
|
9
10
|
var MIGRATIONS = [];
|
|
10
11
|
|
|
11
|
-
export { MIGRATIONS, MIGRATION_000_JOURNEY, MIGRATION_001_TRIAL_CLOCK, MIGRATION_002_PLATFORM_OPERATOR, MIGRATION_003_THE_CLOCK_STARTS_ONCE_AND_ONLY_FOR_WHO_MAY, MIGRATION_004_LEAD_KEEPS_THE_NAME, MIGRATION_005_QA_LEADS_GO_TO_QA_PLATFORM, MIGRATION_006_THE_LEAD_IS_BORN_AT_SIGNUP };
|
|
12
|
+
export { MIGRATIONS, MIGRATION_000_JOURNEY, MIGRATION_001_TRIAL_CLOCK, MIGRATION_002_PLATFORM_OPERATOR, MIGRATION_003_THE_CLOCK_STARTS_ONCE_AND_ONLY_FOR_WHO_MAY, MIGRATION_004_LEAD_KEEPS_THE_NAME, MIGRATION_005_QA_LEADS_GO_TO_QA_PLATFORM, MIGRATION_006_THE_LEAD_IS_BORN_AT_SIGNUP, MIGRATION_007_THE_TRIAL_CLOCK_STARTS_AT_THE_CLICK };
|
|
12
13
|
//# sourceMappingURL=main.browser.js.map
|
|
13
14
|
//# sourceMappingURL=main.browser.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../node_modules/.cache/fayz-migration-stubs/main.ts"],"names":[],"mappings":";AAEO,IAAM,qBAAA,GAAwB;AAC9B,IAAM,yBAAA,GAA4B;AAClC,IAAM,+BAAA,GAAkC;AACxC,IAAM,wDAAA,GAA2D;AACjE,IAAM,iCAAA,GAAoC;AAC1C,IAAM,wCAAA,GAA2C;AACjD,IAAM,wCAAA,GAA2C;AACjD,IAAM,aAAiD","file":"main.browser.js","sourcesContent":["// GENERATED by scripts/tsup-migrations.mjs — the browser build of a\n// migrations barrel: the same names, no SQL. See that script for why.\nexport const MIGRATION_000_JOURNEY = ''\nexport const MIGRATION_001_TRIAL_CLOCK = ''\nexport const MIGRATION_002_PLATFORM_OPERATOR = ''\nexport const MIGRATION_003_THE_CLOCK_STARTS_ONCE_AND_ONLY_FOR_WHO_MAY = ''\nexport const MIGRATION_004_LEAD_KEEPS_THE_NAME = ''\nexport const MIGRATION_005_QA_LEADS_GO_TO_QA_PLATFORM = ''\nexport const MIGRATION_006_THE_LEAD_IS_BORN_AT_SIGNUP = ''\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = []\n"]}
|
|
1
|
+
{"version":3,"sources":["../../node_modules/.cache/fayz-migration-stubs/main.ts"],"names":[],"mappings":";AAEO,IAAM,qBAAA,GAAwB;AAC9B,IAAM,yBAAA,GAA4B;AAClC,IAAM,+BAAA,GAAkC;AACxC,IAAM,wDAAA,GAA2D;AACjE,IAAM,iCAAA,GAAoC;AAC1C,IAAM,wCAAA,GAA2C;AACjD,IAAM,wCAAA,GAA2C;AACjD,IAAM,iDAAA,GAAoD;AAC1D,IAAM,aAAiD","file":"main.browser.js","sourcesContent":["// GENERATED by scripts/tsup-migrations.mjs — the browser build of a\n// migrations barrel: the same names, no SQL. See that script for why.\nexport const MIGRATION_000_JOURNEY = ''\nexport const MIGRATION_001_TRIAL_CLOCK = ''\nexport const MIGRATION_002_PLATFORM_OPERATOR = ''\nexport const MIGRATION_003_THE_CLOCK_STARTS_ONCE_AND_ONLY_FOR_WHO_MAY = ''\nexport const MIGRATION_004_LEAD_KEEPS_THE_NAME = ''\nexport const MIGRATION_005_QA_LEADS_GO_TO_QA_PLATFORM = ''\nexport const MIGRATION_006_THE_LEAD_IS_BORN_AT_SIGNUP = ''\nexport const MIGRATION_007_THE_TRIAL_CLOCK_STARTS_AT_THE_CLICK = ''\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = []\n"]}
|
package/dist/migrations/main.js
CHANGED
|
@@ -1316,6 +1316,252 @@ GRANT EXECUTE ON FUNCTION public.plg_getting_started_on_signed_up(jsonb) TO serv
|
|
|
1316
1316
|
SELECT public.register_event_subscription('getting-started', 'getting_started.signed_up', 'plg_getting_started_on_signed_up',
|
|
1317
1317
|
'Faya lead when the workspace is created (cadastro V2)', 5);
|
|
1318
1318
|
`;
|
|
1319
|
+
var MIGRATION_007_THE_TRIAL_CLOCK_STARTS_AT_THE_CLICK = `-- 007_the_trial_clock_starts_at_the_click.sql \u2014 the trial is 14 days counted from
|
|
1320
|
+
-- "Iniciar teste"; nothing counts before that. Depends on 000, 003 and 006.
|
|
1321
|
+
--
|
|
1322
|
+
-- * v_getting_started_journey: \`expired\` only after a trial that STARTED. A date
|
|
1323
|
+
-- left by the old signup clock (db 044) on a tenant still in setup no longer
|
|
1324
|
+
-- reads "Seu teste terminou" above "Para iniciar o teste". No setup deadline.
|
|
1325
|
+
-- * getting_started_start_trial: a tenant with no date can start \u2014 that is now
|
|
1326
|
+
-- every new tenant. Still once per tenant, never for converted or paying.
|
|
1327
|
+
-- * plg_getting_started_on_binding: a tenant born with this plugin drops the
|
|
1328
|
+
-- date the spine DEFAULT stamped. Apps without getting-started keep that
|
|
1329
|
+
-- DEFAULT (they have no "Iniciar teste" to start a clock).
|
|
1330
|
+
--
|
|
1331
|
+
-- Existing tenants still in setup keep the old date until the one-off cleanup in
|
|
1332
|
+
-- docs/checkpoints/FAY-1763-BACKEND-HANDOFF.md runs (it changes live accounts).
|
|
1333
|
+
|
|
1334
|
+
-- \u2500\u2500 the phase \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1335
|
+
CREATE OR REPLACE VIEW public.v_getting_started_journey WITH (security_invoker = true) AS
|
|
1336
|
+
SELECT t.id AS tenant_id,
|
|
1337
|
+
j.ready_at, j.trial_started_at, j.converted_at, j.wants_contract_at, j.source, j.utm,
|
|
1338
|
+
coalesce(j.trial_days, c.trial_days) AS trial_days,
|
|
1339
|
+
t.trial_ends_at,
|
|
1340
|
+
NULL::timestamptz AS setup_deadline_at,
|
|
1341
|
+
CASE
|
|
1342
|
+
WHEN j.converted_at IS NOT NULL THEN 'converted'
|
|
1343
|
+
WHEN j.trial_started_at IS NULL THEN 'setup'
|
|
1344
|
+
WHEN t.trial_ends_at IS NOT NULL AND t.trial_ends_at <= now() THEN 'expired'
|
|
1345
|
+
ELSE 'trial'
|
|
1346
|
+
END AS phase
|
|
1347
|
+
FROM public.tenants t
|
|
1348
|
+
CROSS JOIN public.plg_getting_started_config c
|
|
1349
|
+
LEFT JOIN public.plg_getting_started_journeys j ON j.tenant_id = t.id;
|
|
1350
|
+
GRANT SELECT ON public.v_getting_started_journey TO authenticated;
|
|
1351
|
+
|
|
1352
|
+
-- \u2500\u2500 the start: no date is the normal case now \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1353
|
+
CREATE OR REPLACE FUNCTION public.getting_started_start_trial(p_tenant uuid DEFAULT NULL)
|
|
1354
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$
|
|
1355
|
+
DECLARE
|
|
1356
|
+
v_tenant uuid := coalesce(p_tenant, app.current_tenant_id());
|
|
1357
|
+
v_days integer;
|
|
1358
|
+
v_min jsonb;
|
|
1359
|
+
v_t record;
|
|
1360
|
+
v_j record;
|
|
1361
|
+
v_started timestamptz;
|
|
1362
|
+
v_ends timestamptz;
|
|
1363
|
+
BEGIN
|
|
1364
|
+
-- Same gate as tenant_setting_set: a session, the tenant in scope, settings.manage.
|
|
1365
|
+
IF auth.uid() IS NULL OR v_tenant IS NULL OR v_tenant IS DISTINCT FROM app.current_tenant_id() THEN
|
|
1366
|
+
RAISE EXCEPTION 'getting_started_start_trial: no session or tenant out of scope' USING ERRCODE = '42501';
|
|
1367
|
+
END IF;
|
|
1368
|
+
IF NOT app.has_permission('settings.manage') THEN
|
|
1369
|
+
RAISE EXCEPTION 'getting_started_start_trial: settings.manage is required' USING ERRCODE = '42501';
|
|
1370
|
+
END IF;
|
|
1371
|
+
|
|
1372
|
+
-- The lock: a second concurrent call waits here and then sees the start.
|
|
1373
|
+
SELECT t.id, t.trial_ends_at, t.plan INTO v_t FROM public.tenants t WHERE t.id = v_tenant FOR UPDATE;
|
|
1374
|
+
IF NOT FOUND THEN
|
|
1375
|
+
RAISE EXCEPTION 'getting_started_start_trial: tenant not found' USING ERRCODE = 'P0002';
|
|
1376
|
+
END IF;
|
|
1377
|
+
SELECT j.trial_started_at, j.converted_at INTO v_j
|
|
1378
|
+
FROM public.plg_getting_started_journeys j WHERE j.tenant_id = v_tenant FOR UPDATE;
|
|
1379
|
+
|
|
1380
|
+
-- Idempotent: a second call returns the state and leaves the date alone.
|
|
1381
|
+
IF v_j.trial_started_at IS NOT NULL THEN
|
|
1382
|
+
RETURN jsonb_build_object('ok', true, 'already_started', true, 'trial_started_at', v_j.trial_started_at,
|
|
1383
|
+
'trial_ends_at', v_t.trial_ends_at);
|
|
1384
|
+
END IF;
|
|
1385
|
+
|
|
1386
|
+
v_min := public.getting_started_minimums(v_tenant);
|
|
1387
|
+
|
|
1388
|
+
-- Who is already converted, or already paying, is not offered a clock that
|
|
1389
|
+
-- would expire on them. A NULL trial_ends_at no longer means "no trial": since
|
|
1390
|
+
-- 007 every new tenant is born without a clock and gets it here.
|
|
1391
|
+
IF v_j.converted_at IS NOT NULL THEN
|
|
1392
|
+
RETURN jsonb_build_object('ok', false, 'reason', 'already_converted', 'minimums', v_min);
|
|
1393
|
+
END IF;
|
|
1394
|
+
IF coalesce(nullif(btrim(v_t.plan), ''), 'free') <> 'free' THEN
|
|
1395
|
+
RETURN jsonb_build_object('ok', false, 'reason', 'already_paid', 'minimums', v_min, 'plan', v_t.plan);
|
|
1396
|
+
END IF;
|
|
1397
|
+
|
|
1398
|
+
-- Minimums are checked here, not in the button.
|
|
1399
|
+
IF NOT coalesce((v_min ->> 'ready')::boolean, false) THEN
|
|
1400
|
+
RETURN jsonb_build_object('ok', false, 'reason', 'minimums_missing', 'minimums', v_min);
|
|
1401
|
+
END IF;
|
|
1402
|
+
|
|
1403
|
+
SELECT c.trial_days INTO v_days FROM public.plg_getting_started_config c;
|
|
1404
|
+
v_days := coalesce(v_days, 14);
|
|
1405
|
+
v_started := now();
|
|
1406
|
+
v_ends := v_started + make_interval(days => v_days);
|
|
1407
|
+
|
|
1408
|
+
INSERT INTO public.plg_getting_started_journeys (tenant_id, ready_at, trial_started_at, trial_days)
|
|
1409
|
+
VALUES (v_tenant, v_started, v_started, v_days)
|
|
1410
|
+
ON CONFLICT (tenant_id) DO UPDATE
|
|
1411
|
+
SET ready_at = coalesce(public.plg_getting_started_journeys.ready_at, EXCLUDED.ready_at),
|
|
1412
|
+
trial_started_at = EXCLUDED.trial_started_at,
|
|
1413
|
+
trial_days = EXCLUDED.trial_days,
|
|
1414
|
+
updated_at = now();
|
|
1415
|
+
|
|
1416
|
+
-- The real clock. Passes tenants_trial_is_readonly (db 221): this DEFINER
|
|
1417
|
+
-- function runs as its owner, which the gate lets through.
|
|
1418
|
+
UPDATE public.tenants SET trial_ends_at = v_ends WHERE id = v_tenant;
|
|
1419
|
+
|
|
1420
|
+
INSERT INTO public.plg_getting_started_events (tenant_id, kind, actor_id, payload)
|
|
1421
|
+
VALUES (v_tenant, 'trial_started', auth.uid(), jsonb_build_object('trial_days', v_days, 'ends_at', v_ends))
|
|
1422
|
+
ON CONFLICT (tenant_id, kind, day) DO NOTHING;
|
|
1423
|
+
|
|
1424
|
+
PERFORM public.plg_emit_event('getting_started.trial_started',
|
|
1425
|
+
jsonb_build_object('trial_days', v_days, 'trial_ends_at', v_ends), 'tenant', v_tenant::text, v_tenant);
|
|
1426
|
+
|
|
1427
|
+
RETURN jsonb_build_object('ok', true, 'trial_started_at', v_started, 'trial_ends_at', v_ends, 'trial_days', v_days);
|
|
1428
|
+
END $$;
|
|
1429
|
+
REVOKE ALL ON FUNCTION public.getting_started_start_trial(uuid) FROM PUBLIC;
|
|
1430
|
+
GRANT EXECUTE ON FUNCTION public.getting_started_start_trial(uuid) TO authenticated, service_role;
|
|
1431
|
+
|
|
1432
|
+
-- \u2500\u2500 the platform config: the client reads only what it needs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1433
|
+
-- The view (000) reads trial_days; the setup window is documentation of the
|
|
1434
|
+
-- spine default. crm_tenant_slug and reminder_hour_utc are read inside the
|
|
1435
|
+
-- DEFINER functions only. The policy stays (USING true); the GRANT narrows.
|
|
1436
|
+
REVOKE SELECT ON public.plg_getting_started_config FROM authenticated;
|
|
1437
|
+
GRANT SELECT (id, trial_days, setup_window_days) ON public.plg_getting_started_config TO authenticated;
|
|
1438
|
+
|
|
1439
|
+
-- \u2500\u2500 the handlers accept only what the RPCs wrote \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1440
|
+
-- plg_emit_event is open to any member of the tenant; the journey row and the
|
|
1441
|
+
-- timeline are written by DEFINER RPCs only (no write policy). So a lead is
|
|
1442
|
+
-- created only when the mark the RPC leaves is there.
|
|
1443
|
+
CREATE OR REPLACE FUNCTION public.plg_getting_started_on_trial_started(p_event jsonb)
|
|
1444
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$
|
|
1445
|
+
DECLARE
|
|
1446
|
+
v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;
|
|
1447
|
+
BEGIN
|
|
1448
|
+
IF v_tenant IS NULL THEN
|
|
1449
|
+
RETURN jsonb_build_object('skipped', 'no tenant');
|
|
1450
|
+
END IF;
|
|
1451
|
+
IF NOT EXISTS (
|
|
1452
|
+
SELECT 1 FROM public.plg_getting_started_journeys j
|
|
1453
|
+
WHERE j.tenant_id = v_tenant AND j.trial_started_at IS NOT NULL)
|
|
1454
|
+
OR NOT EXISTS (
|
|
1455
|
+
SELECT 1 FROM public.plg_getting_started_events e
|
|
1456
|
+
WHERE e.tenant_id = v_tenant AND e.kind = 'trial_started') THEN
|
|
1457
|
+
RETURN jsonb_build_object('skipped', 'trial_started was not written by getting_started_start_trial');
|
|
1458
|
+
END IF;
|
|
1459
|
+
RETURN public.plg_getting_started_lead(v_tenant, 'trial_started', coalesce(p_event -> 'payload', '{}'::jsonb));
|
|
1460
|
+
END $$;
|
|
1461
|
+
|
|
1462
|
+
CREATE OR REPLACE FUNCTION public.plg_getting_started_on_wants_contract(p_event jsonb)
|
|
1463
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$
|
|
1464
|
+
DECLARE
|
|
1465
|
+
v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;
|
|
1466
|
+
BEGIN
|
|
1467
|
+
IF v_tenant IS NULL THEN
|
|
1468
|
+
RETURN jsonb_build_object('skipped', 'no tenant');
|
|
1469
|
+
END IF;
|
|
1470
|
+
IF NOT EXISTS (
|
|
1471
|
+
SELECT 1 FROM public.plg_getting_started_journeys j
|
|
1472
|
+
WHERE j.tenant_id = v_tenant AND j.wants_contract_at IS NOT NULL)
|
|
1473
|
+
OR NOT EXISTS (
|
|
1474
|
+
SELECT 1 FROM public.plg_getting_started_events e
|
|
1475
|
+
WHERE e.tenant_id = v_tenant AND e.kind = 'wants_contract'
|
|
1476
|
+
AND e.created_at >= now() - interval '2 days') THEN
|
|
1477
|
+
RETURN jsonb_build_object('skipped', 'wants_contract was not written by getting_started_request_contract');
|
|
1478
|
+
END IF;
|
|
1479
|
+
RETURN public.plg_getting_started_lead(v_tenant, 'quer_contratar', coalesce(p_event -> 'payload', '{}'::jsonb));
|
|
1480
|
+
END $$;
|
|
1481
|
+
REVOKE ALL ON FUNCTION public.plg_getting_started_on_trial_started(jsonb) FROM PUBLIC, anon, authenticated;
|
|
1482
|
+
REVOKE ALL ON FUNCTION public.plg_getting_started_on_wants_contract(jsonb) FROM PUBLIC, anon, authenticated;
|
|
1483
|
+
GRANT EXECUTE ON FUNCTION public.plg_getting_started_on_trial_started(jsonb) TO service_role;
|
|
1484
|
+
GRANT EXECUTE ON FUNCTION public.plg_getting_started_on_wants_contract(jsonb) TO service_role;
|
|
1485
|
+
REVOKE ALL ON FUNCTION public.getting_started_start_trial(uuid) FROM PUBLIC;
|
|
1486
|
+
GRANT EXECUTE ON FUNCTION public.getting_started_start_trial(uuid) TO authenticated, service_role;
|
|
1487
|
+
|
|
1488
|
+
-- \u2500\u2500 the birth: drop the signup date \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1489
|
+
CREATE OR REPLACE FUNCTION public.plg_getting_started_on_binding()
|
|
1490
|
+
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$
|
|
1491
|
+
DECLARE
|
|
1492
|
+
v_t record;
|
|
1493
|
+
v_j record;
|
|
1494
|
+
v_payload jsonb;
|
|
1495
|
+
BEGIN
|
|
1496
|
+
IF NEW.plugin_id <> 'getting-started' OR NEW.facet <> '' OR NEW.status <> 'active' THEN RETURN NULL; END IF;
|
|
1497
|
+
IF TG_OP = 'UPDATE' AND OLD.status = 'active' THEN RETURN NULL; END IF;
|
|
1498
|
+
-- Never fails the signup: an account created without its lead is recoverable, a lost signup is not.
|
|
1499
|
+
BEGIN
|
|
1500
|
+
-- Once per tenant, ever: switching the module off and on is not a second signup.
|
|
1501
|
+
IF EXISTS (SELECT 1 FROM public.plg_getting_started_events e
|
|
1502
|
+
WHERE e.tenant_id = NEW.tenant_id AND e.kind = 'signed_up') THEN
|
|
1503
|
+
RETURN NULL;
|
|
1504
|
+
END IF;
|
|
1505
|
+
SELECT t.plan, t.trial_ends_at, t.created_at INTO v_t FROM public.tenants t WHERE t.id = NEW.tenant_id;
|
|
1506
|
+
IF NOT FOUND
|
|
1507
|
+
OR coalesce(nullif(btrim(v_t.plan), ''), 'free') <> 'free'
|
|
1508
|
+
OR v_t.trial_ends_at IS NULL
|
|
1509
|
+
OR v_t.created_at < now() - interval '48 hours' THEN
|
|
1510
|
+
RETURN NULL;
|
|
1511
|
+
END IF;
|
|
1512
|
+
SELECT j.trial_started_at, j.converted_at INTO v_j
|
|
1513
|
+
FROM public.plg_getting_started_journeys j WHERE j.tenant_id = NEW.tenant_id;
|
|
1514
|
+
IF v_j.trial_started_at IS NOT NULL OR v_j.converted_at IS NOT NULL THEN
|
|
1515
|
+
RETURN NULL;
|
|
1516
|
+
END IF;
|
|
1517
|
+
|
|
1518
|
+
v_payload := jsonb_build_object('source', 'cadastro_v2', 'binding_source', NEW.source);
|
|
1519
|
+
INSERT INTO public.plg_getting_started_events (tenant_id, kind, actor_id, payload)
|
|
1520
|
+
VALUES (NEW.tenant_id, 'signed_up', auth.uid(), v_payload)
|
|
1521
|
+
ON CONFLICT (tenant_id, kind, day) DO NOTHING;
|
|
1522
|
+
-- plg_emit_event refuses a caller who is not yet a member, and inside create_tenant the
|
|
1523
|
+
-- membership comes after the binding: file the fact directly (002's helper).
|
|
1524
|
+
PERFORM public.plg_getting_started_platform_emit(NEW.tenant_id, 'getting_started.signed_up', v_payload);
|
|
1525
|
+
-- The spine DEFAULT (db 044) stamped a 30-day date at signup; the trial is 14
|
|
1526
|
+
-- days from "Iniciar teste", so this tenant starts with no clock at all.
|
|
1527
|
+
UPDATE public.tenants SET trial_ends_at = NULL WHERE id = NEW.tenant_id;
|
|
1528
|
+
EXCEPTION WHEN OTHERS THEN
|
|
1529
|
+
RAISE WARNING '[getting-started] signed_up not recorded for tenant %: %', NEW.tenant_id, SQLERRM;
|
|
1530
|
+
END;
|
|
1531
|
+
RETURN NULL;
|
|
1532
|
+
END $$;
|
|
1533
|
+
REVOKE ALL ON FUNCTION public.plg_getting_started_on_binding() FROM PUBLIC, anon, authenticated;
|
|
1534
|
+
|
|
1535
|
+
DROP TRIGGER IF EXISTS plg_getting_started_signed_up ON app.tenant_plugins;
|
|
1536
|
+
CREATE TRIGGER plg_getting_started_signed_up
|
|
1537
|
+
AFTER INSERT OR UPDATE OF status ON app.tenant_plugins
|
|
1538
|
+
FOR EACH ROW
|
|
1539
|
+
WHEN (NEW.plugin_id = 'getting-started' AND NEW.status = 'active')
|
|
1540
|
+
EXECUTE FUNCTION public.plg_getting_started_on_binding();
|
|
1541
|
+
|
|
1542
|
+
-- \u2500\u2500 the handler accepts only what the trigger wrote (003's rule) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1543
|
+
CREATE OR REPLACE FUNCTION public.plg_getting_started_on_signed_up(p_event jsonb)
|
|
1544
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$
|
|
1545
|
+
DECLARE
|
|
1546
|
+
v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;
|
|
1547
|
+
BEGIN
|
|
1548
|
+
IF v_tenant IS NULL THEN
|
|
1549
|
+
RETURN jsonb_build_object('skipped', 'no tenant');
|
|
1550
|
+
END IF;
|
|
1551
|
+
IF NOT EXISTS (
|
|
1552
|
+
SELECT 1 FROM public.plg_getting_started_events e
|
|
1553
|
+
WHERE e.tenant_id = v_tenant AND e.kind = 'signed_up') THEN
|
|
1554
|
+
RETURN jsonb_build_object('skipped', 'signed_up was not written by the getting-started binding');
|
|
1555
|
+
END IF;
|
|
1556
|
+
RETURN public.plg_getting_started_lead(v_tenant, 'cadastro_v2', coalesce(p_event -> 'payload', '{}'::jsonb));
|
|
1557
|
+
END $$;
|
|
1558
|
+
REVOKE ALL ON FUNCTION public.plg_getting_started_on_signed_up(jsonb) FROM PUBLIC, anon, authenticated;
|
|
1559
|
+
GRANT EXECUTE ON FUNCTION public.plg_getting_started_on_signed_up(jsonb) TO service_role;
|
|
1560
|
+
|
|
1561
|
+
SELECT public.register_event_subscription('getting-started', 'getting_started.signed_up', 'plg_getting_started_on_signed_up',
|
|
1562
|
+
'Faya lead when the workspace is created (cadastro V2)', 5);
|
|
1563
|
+
REVOKE ALL ON FUNCTION public.plg_getting_started_on_binding() FROM PUBLIC, anon, authenticated;
|
|
1564
|
+
`;
|
|
1319
1565
|
var MIGRATIONS = [
|
|
1320
1566
|
{ id: "000_journey", sql: MIGRATION_000_JOURNEY },
|
|
1321
1567
|
{ id: "001_trial_clock", sql: MIGRATION_001_TRIAL_CLOCK },
|
|
@@ -1323,9 +1569,10 @@ var MIGRATIONS = [
|
|
|
1323
1569
|
{ id: "003_the_clock_starts_once_and_only_for_who_may", sql: MIGRATION_003_THE_CLOCK_STARTS_ONCE_AND_ONLY_FOR_WHO_MAY },
|
|
1324
1570
|
{ id: "004_lead_keeps_the_name", sql: MIGRATION_004_LEAD_KEEPS_THE_NAME },
|
|
1325
1571
|
{ id: "005_qa_leads_go_to_qa_platform", sql: MIGRATION_005_QA_LEADS_GO_TO_QA_PLATFORM },
|
|
1326
|
-
{ id: "006_the_lead_is_born_at_signup", sql: MIGRATION_006_THE_LEAD_IS_BORN_AT_SIGNUP }
|
|
1572
|
+
{ id: "006_the_lead_is_born_at_signup", sql: MIGRATION_006_THE_LEAD_IS_BORN_AT_SIGNUP },
|
|
1573
|
+
{ id: "007_the_trial_clock_starts_at_the_click", sql: MIGRATION_007_THE_TRIAL_CLOCK_STARTS_AT_THE_CLICK }
|
|
1327
1574
|
];
|
|
1328
1575
|
|
|
1329
|
-
export { MIGRATIONS, MIGRATION_000_JOURNEY, MIGRATION_001_TRIAL_CLOCK, MIGRATION_002_PLATFORM_OPERATOR, MIGRATION_003_THE_CLOCK_STARTS_ONCE_AND_ONLY_FOR_WHO_MAY, MIGRATION_004_LEAD_KEEPS_THE_NAME, MIGRATION_005_QA_LEADS_GO_TO_QA_PLATFORM, MIGRATION_006_THE_LEAD_IS_BORN_AT_SIGNUP };
|
|
1576
|
+
export { MIGRATIONS, MIGRATION_000_JOURNEY, MIGRATION_001_TRIAL_CLOCK, MIGRATION_002_PLATFORM_OPERATOR, MIGRATION_003_THE_CLOCK_STARTS_ONCE_AND_ONLY_FOR_WHO_MAY, MIGRATION_004_LEAD_KEEPS_THE_NAME, MIGRATION_005_QA_LEADS_GO_TO_QA_PLATFORM, MIGRATION_006_THE_LEAD_IS_BORN_AT_SIGNUP, MIGRATION_007_THE_TRIAL_CLOCK_STARTS_AT_THE_CLICK };
|
|
1330
1577
|
//# sourceMappingURL=main.js.map
|
|
1331
1578
|
//# sourceMappingURL=main.js.map
|