@fayz-ai/plugin-getting-started 0.0.0-stage → 0.2.0-canary.20261027164140
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/LICENSE +21 -0
- package/dist/chunk-QAYRR3A4.js +2964 -0
- package/dist/chunk-QAYRR3A4.js.map +1 -0
- package/dist/components/ProgressRing.d.ts +16 -0
- package/dist/components/ProgressRing.d.ts.map +1 -0
- package/dist/components/StartTrialAction.d.ts +70 -0
- package/dist/components/StartTrialAction.d.ts.map +1 -0
- package/dist/components/StartTrialCta.d.ts +12 -0
- package/dist/components/StartTrialCta.d.ts.map +1 -0
- package/dist/components/StoreLiveCard.d.ts +21 -0
- package/dist/components/StoreLiveCard.d.ts.map +1 -0
- package/dist/components/TrialEndBanner.d.ts +12 -0
- package/dist/components/TrialEndBanner.d.ts.map +1 -0
- package/dist/config.d.ts +82 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/context.d.ts +33 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/mock.d.ts +27 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +10 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/tables.d.ts +15 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/types.d.ts +24 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/host.d.ts +93 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +78 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +138 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/agent-tools.d.ts +143 -0
- package/dist/lib/agent-tools.d.ts.map +1 -0
- package/dist/lib/journey-steps.d.ts +152 -0
- package/dist/lib/journey-steps.d.ts.map +1 -0
- package/dist/lib/program.d.ts +37 -0
- package/dist/lib/program.d.ts.map +1 -0
- package/dist/lib/store-profile.d.ts +75 -0
- package/dist/lib/store-profile.d.ts.map +1 -0
- package/dist/lib/store-settings.d.ts +70 -0
- package/dist/lib/store-settings.d.ts.map +1 -0
- package/dist/lib/tour.d.ts +86 -0
- package/dist/lib/tour.d.ts.map +1 -0
- package/dist/lib/trial-clock.d.ts +14 -0
- package/dist/lib/trial-clock.d.ts.map +1 -0
- package/dist/lib/trial-source-bridge.d.ts +17 -0
- package/dist/lib/trial-source-bridge.d.ts.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +10 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/main.browser.js +11 -0
- package/dist/migrations/main.browser.js.map +1 -0
- package/dist/migrations/main.js +992 -0
- package/dist/migrations/main.js.map +1 -0
- package/dist/settings/TrialSettingsTab.d.ts +23 -0
- package/dist/settings/TrialSettingsTab.d.ts.map +1 -0
- package/dist/store.d.ts +23 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +74 -3
- package/README.md +0 -3
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Answer keys other readers depend on (plugin-orders' delivery step, the handbook). */
|
|
2
|
+
export declare const PROFILE_KEYS: {
|
|
3
|
+
readonly operation: "food.operation";
|
|
4
|
+
readonly serviceModes: "food.serviceModes";
|
|
5
|
+
readonly deliveryMethod: "food.deliveryMethod";
|
|
6
|
+
readonly tables: "food.tables";
|
|
7
|
+
readonly marketplaceApps: "food.marketplaceApps";
|
|
8
|
+
readonly welcomeArticle: "workspace.welcomeArticle";
|
|
9
|
+
};
|
|
10
|
+
export declare const OPERATIONS: readonly ["restaurant", "snack-bar", "pizzeria", "cafe", "bar", "dark-kitchen", "food-truck", "bakery", "other"];
|
|
11
|
+
export declare const SERVICE_MODES: readonly ["counter", "own-delivery", "dine-in", "aggregator"];
|
|
12
|
+
export declare const DELIVERY_METHODS: readonly ["radius", "neighborhood"];
|
|
13
|
+
export declare const TABLE_BANDS: readonly ["lt10", "10-25", "25-50", "gt50"];
|
|
14
|
+
export declare const WELCOME_ARTICLES: readonly ["a", "o"];
|
|
15
|
+
export type StoreOperation = (typeof OPERATIONS)[number];
|
|
16
|
+
export type StoreServiceMode = (typeof SERVICE_MODES)[number];
|
|
17
|
+
export type StoreDeliveryMethod = (typeof DELIVERY_METHODS)[number];
|
|
18
|
+
export type StoreTableBand = (typeof TABLE_BANDS)[number];
|
|
19
|
+
/** The step key of the profile step in the store-live program. */
|
|
20
|
+
export declare const STORE_PROFILE_STEP_KEY = "profile";
|
|
21
|
+
/** What the profile step asks and the `saveStoreProfile` tool writes. Every field optional: a save is a patch. */
|
|
22
|
+
export interface StoreProfile {
|
|
23
|
+
/** The kind of place. */
|
|
24
|
+
operation?: StoreOperation;
|
|
25
|
+
/** How people order. The step is done once this has at least one mode. */
|
|
26
|
+
serviceModes?: StoreServiceMode[];
|
|
27
|
+
/** How delivery is charged. */
|
|
28
|
+
deliveryMethod?: StoreDeliveryMethod;
|
|
29
|
+
/** How many tables (band). */
|
|
30
|
+
tables?: StoreTableBand;
|
|
31
|
+
/** Delivery apps the orders arrive through, lower-case ids (`ifood`, `rappi`…). */
|
|
32
|
+
marketplaceApps?: string[];
|
|
33
|
+
/** The article of the greeting ("a Cantina", "o Bar"). */
|
|
34
|
+
welcomeArticle?: (typeof WELCOME_ARTICLES)[number];
|
|
35
|
+
}
|
|
36
|
+
export type SaveStoreProfileWriteResult = {
|
|
37
|
+
ok: true;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
reason: 'no-user' | 'offline' | 'write-failed';
|
|
41
|
+
message?: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Where the store profile lives. The platform default reads and writes the
|
|
45
|
+
* business-profile store (`plg_onboarding_responses`); a host installs its own
|
|
46
|
+
* through `createGettingStartedHost({ storeProfile })`. Both methods resolve —
|
|
47
|
+
* a failure is `null` / `{ ok: false }`, never a throw.
|
|
48
|
+
*/
|
|
49
|
+
export interface StoreProfileProvider {
|
|
50
|
+
/** The tenant's profile, `null` when never answered or unknown. */
|
|
51
|
+
read(tenantId: string): Promise<StoreProfile | null>;
|
|
52
|
+
/** Merges `patch` into the tenant's profile. `businessName` is the org's name, for providers that keep it. */
|
|
53
|
+
save(tenantId: string, patch: StoreProfile, context: {
|
|
54
|
+
businessName?: string;
|
|
55
|
+
}): Promise<SaveStoreProfileWriteResult>;
|
|
56
|
+
}
|
|
57
|
+
export declare function someOf<T extends string>(value: unknown, allowed: readonly T[]): T[];
|
|
58
|
+
/** The fields that are answers from the catalogue; unknown values are dropped. */
|
|
59
|
+
export declare function storeProfileFrom(source: Record<string, unknown> | null | undefined): StoreProfile;
|
|
60
|
+
/** A profile as the wizard keyed its answers (`food.serviceModes`, …). */
|
|
61
|
+
export declare function storeProfileToAnswers(profile: StoreProfile): Record<string, unknown>;
|
|
62
|
+
/** The wizard's answers read back as a profile. */
|
|
63
|
+
export declare function storeProfileFromAnswers(answers: Record<string, unknown> | null | undefined): StoreProfile;
|
|
64
|
+
/** The profile step closes when the house said how it serves. */
|
|
65
|
+
export declare function storeProfileAnswered(profile: StoreProfile | null | undefined): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The platform's store profile: the business-profile store, which paints the
|
|
68
|
+
* answers in memory first (every `appliesWhen` re-reads at once) and then
|
|
69
|
+
* writes a new `plg_onboarding_responses` row.
|
|
70
|
+
*/
|
|
71
|
+
export declare const businessProfileStoreProfileProvider: StoreProfileProvider;
|
|
72
|
+
/** Installs the host's store profile; `null` restores the platform default. */
|
|
73
|
+
export declare function configureStoreProfile(provider: StoreProfileProvider | null): void;
|
|
74
|
+
export declare function getStoreProfileProvider(): StoreProfileProvider;
|
|
75
|
+
//# sourceMappingURL=store-profile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-profile.d.ts","sourceRoot":"","sources":["../../src/lib/store-profile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type PaymentMethod, type StoreHours } from '../types';
|
|
2
|
+
export type StoreSettingKey = 'hours' | 'payment_methods';
|
|
3
|
+
/** Local fact the shell's activation sweep wakes on (any domain event does). */
|
|
4
|
+
export declare const STORE_SETTINGS_SAVED_EVENT = "getting_started.store_settings_saved";
|
|
5
|
+
export declare function asHours(value: unknown): StoreHours;
|
|
6
|
+
/** The UI kit's vocabulary (the wizard's ids); legacy ids map, unknown ones drop. */
|
|
7
|
+
export declare function asMethods(value: unknown): PaymentMethod[];
|
|
8
|
+
/**
|
|
9
|
+
* `tenants.settings.store.*` as the workspace in memory has it — the same row
|
|
10
|
+
* the wizard wrote at birth and the sensors read (tech design, decision 5).
|
|
11
|
+
*/
|
|
12
|
+
export declare function readStoreSettings(settings?: Record<string, unknown> | undefined): {
|
|
13
|
+
hours: StoreHours;
|
|
14
|
+
methods: PaymentMethod[];
|
|
15
|
+
};
|
|
16
|
+
/** Repaints the workspace in memory and raises the local fact the sweep listens to. */
|
|
17
|
+
export declare function reflectInWorkspace(key: StoreSettingKey, value: unknown): void;
|
|
18
|
+
export type SaveStoreSettingVia = 'rpc' | 'row' | 'provider';
|
|
19
|
+
export type SaveStoreSettingResult = {
|
|
20
|
+
ok: true;
|
|
21
|
+
via: SaveStoreSettingVia;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
reason: 'offline' | 'no-tenant' | 'error';
|
|
25
|
+
message?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Where the store's own settings (hours, payment methods) live. The default
|
|
29
|
+
* writes `tenants.settings.store` on the platform; a host with its own schema
|
|
30
|
+
* installs one with `configureStoreSettings`. Both methods resolve — a failure
|
|
31
|
+
* is `{ ok: false }` / `undefined`, never a throw.
|
|
32
|
+
*/
|
|
33
|
+
export interface StoreSettingsProvider {
|
|
34
|
+
/** The tenant's `store` branch (`{ hours, payment_methods }`), `undefined` when unknown. */
|
|
35
|
+
read(tenantId: string): Promise<Record<string, unknown> | undefined>;
|
|
36
|
+
save(tenantId: string, key: StoreSettingKey, value: unknown): Promise<SaveStoreSettingResult>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The platform: `tenant_setting_set` the way the settings registry wants it,
|
|
40
|
+
* falling back to the row when the registry does not know the key yet.
|
|
41
|
+
*
|
|
42
|
+
* Both paths land in the same place — `tenant_setting_set` is an UPDATE on
|
|
43
|
+
* `tenants.settings` — so the fallback is not a second store, only a second
|
|
44
|
+
* door to the same one, for the pool that has not applied the plugin's
|
|
45
|
+
* migration.
|
|
46
|
+
*/
|
|
47
|
+
export declare const supabaseStoreSettingsProvider: StoreSettingsProvider;
|
|
48
|
+
/** `null` restores the platform default. */
|
|
49
|
+
export declare function configureStoreSettings(provider: StoreSettingsProvider | null): void;
|
|
50
|
+
export declare function getStoreSettingsProvider(): StoreSettingsProvider;
|
|
51
|
+
/**
|
|
52
|
+
* Writes `store.<key>` through the provider. The in-memory workspace is
|
|
53
|
+
* repainted first either way: the checklist closes the step the moment the
|
|
54
|
+
* owner saves, not on the next reload.
|
|
55
|
+
*/
|
|
56
|
+
export declare function saveStoreSetting(key: StoreSettingKey, value: unknown): Promise<SaveStoreSettingResult>;
|
|
57
|
+
/**
|
|
58
|
+
* `tenants.settings` (as the sensors read it) for the active tenant: the
|
|
59
|
+
* workspace in memory when it already carries `store`, the provider when it
|
|
60
|
+
* does not.
|
|
61
|
+
*
|
|
62
|
+
* The org store does paint `settings`, but a fresh workspace reaches the first
|
|
63
|
+
* sweep by a path that may not have it yet (the org handed back by the wizard,
|
|
64
|
+
* a switch mid-load, a host that paints no settings at all). A sensor that
|
|
65
|
+
* answers "no" from a memory that was merely empty leaves the step open until
|
|
66
|
+
* some other event happens to re-run it — so when memory has no `store`, the
|
|
67
|
+
* provider is asked, once per ten seconds per tenant.
|
|
68
|
+
*/
|
|
69
|
+
export declare function loadStoreSettings(): Promise<Record<string, unknown> | undefined>;
|
|
70
|
+
//# sourceMappingURL=store-settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-settings.d.ts","sourceRoot":"","sources":["../../src/lib/store-settings.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6B,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAA;AAEzF,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,iBAAiB,CAAA;AAEzD,gFAAgF;AAChF,eAAO,MAAM,0BAA0B,yCAAyC,CAAA;AAEhF,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAYlD;AAED,qFAAqF;AACrF,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,EAAE,CAEzD;AAOD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAgE,GAAG;IACvI,KAAK,EAAE,UAAU,CAAA;IACjB,OAAO,EAAE,aAAa,EAAE,CAAA;CACzB,CAGA;AAED,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAM7E;AAED,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,KAAK,GAAG,UAAU,CAAA;AAE5D,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,mBAAmB,CAAA;CAAE,GACtC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9E;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,4FAA4F;IAC5F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAAA;IACpE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAA;CAC9F;AAUD;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,EAAE,qBAyB3C,CAAA;AAID,4CAA4C;AAC5C,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,GAAG,IAAI,CAGnF;AAED,wBAAgB,wBAAwB,IAAI,qBAAqB,CAEhE;AAMD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAa5G;AAWD;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAyBtF"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type OnboardingTourDef, type PluginRuntime, type TourDef, type TourStopDef } from '@fayz-ai/core';
|
|
3
|
+
import { type TourActionResult } from '@fayz-ai/ui';
|
|
4
|
+
import { type GettingStartedRoutes } from '../config';
|
|
5
|
+
import type { JourneyState } from '../store';
|
|
6
|
+
/**
|
|
7
|
+
* The closing tour of the store-live journey: a walk through the data the
|
|
8
|
+
* house just created, in the screens that hold it. Every plugin contributes
|
|
9
|
+
* its own stops (`onboarding.tour` in its manifest); this module only orders
|
|
10
|
+
* and composes — it opens on the journey card and closes on the trial tab.
|
|
11
|
+
*/
|
|
12
|
+
export declare const JOURNEY_TOUR_ID = "getting-started.journey";
|
|
13
|
+
export declare const CARD_ANCHOR = "getting-started.card";
|
|
14
|
+
export declare const TRIAL_TAB_ANCHOR = "getting-started.trial-tab";
|
|
15
|
+
export declare const DESCRIBE_CARD_ACTION = "getting-started.describe-card";
|
|
16
|
+
export declare const DESCRIBE_TRIAL_TAB_ACTION = "getting-started.describe-trial-tab";
|
|
17
|
+
export declare const TRIAL_STARTED_EVENT = "getting_started.trial_started";
|
|
18
|
+
export declare const MINIMUMS_READY_EVENT = "getting_started.minimums_ready";
|
|
19
|
+
export declare const TOUR_DONE_EVENT = "getting_started.tour_done";
|
|
20
|
+
/** How long a start waits for the dialog or takeover that fired it to close. */
|
|
21
|
+
export declare const TRIAL_START_SETTLE_MS = 600;
|
|
22
|
+
export type JourneyTourMoment = 'trial_started' | 'minimums_ready' | 'manual';
|
|
23
|
+
/**
|
|
24
|
+
* This plugin's own contribution: the card, first, on the app's home route.
|
|
25
|
+
* Goes in the manifest as `onboarding.tour`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildOwnJourneyTour(routes?: Pick<GettingStartedRoutes, 'home'>): OnboardingTourDef;
|
|
28
|
+
/** The contribution on the default routes. */
|
|
29
|
+
export declare const journeyTour: OnboardingTourDef;
|
|
30
|
+
/** The last stop, always: where the clock, the hours and the payment methods live (the app's trial route). */
|
|
31
|
+
export declare function buildJourneyClosingStop(routes?: Pick<GettingStartedRoutes, 'trial'>): TourStopDef;
|
|
32
|
+
/** The closing stop on the default routes. */
|
|
33
|
+
export declare const journeyClosingStop: TourStopDef;
|
|
34
|
+
/**
|
|
35
|
+
* Card → the contributed stops, by their `order` → trial tab. A contribution
|
|
36
|
+
* that asked for `manual` is left out (it wants to be shown on its own); every
|
|
37
|
+
* stop is skippable unless it said otherwise, so a module that is off in this
|
|
38
|
+
* app costs the person nothing.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildJourneyTour(runtime?: Pick<PluginRuntime, 'onboardingTours'> | null, routes?: Pick<Required<GettingStartedRoutes>, 'home' | 'trial'>): TourDef;
|
|
41
|
+
export declare function journeyTourDone(tenantId: string | null | undefined): boolean;
|
|
42
|
+
export declare function markJourneyTourDone(tenantId: string): void;
|
|
43
|
+
/** Tests and support: lets the tour fire again for a tenant. */
|
|
44
|
+
export declare function forgetJourneyTour(tenantId: string): void;
|
|
45
|
+
export interface JourneyTourState {
|
|
46
|
+
/** A start that is waiting for the right screen (the card, uncovered). */
|
|
47
|
+
pending: JourneyTourMoment | null;
|
|
48
|
+
runtime: Pick<PluginRuntime, 'onboardingTours'> | null;
|
|
49
|
+
rememberRuntime: (runtime: Pick<PluginRuntime, 'onboardingTours'> | null) => void;
|
|
50
|
+
}
|
|
51
|
+
export declare const useJourneyTourStore: import("zustand").UseBoundStore<import("zustand").StoreApi<JourneyTourState>>;
|
|
52
|
+
/**
|
|
53
|
+
* Starts the tour now. Without `force`, only once per tenant and never over a
|
|
54
|
+
* tour already running (the pill and the app's screen may both react to the
|
|
55
|
+
* same event — the second caller finds the flag set). Returns whether it ran.
|
|
56
|
+
*/
|
|
57
|
+
export declare function startJourneyTour(opts?: {
|
|
58
|
+
moment?: JourneyTourMoment;
|
|
59
|
+
force?: boolean;
|
|
60
|
+
runtime?: Pick<PluginRuntime, 'onboardingTours'> | null;
|
|
61
|
+
}): boolean;
|
|
62
|
+
/** Lets the dialog or takeover that fired the event close before the overlay opens. */
|
|
63
|
+
export declare function startJourneyTourSoon(moment: JourneyTourMoment, delayMs?: number): void;
|
|
64
|
+
/**
|
|
65
|
+
* Wires the two moments. Idempotent: the plugin factory calls it on every
|
|
66
|
+
* bind, and a second install replaces the first.
|
|
67
|
+
*/
|
|
68
|
+
export declare function installJourneyTourTriggers(store: {
|
|
69
|
+
getState: () => JourneyState;
|
|
70
|
+
subscribe: (fn: () => void) => () => void;
|
|
71
|
+
}): () => void;
|
|
72
|
+
/** Hands the plugin runtime to the triggers, which run outside React. */
|
|
73
|
+
export declare function useRememberJourneyRuntime(): Pick<PluginRuntime, 'onboardingTours'> | null;
|
|
74
|
+
/**
|
|
75
|
+
* Registers the stop's action as "describe me": the copy is written at the
|
|
76
|
+
* moment the stop shows, over whatever the surface knows then — not when the
|
|
77
|
+
* component mounted.
|
|
78
|
+
*/
|
|
79
|
+
export declare function useTourDescription(action: string, describe: () => TourActionResult): void;
|
|
80
|
+
/**
|
|
81
|
+
* Starts a waiting tour once this element is on screen and uncovered: not
|
|
82
|
+
* under the PDV takeover, not under a dialog, not scrolled away. Polled while
|
|
83
|
+
* something is pending, and only then.
|
|
84
|
+
*/
|
|
85
|
+
export declare function useJourneyTourAutostart(ref: React.RefObject<HTMLElement | null>, enabled: boolean, runtime: Pick<PluginRuntime, 'onboardingTours'> | null): void;
|
|
86
|
+
//# sourceMappingURL=tour.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tour.d.ts","sourceRoot":"","sources":["../../src/lib/tour.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D0 as the UX spec defines it (§2.4, moment 10): only under 24 hours does the
|
|
3
|
+
* clock stop saying "days" and start saying "hoje às HH:mm". 23 hours is still
|
|
4
|
+
* "1 dia" everywhere else; here it is the last day.
|
|
5
|
+
*/
|
|
6
|
+
export declare function trialEndsToday(trialEndsAt: string | null | undefined, now?: Date): boolean;
|
|
7
|
+
/** "23:59" in the app's locale — the hour the copy of the D0 banner says. */
|
|
8
|
+
export declare function formatEndTime(iso: string | null | undefined, locale?: string): string;
|
|
9
|
+
export declare const D0_DISMISS_KEY = "fayz:getting-started:d0-dismissed-until";
|
|
10
|
+
export declare const D0_DISMISS_HOURS = 8;
|
|
11
|
+
/** The banner is dismissable for 8 h (spec, moment 10) — per browser, never per server. */
|
|
12
|
+
export declare function isD0Dismissed(now?: Date, storage?: Pick<Storage, 'getItem'> | undefined): boolean;
|
|
13
|
+
export declare function dismissD0(now?: Date, storage?: Pick<Storage, 'setItem'> | undefined): void;
|
|
14
|
+
//# sourceMappingURL=trial-clock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trial-clock.d.ts","sourceRoot":"","sources":["../../src/lib/trial-clock.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,GAAG,GAAE,IAAiB,GAAG,OAAO,CAMtG;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,MAAM,GAAE,MAA2B,GAAG,MAAM,CASzG;AAED,eAAO,MAAM,cAAc,4CAA4C,CAAA;AACvE,eAAO,MAAM,gBAAgB,IAAI,CAAA;AAEjC,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,GAAG,GAAE,IAAiB,EAAE,OAAO,GAAE,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,GAAG,SAAyB,GAAG,OAAO,CAO5H;AAED,wBAAgB,SAAS,CAAC,GAAG,GAAE,IAAiB,EAAE,OAAO,GAAE,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,GAAG,SAAyB,GAAG,IAAI,CAMrH"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type TrialSourceState } from '@fayz-ai/admin/host';
|
|
2
|
+
import type { Journey } from '../types';
|
|
3
|
+
export interface TrialSourceInput {
|
|
4
|
+
journey: Journey | null;
|
|
5
|
+
/** Minimums already green, as the CTA counts them. */
|
|
6
|
+
readyCount: number;
|
|
7
|
+
readyTotal: number;
|
|
8
|
+
onStart?: () => Promise<void>;
|
|
9
|
+
onRequestContract?: () => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/** The journey as the shell's clock wants it. `null` hands the shell back its default behaviour. */
|
|
12
|
+
export declare function toTrialSource(input: TrialSourceInput): TrialSourceState | null;
|
|
13
|
+
export declare function publishTrialSource(input: TrialSourceInput): void;
|
|
14
|
+
export declare function clearTrialSource(): void;
|
|
15
|
+
/** "9 de novembro" in the app's locale — the date the copy of the CTA, the banner and the tab say. */
|
|
16
|
+
export declare function formatTrialDate(iso: string | null | undefined, locale?: string): string;
|
|
17
|
+
//# sourceMappingURL=trial-source-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trial-source-bridge.d.ts","sourceRoot":"","sources":["../../src/lib/trial-source-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAE3E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAEvC,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACxC;AAED,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,IAAI,CAgB9E;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAEhE;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED,sGAAsG;AACtG,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,MAAM,GAAE,MAA2B,GAAG,MAAM,CAS3G"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAwJrC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/locales/index.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGxE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAwJvC,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const MIGRATION_000_JOURNEY = "-- 000_journey.sql \u2014 the getting-started plugin keeps the journey; tenants.trial_ends_at stays the end of the trial.\n\n-- \u2500\u2500 platform config (one row; only service_role/postgres write) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE TABLE IF NOT EXISTS public.plg_getting_started_config (\n id boolean PRIMARY KEY DEFAULT true CHECK (id),\n trial_days integer NOT NULL DEFAULT 14 CHECK (trial_days BETWEEN 1 AND 90),\n -- Documents the spine DEFAULT (db 044); never changes it.\n setup_window_days integer NOT NULL DEFAULT 30,\n crm_tenant_slug text NOT NULL DEFAULT 'controlgroup',\n reminder_hour_utc integer NOT NULL DEFAULT 13 CHECK (reminder_hour_utc BETWEEN 0 AND 23),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nINSERT INTO public.plg_getting_started_config (id) VALUES (true) ON CONFLICT (id) DO NOTHING;\nALTER TABLE public.plg_getting_started_config ENABLE ROW LEVEL SECURITY;\nREVOKE ALL ON public.plg_getting_started_config FROM PUBLIC, anon, authenticated;\n-- Read by the view and the RPCs; no write policy on purpose.\nGRANT SELECT ON public.plg_getting_started_config TO authenticated;\nDROP POLICY IF EXISTS plg_getting_started_config_read ON public.plg_getting_started_config;\nCREATE POLICY plg_getting_started_config_read ON public.plg_getting_started_config\n FOR SELECT TO authenticated USING (true);\n\n-- \u2500\u2500 the journey: one row per tenant \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 TABLE IF NOT EXISTS public.plg_getting_started_journeys (\n tenant_id uuid PRIMARY KEY REFERENCES public.tenants(id) ON DELETE CASCADE,\n ready_at timestamptz,\n trial_started_at timestamptz,\n trial_days integer,\n converted_at timestamptz,\n wants_contract_at timestamptz,\n source text,\n utm jsonb NOT NULL DEFAULT '{}'::jsonb,\n answers_snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_getting_started_journeys ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_getting_started_journeys FORCE ROW LEVEL SECURITY;\n-- Writes go through the RPCs only.\nGRANT SELECT ON public.plg_getting_started_journeys TO authenticated;\nDROP POLICY IF EXISTS plg_getting_started_journeys_select ON public.plg_getting_started_journeys;\nCREATE POLICY plg_getting_started_journeys_select ON public.plg_getting_started_journeys\n FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- \u2500\u2500 timeline + idempotency lock (one row per tenant, kind and day) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE TABLE IF NOT EXISTS public.plg_getting_started_events (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n kind text NOT NULL CHECK (kind ~ '^[a-z][a-z0-9_]*$'),\n day date NOT NULL DEFAULT (now() AT TIME ZONE 'UTC')::date,\n channel text,\n payload jsonb NOT NULL DEFAULT '{}'::jsonb,\n actor_id uuid,\n created_at timestamptz NOT NULL DEFAULT now()\n);\nCREATE UNIQUE INDEX IF NOT EXISTS plg_getting_started_events_once_a_day\n ON public.plg_getting_started_events (tenant_id, kind, day);\nCREATE INDEX IF NOT EXISTS plg_getting_started_events_tenant\n ON public.plg_getting_started_events (tenant_id, created_at DESC);\nALTER TABLE public.plg_getting_started_events ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_getting_started_events FORCE ROW LEVEL SECURITY;\nGRANT SELECT ON public.plg_getting_started_events TO authenticated;\nDROP POLICY IF EXISTS plg_getting_started_events_select ON public.plg_getting_started_events;\nCREATE POLICY plg_getting_started_events_select ON public.plg_getting_started_events\n FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- \u2500\u2500 wizard keys this plugin owns (tech design, decision 5) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nSELECT app.seed_setting_keys($$[\n { \"key\": \"store.hours\", \"type\": \"object\", \"default\": {}, \"reader\": \"plugin:getting-started\",\n \"description\": \"Opening hours per weekday: {\\\"mon\\\":[{\\\"open\\\":\\\"11:00\\\",\\\"close\\\":\\\"23:00\\\"}],\u2026}. Empty = not informed.\" },\n { \"key\": \"store.payment_methods\", \"type\": \"array\", \"default\": [], \"reader\": \"plugin:getting-started\",\n \"description\": \"Payment methods accepted at the counter and on delivery: cash, pix, credit, debit, voucher.\" },\n { \"key\": \"store.welcome_article\", \"type\": \"string\", \"default\": \"a\", \"reader\": \"plugin:getting-started\",\n \"description\": \"Article before the store name in greetings: a | o.\" }\n]$$::jsonb);\n\n-- \u2500\u2500 the phase, derived (decision 1 and \u00A73) \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\n-- security_invoker: the RLS of tenants decides which rows are visible (same pattern as v_team_members, db 004).\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 CASE WHEN j.trial_started_at IS NULL THEN t.trial_ends_at END AS setup_deadline_at,\n CASE\n WHEN j.converted_at IS NOT NULL THEN 'converted'\n WHEN t.trial_ends_at IS NOT NULL AND t.trial_ends_at <= now() THEN 'expired'\n WHEN j.trial_started_at IS NOT NULL THEN 'trial'\n ELSE 'setup'\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 minimums, one function (used by the start RPC and by the read) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.getting_started_minimums(p_tenant uuid)\nRETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_menu boolean := false;\n v_hours boolean := false;\n v_order boolean := false;\n v_settings jsonb;\nBEGIN\n SELECT t.settings INTO v_settings FROM public.tenants t WHERE t.id = p_tenant;\n v_hours := jsonb_typeof(v_settings #> '{store,hours}') = 'object'\n AND (v_settings #> '{store,hours}') <> '{}'::jsonb;\n -- A pool without plugin-menu must not break the read.\n IF to_regclass('public.plg_menu_items') IS NOT NULL THEN\n EXECUTE $q$SELECT EXISTS (\n SELECT 1 FROM public.plg_menu_items mi\n JOIN public.products p ON p.id = mi.product_id\n WHERE mi.tenant_id = $1 AND mi.status = 'available' AND p.is_active\n AND coalesce(p.metadata->>'demo','') <> 'true')$q$\n INTO v_menu USING p_tenant;\n END IF;\n SELECT EXISTS (\n SELECT 1 FROM public.orders o\n WHERE o.tenant_id = p_tenant AND o.status = 'completed'\n AND o.metadata @> '{\"test\":true}'::jsonb\n ) INTO v_order;\n RETURN jsonb_build_object(\n 'menu', v_menu, 'hours', v_hours, 'test_order', v_order,\n 'ready', v_menu AND v_hours AND v_order\n );\nEND $$;\nREVOKE ALL ON FUNCTION public.getting_started_minimums(uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.getting_started_minimums(uuid) TO authenticated, service_role;\n\n-- \u2500\u2500 the read for the client \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 FUNCTION public.getting_started_journey()\nRETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_row record;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'getting_started_journey: no active tenant' USING ERRCODE = '42501';\n END IF;\n SELECT * INTO v_row FROM public.v_getting_started_journey v WHERE v.tenant_id = v_tenant;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('tenant_id', v_tenant, 'phase', 'setup',\n 'minimums', public.getting_started_minimums(v_tenant));\n END IF;\n RETURN to_jsonb(v_row) || jsonb_build_object('minimums', public.getting_started_minimums(v_tenant));\nEND $$;\nREVOKE ALL ON FUNCTION public.getting_started_journey() FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.getting_started_journey() TO authenticated, service_role;\n";
|
|
2
|
+
export declare const MIGRATION_001_TRIAL_CLOCK = "-- 001_trial_clock.sql \u2014 start, end, reminders and the Faya lead. Depends on 000_journey.sql.\n\n-- \u2500\u2500 start the trial \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 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_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 -- Idempotent: a second call returns the state and leaves the date alone.\n SELECT j.trial_started_at INTO v_started FROM public.plg_getting_started_journeys j WHERE j.tenant_id = v_tenant;\n IF v_started IS NOT NULL THEN\n RETURN jsonb_build_object('ok', true, 'already_started', true, 'trial_started_at', v_started,\n 'trial_ends_at', (SELECT t.trial_ends_at FROM public.tenants t WHERE t.id = v_tenant));\n END IF;\n\n -- Minimums are checked here, not in the button.\n v_min := public.getting_started_minimums(v_tenant);\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 because SECURITY DEFINER runs as the owner (postgres).\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-- Agent executor shape (p_tenant_id, p_actor_user_id, p_payload), as agent_crm_create_lead.\nCREATE OR REPLACE FUNCTION public.agent_getting_started_start_trial(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb DEFAULT '{}'::jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nBEGIN\n IF auth.uid() IS NOT NULL AND auth.uid() IS DISTINCT FROM p_actor_user_id THEN\n RAISE EXCEPTION 'agent_getting_started_start_trial: actor mismatch' USING ERRCODE = '42501';\n END IF;\n RETURN public.getting_started_start_trial(p_tenant_id);\nEND $$;\nREVOKE ALL ON FUNCTION public.agent_getting_started_start_trial(uuid, uuid, jsonb) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.agent_getting_started_start_trial(uuid, uuid, jsonb) TO authenticated, service_role;\n\n-- \u2500\u2500 \"I want to subscribe\" \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 FUNCTION public.getting_started_request_contract(p_tenant uuid DEFAULT NULL, p_note text 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_inserted boolean;\nBEGIN\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_request_contract: 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_request_contract: settings.manage is required' USING ERRCODE = '42501';\n END IF;\n\n -- Rate limit: the timeline's one-per-day lock. The V1 had none.\n INSERT INTO public.plg_getting_started_events (tenant_id, kind, actor_id, payload)\n VALUES (v_tenant, 'wants_contract', auth.uid(), jsonb_build_object('note', left(coalesce(p_note, ''), 500)))\n ON CONFLICT (tenant_id, kind, day) DO NOTHING\n RETURNING true INTO v_inserted;\n IF NOT coalesce(v_inserted, false) THEN\n RETURN jsonb_build_object('ok', true, 'already_requested_today', true);\n END IF;\n\n INSERT INTO public.plg_getting_started_journeys (tenant_id, wants_contract_at)\n VALUES (v_tenant, now())\n ON CONFLICT (tenant_id) DO UPDATE\n SET wants_contract_at = coalesce(public.plg_getting_started_journeys.wants_contract_at, now()),\n updated_at = now();\n\n PERFORM public.plg_emit_event('getting_started.wants_contract',\n jsonb_build_object('note', left(coalesce(p_note, ''), 500)), 'tenant', v_tenant::text, v_tenant);\n\n RETURN jsonb_build_object('ok', true);\nEND $$;\nREVOKE ALL ON FUNCTION public.getting_started_request_contract(uuid, text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.getting_started_request_contract(uuid, text) TO authenticated, service_role;\n\n-- \u2500\u2500 reminders D\u22123 / D0 \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\n-- A tenant's calendar day. A bad zone name must not take the whole run down.\nCREATE OR REPLACE FUNCTION public.plg_getting_started_local_date(p_at timestamptz, p_tz text)\nRETURNS date LANGUAGE plpgsql STABLE SET search_path TO '' AS $$\nBEGIN\n RETURN (p_at AT TIME ZONE coalesce(nullif(p_tz, ''), 'UTC'))::date;\nEXCEPTION WHEN OTHERS THEN\n RETURN (p_at AT TIME ZONE 'UTC')::date;\nEND $$;\n\n-- Who is due today. `p_now` is injectable so QA can simulate the day without touching the clock.\n-- Due-ness is by the tenant's local day; the once-a-day lock is by the run's UTC day, so a\n-- reminder sent before an extend does not block the one the new end date earns.\nCREATE OR REPLACE FUNCTION public.getting_started_reminders_due(p_now timestamptz DEFAULT now())\nRETURNS TABLE (tenant_id uuid, kind text, trial_ends_at timestamptz)\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path TO '' AS $$\n WITH due AS (\n SELECT j.tenant_id, t.trial_ends_at,\n public.plg_getting_started_local_date(t.trial_ends_at, t.settings ->> 'timezone') AS end_day,\n public.plg_getting_started_local_date(p_now, t.settings ->> 'timezone') AS today\n FROM public.plg_getting_started_journeys j\n JOIN public.tenants t ON t.id = j.tenant_id\n WHERE j.trial_started_at IS NOT NULL\n AND j.converted_at IS NULL\n AND t.trial_ends_at IS NOT NULL\n )\n SELECT d.tenant_id,\n CASE WHEN d.end_day = d.today + 3 THEN 'reminder_d3' ELSE 'reminder_d0' END AS kind,\n d.trial_ends_at\n FROM due d\n WHERE d.end_day IN (d.today + 3, d.today)\n AND NOT EXISTS (\n SELECT 1 FROM public.plg_getting_started_events e\n WHERE e.tenant_id = d.tenant_id\n AND e.day = (p_now AT TIME ZONE 'UTC')::date\n AND e.kind = CASE WHEN d.end_day = d.today + 3 THEN 'reminder_d3' ELSE 'reminder_d0' END\n );\n$$;\nREVOKE ALL ON FUNCTION public.getting_started_reminders_due(timestamptz) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.getting_started_reminders_due(timestamptz) TO service_role;\n\n-- Templates are per tenant (notifications UNIQUE (tenant_id, key, channel, locale)).\nCREATE OR REPLACE FUNCTION public.getting_started_ensure_templates(p_tenant uuid)\nRETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nBEGIN\n IF to_regclass('public.plg_notifications_templates') IS NULL THEN RETURN; END IF;\n INSERT INTO public.plg_notifications_templates (tenant_id, key, channel, locale, subject, body, variables, is_default)\n VALUES\n (p_tenant, 'getting_started.reminder_d3', 'in_app', 'pt-BR', 'Seu teste termina em 3 dias',\n 'Faltam 3 dias de teste gr\u00E1tis. At\u00E9 l\u00E1 est\u00E1 tudo liberado. Para continuar depois, \u00E9 s\u00F3 escolher um plano em Configura\u00E7\u00F5es \u203A Assinatura \u2014 sem fidelidade.',\n '[\"trial_ends_at\"]'::jsonb, true),\n (p_tenant, 'getting_started.reminder_d0', 'in_app', 'pt-BR', 'Hoje \u00E9 o \u00FAltimo dia do seu teste',\n 'Termina hoje. Depois disso a loja continua e seus dados tamb\u00E9m; o que \u00E9 do plano pago fica fechado at\u00E9 voc\u00EA assinar.',\n '[\"trial_ends_at\"]'::jsonb, true)\n ON CONFLICT (tenant_id, key, channel, locale) DO NOTHING;\nEND $$;\nREVOKE ALL ON FUNCTION public.getting_started_ensure_templates(uuid) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.getting_started_ensure_templates(uuid) TO service_role;\n\n-- The job. Writes the event BEFORE enqueueing (the lock), then enqueues through notifications when present.\nCREATE OR REPLACE FUNCTION public.getting_started_reminders_run(p_now timestamptz DEFAULT now())\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n r record;\n v_day date := (p_now AT TIME ZONE 'UTC')::date;\n v_person uuid;\n v_sent integer := 0;\n v_skipped integer := 0;\n v_queue uuid;\n v_enqueue boolean := to_regprocedure('public.notifications_enqueue(uuid,text,uuid,jsonb,text,timestamptz,text,text,uuid,uuid)') IS NOT NULL;\nBEGIN\n IF NOT pg_try_advisory_xact_lock(hashtext('getting_started_reminders')) THEN\n RETURN jsonb_build_object('skipped', 'another run is active');\n END IF;\n\n FOR r IN SELECT * FROM public.getting_started_reminders_due(p_now) LOOP\n -- The owner: the team person whose e-mail is the first active member's.\n SELECT pe.id INTO v_person\n FROM app.memberships m\n JOIN auth.users u ON u.id = m.user_id\n JOIN public.people pe ON pe.tenant_id = m.tenant_id AND lower(pe.email) = lower(u.email)\n WHERE m.tenant_id = r.tenant_id AND m.active\n ORDER BY m.created_at\n LIMIT 1;\n\n INSERT INTO public.plg_getting_started_events (tenant_id, kind, day, channel, payload)\n VALUES (r.tenant_id, r.kind, v_day, CASE WHEN v_person IS NULL THEN 'none' ELSE 'in_app' END,\n jsonb_build_object('trial_ends_at', r.trial_ends_at))\n ON CONFLICT (tenant_id, kind, day) DO NOTHING;\n IF NOT FOUND THEN v_skipped := v_skipped + 1; CONTINUE; END IF;\n\n IF v_person IS NULL OR NOT v_enqueue THEN\n v_skipped := v_skipped + 1; CONTINUE;\n END IF;\n\n PERFORM public.getting_started_ensure_templates(r.tenant_id);\n BEGIN\n EXECUTE 'SELECT public.notifications_enqueue($1, $2, $3, $4, $5, $6, $7)'\n INTO v_queue\n USING r.tenant_id, 'getting_started.' || r.kind, v_person,\n jsonb_build_object('trial_ends_at', r.trial_ends_at),\n 'getting_started:' || r.kind || ':' || r.tenant_id || ':' || v_day, p_now, 'in_app';\n v_sent := v_sent + 1;\n EXCEPTION WHEN OTHERS THEN\n -- A provider down for one tenant does not stop the others; the event already marked the day.\n UPDATE public.plg_getting_started_events SET payload = payload || jsonb_build_object('error', SQLERRM)\n WHERE tenant_id = r.tenant_id AND kind = r.kind AND day = v_day;\n v_skipped := v_skipped + 1;\n END;\n END LOOP;\n\n RETURN jsonb_build_object('sent', v_sent, 'skipped', v_skipped);\nEND $$;\nREVOKE ALL ON FUNCTION public.getting_started_reminders_run(timestamptz) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.getting_started_reminders_run(timestamptz) TO service_role;\n\n-- cron: the spine's guard, plus a row in app.scheduled_jobs so the schedule is visible without pg_cron.\nDO $cron$\nDECLARE v_hour integer;\nBEGIN\n SELECT reminder_hour_utc INTO v_hour FROM public.plg_getting_started_config;\n v_hour := coalesce(v_hour, 13);\n INSERT INTO app.scheduled_jobs (job_name, schedule, command, scheduled, reason)\n VALUES ('fayz_getting_started_reminders', format('0 %s * * *', v_hour), 'SELECT public.getting_started_reminders_run()', false, 'pending')\n ON CONFLICT (job_name) DO UPDATE\n SET schedule = EXCLUDED.schedule, command = EXCLUDED.command, updated_at = now();\n IF to_regprocedure('cron.schedule(text, text, text)') IS NULL THEN\n UPDATE app.scheduled_jobs SET scheduled = false, reason = 'pg_cron absent' WHERE job_name = 'fayz_getting_started_reminders';\n RAISE NOTICE 'getting-started: pg_cron absent \u2014 reminders not scheduled';\n RETURN;\n END IF;\n PERFORM cron.unschedule(jobid) FROM cron.job WHERE jobname = 'fayz_getting_started_reminders';\n PERFORM cron.schedule('fayz_getting_started_reminders', format('0 %s * * *', v_hour), $job$SELECT public.getting_started_reminders_run()$job$);\n UPDATE app.scheduled_jobs SET scheduled = true, reason = NULL WHERE job_name = 'fayz_getting_started_reminders';\nEND $cron$;\n\n-- \u2500\u2500 the lead in Faya's CRM \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 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(rpad(btrim(v_name), 2, '.'), 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$$;\nCREATE OR REPLACE FUNCTION public.plg_getting_started_on_wants_contract(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, 'quer_contratar', coalesce(p_event -> 'payload', '{}'::jsonb));\n$$;\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;\n\nSELECT public.register_event_subscription('getting-started', 'getting_started.trial_started', 'plg_getting_started_on_trial_started', 'Faya lead when the trial starts', 5);\nSELECT public.register_event_subscription('getting-started', 'getting_started.wants_contract', 'plg_getting_started_on_wants_contract', 'Faya lead when the tenant wants to subscribe', 5);\n";
|
|
3
|
+
export declare const MIGRATION_002_PLATFORM_OPERATOR = "-- 002_platform_operator.sql \u2014 the platform reads every tenant's trial and acts on it.\n-- Functions only; no new table. Depends on 000_journey.sql and 001_trial_clock.sql.\n--\n-- \"Platform operator\" = an active member of the tenant named by\n-- plg_getting_started_config.crm_tenant_slug, acting INSIDE that tenant\n-- (app.current_tenant_id()), or \u2014 where the live cluster has the platform\n-- schema \u2014 platform.can(auth.uid(), 'tenants.write'). Same shape as\n-- support_is_operator(): the boundary is a SECURITY DEFINER function with a\n-- guard; no RLS policy opens between tenants.\n\n-- \u2500\u2500 who is the platform \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 FUNCTION public.getting_started_platform_tenant()\nRETURNS uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO '' AS $$\n SELECT t.id\n FROM public.plg_getting_started_config c\n JOIN public.tenants t ON t.slug = c.crm_tenant_slug\n LIMIT 1;\n$$;\nREVOKE ALL ON FUNCTION public.getting_started_platform_tenant() FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.getting_started_platform_tenant() TO authenticated, service_role;\n\nCREATE OR REPLACE FUNCTION public.getting_started_is_platform_operator()\nRETURNS boolean LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_uid uuid := auth.uid();\n v_platform uuid := public.getting_started_platform_tenant();\n v_can boolean := false;\nBEGIN\n IF v_uid IS NULL THEN RETURN false; END IF;\n IF v_platform IS NOT NULL AND v_platform = app.current_tenant_id() AND EXISTS (\n SELECT 1 FROM app.memberships m\n WHERE m.tenant_id = v_platform AND m.user_id = v_uid AND m.active) THEN\n RETURN true;\n END IF;\n -- Optional reinforcement: the platform schema lives only on the live cluster.\n IF to_regprocedure('platform.can(uuid,text)') IS NOT NULL THEN\n EXECUTE 'SELECT platform.can($1, $2)' INTO v_can USING v_uid, 'tenants.write';\n END IF;\n RETURN coalesce(v_can, false);\nEND $$;\nREVOKE ALL ON FUNCTION public.getting_started_is_platform_operator() FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.getting_started_is_platform_operator() TO authenticated, service_role;\n\nCREATE OR REPLACE FUNCTION public.getting_started_platform_guard()\nRETURNS void LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO '' AS $$\nBEGIN\n IF NOT public.getting_started_is_platform_operator() THEN\n RAISE EXCEPTION 'not a platform operator' USING ERRCODE = '42501';\n END IF;\nEND $$;\nREVOKE ALL ON FUNCTION public.getting_started_platform_guard() FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.getting_started_platform_guard() TO authenticated, service_role;\n\n-- \u2500\u2500 timeline note that survives a repeat on the same day \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- plg_getting_started_events is UNIQUE (tenant_id, kind, day). A second\n-- extension on the same day keeps one row and folds the latest payload in,\n-- counting the repeats; the customer-success timeline keeps every action.\nCREATE OR REPLACE FUNCTION public.plg_getting_started_note(p_tenant uuid, p_kind text, p_payload jsonb, p_actor uuid DEFAULT auth.uid())\nRETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nBEGIN\n INSERT INTO public.plg_getting_started_events (tenant_id, kind, actor_id, payload)\n VALUES (p_tenant, p_kind, p_actor, coalesce(p_payload, '{}'::jsonb))\n ON CONFLICT (tenant_id, kind, day) DO UPDATE\n SET payload = public.plg_getting_started_events.payload || EXCLUDED.payload\n || jsonb_build_object('repeats', coalesce((public.plg_getting_started_events.payload ->> 'repeats')::int, 0) + 1),\n actor_id = coalesce(EXCLUDED.actor_id, public.plg_getting_started_events.actor_id);\nEND $$;\nREVOKE ALL ON FUNCTION public.plg_getting_started_note(uuid, text, jsonb, uuid) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_getting_started_note(uuid, text, jsonb, uuid) TO service_role;\n\n-- \u2500\u2500 every tenant's journey, for the platform \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.getting_started_platform_journeys(p_search text DEFAULT NULL, p_phase text DEFAULT NULL)\nRETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_platform uuid := public.getting_started_platform_tenant();\n v_search text := nullif(btrim(coalesce(p_search, '')), '');\n v_result jsonb;\nBEGIN\n PERFORM public.getting_started_platform_guard();\n SELECT coalesce(jsonb_agg(to_jsonb(r) ORDER BY r.tenant_created_at DESC), '[]'::jsonb) INTO v_result\n FROM (\n SELECT t.id AS tenant_id,\n t.name AS tenant_name,\n t.slug AS tenant_slug,\n t.vertical_id,\n t.plan AS pool_plan,\n t.settings #>> '{billing,plan_id}' AS grants_plan,\n t.kind,\n t.created_at AS tenant_created_at,\n coalesce((SELECT array_agg(ta.app_id ORDER BY ta.created_at)\n FROM app.tenant_apps ta WHERE ta.tenant_id = t.id AND ta.status = 'active'), '{}'::text[]) AS apps,\n coalesce((SELECT l.state FROM app.tenant_lifecycle l WHERE l.tenant_id = t.id), 'active') AS lifecycle_state,\n coalesce(j.phase, 'setup') AS phase,\n j.ready_at, j.trial_started_at, j.trial_ends_at, j.setup_deadline_at, j.converted_at, j.wants_contract_at,\n coalesce(j.trial_days, c.trial_days) AS trial_days,\n j.source, coalesce(j.utm, '{}'::jsonb) AS utm,\n public.getting_started_minimums(t.id) AS minimums,\n (SELECT jsonb_build_object('email', u.email, 'name', pe.name, 'phone', pe.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.id = m.person_id\n WHERE m.tenant_id = t.id AND m.active\n ORDER BY m.created_at LIMIT 1) AS contact,\n (SELECT jsonb_build_object('kind', e.kind, 'created_at', e.created_at)\n FROM public.plg_getting_started_events e\n WHERE e.tenant_id = t.id ORDER BY e.created_at DESC LIMIT 1) AS last_event,\n (SELECT count(*) FROM public.plg_getting_started_events e WHERE e.tenant_id = t.id) AS events_count\n FROM public.tenants t\n CROSS JOIN public.plg_getting_started_config c\n LEFT JOIN public.v_getting_started_journey j ON j.tenant_id = t.id\n WHERE (v_platform IS NULL OR t.id <> v_platform)\n AND (p_phase IS NULL OR coalesce(j.phase, 'setup') = p_phase)\n AND (v_search IS NULL\n OR t.name ILIKE '%' || v_search || '%'\n OR t.slug ILIKE '%' || v_search || '%'\n OR EXISTS (SELECT 1 FROM app.memberships m JOIN auth.users u ON u.id = m.user_id\n WHERE m.tenant_id = t.id AND m.active AND u.email ILIKE '%' || v_search || '%'))\n ) r;\n RETURN v_result;\nEND $$;\nREVOKE ALL ON FUNCTION public.getting_started_platform_journeys(text, text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.getting_started_platform_journeys(text, text) TO authenticated, service_role;\n\n-- \u2500\u2500 act on one tenant's trial \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\n-- extend : adds p_days (1..90) on top of greatest(trial_ends_at, now()) \u2014 V1 \u00A76.6.\n-- end_now : trial_ends_at = now(); emits getting_started.expired.\n-- convert : converted_at = coalesce(converted_at, now()); emits getting_started.converted.\n-- remind : one in_app notice to the first member, template getting_started.reminder_d0.\n-- The operator is not a member of the house, and plg_emit_event refuses a caller outside\n-- the tenant. The guard above already authorised the platform; file the fact directly.\nCREATE OR REPLACE FUNCTION public.plg_getting_started_platform_emit(p_tenant uuid, p_key text, p_payload jsonb)\nRETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE v_id uuid;\nBEGIN\n INSERT INTO public.event_log (tenant_id, event_key, payload, actor_id, subject_type, subject_id)\n VALUES (p_tenant, p_key, coalesce(p_payload, '{}'::jsonb), auth.uid(), 'tenant', p_tenant::text)\n RETURNING id INTO v_id;\n RETURN v_id;\nEXCEPTION WHEN OTHERS THEN\n RAISE WARNING '[getting_started_trial_set] % not recorded: %', p_key, SQLERRM;\n RETURN NULL;\nEND $$;\nREVOKE ALL ON FUNCTION public.plg_getting_started_platform_emit(uuid, text, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_getting_started_platform_emit(uuid, text, jsonb) TO service_role;\n\nCREATE OR REPLACE FUNCTION public.getting_started_trial_set(p_tenant uuid, p_action text, p_days integer DEFAULT NULL, p_reason text DEFAULT NULL)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_platform uuid := public.getting_started_platform_tenant();\n v_actor uuid := auth.uid();\n v_reason text := nullif(btrim(coalesce(p_reason, '')), '');\n v_t record;\n v_j record;\n v_before jsonb;\n v_after jsonb;\n v_payload jsonb := '{}'::jsonb;\n v_ends timestamptz;\n v_converted timestamptz;\n v_person uuid;\n v_nonce text;\n v_queue uuid;\n v_enqueue boolean := to_regprocedure('public.notifications_enqueue(uuid,text,uuid,jsonb,text,timestamptz,text,text,uuid,uuid)') IS NOT NULL;\n v_ok boolean := true;\nBEGIN\n PERFORM public.getting_started_platform_guard();\n IF p_action IS NULL OR p_action NOT IN ('extend', 'end_now', 'convert', 'remind') THEN\n RAISE EXCEPTION 'invalid action %', p_action USING ERRCODE = '22023';\n END IF;\n IF p_tenant IS NULL OR p_tenant = v_platform THEN\n RAISE EXCEPTION 'tenant out of scope' USING ERRCODE = '22023';\n END IF;\n SELECT t.id, t.trial_ends_at INTO v_t FROM public.tenants t WHERE t.id = p_tenant FOR UPDATE;\n IF NOT FOUND THEN RAISE EXCEPTION 'tenant not found' USING ERRCODE = 'P0002'; END IF;\n SELECT j.trial_started_at, j.converted_at INTO v_j FROM public.plg_getting_started_journeys j WHERE j.tenant_id = p_tenant;\n\n v_before := jsonb_build_object('trial_ends_at', v_t.trial_ends_at, 'trial_started_at', v_j.trial_started_at, 'converted_at', v_j.converted_at);\n v_ends := v_t.trial_ends_at;\n v_converted := v_j.converted_at;\n\n IF p_action = 'extend' THEN\n IF p_days IS NULL OR p_days NOT BETWEEN 1 AND 90 THEN\n RAISE EXCEPTION 'p_days must be between 1 and 90' USING ERRCODE = '22023';\n END IF;\n v_ends := greatest(coalesce(v_t.trial_ends_at, now()), now()) + make_interval(days => p_days);\n INSERT INTO public.plg_getting_started_journeys (tenant_id, trial_started_at, trial_days)\n VALUES (p_tenant, now(), (SELECT c.trial_days FROM public.plg_getting_started_config c))\n ON CONFLICT (tenant_id) DO UPDATE\n SET trial_started_at = coalesce(public.plg_getting_started_journeys.trial_started_at, EXCLUDED.trial_started_at),\n updated_at = now();\n -- Passes tenants_trial_is_readonly: SECURITY DEFINER runs as the owner.\n UPDATE public.tenants SET trial_ends_at = v_ends WHERE id = p_tenant;\n v_payload := jsonb_build_object('days', p_days, 'ends_at', v_ends, 'reason', v_reason);\n PERFORM public.plg_getting_started_note(p_tenant, 'trial_extended', v_payload, v_actor);\n\n ELSIF p_action = 'end_now' THEN\n IF v_reason IS NULL THEN RAISE EXCEPTION 'a reason is required to end a trial' USING ERRCODE = '22023'; END IF;\n v_ends := now();\n UPDATE public.tenants SET trial_ends_at = v_ends WHERE id = p_tenant;\n v_payload := jsonb_build_object('ends_at', v_ends, 'reason', v_reason);\n PERFORM public.plg_getting_started_note(p_tenant, 'trial_ended', v_payload, v_actor);\n PERFORM public.plg_getting_started_platform_emit(p_tenant, 'getting_started.expired',\n jsonb_build_object('trial_ends_at', v_ends, 'reason', v_reason, 'ended_by', v_actor));\n\n ELSIF p_action = 'convert' THEN\n -- Without a proposal the operator writes why; with one, the caller passes 'proposal:<number>'.\n IF v_reason IS NULL THEN RAISE EXCEPTION 'a reason is required to convert without a proposal' USING ERRCODE = '22023'; END IF;\n v_converted := coalesce(v_j.converted_at, now());\n INSERT INTO public.plg_getting_started_journeys (tenant_id, converted_at)\n VALUES (p_tenant, v_converted)\n ON CONFLICT (tenant_id) DO UPDATE\n SET converted_at = coalesce(public.plg_getting_started_journeys.converted_at, EXCLUDED.converted_at),\n updated_at = now();\n v_payload := jsonb_build_object('converted_at', v_converted, 'reason', v_reason);\n IF v_j.converted_at IS NULL THEN\n PERFORM public.plg_getting_started_note(p_tenant, 'converted', v_payload, v_actor);\n PERFORM public.plg_getting_started_platform_emit(p_tenant, 'getting_started.converted',\n jsonb_build_object('converted_at', v_converted, 'reason', v_reason, 'converted_by', v_actor));\n END IF;\n\n ELSIF p_action = 'remind' THEN\n SELECT pe.id INTO v_person\n FROM app.memberships m\n JOIN auth.users u ON u.id = m.user_id\n 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 LIMIT 1;\n v_nonce := left(md5(random()::text || clock_timestamp()::text), 12);\n v_payload := jsonb_build_object('trial_ends_at', v_t.trial_ends_at, 'nonce', v_nonce, 'reason', v_reason,\n 'channel', CASE WHEN v_person IS NULL OR NOT v_enqueue THEN 'none' ELSE 'in_app' END);\n IF v_person IS NULL OR NOT v_enqueue THEN\n v_ok := false;\n v_payload := v_payload || jsonb_build_object('skipped', CASE WHEN v_person IS NULL THEN 'no contact' ELSE 'plugin-notifications absent' END);\n ELSE\n PERFORM public.getting_started_ensure_templates(p_tenant);\n EXECUTE 'SELECT public.notifications_enqueue($1, $2, $3, $4, $5, $6, $7)'\n INTO v_queue\n USING p_tenant, 'getting_started.reminder_d0', v_person,\n jsonb_build_object('trial_ends_at', v_t.trial_ends_at),\n 'getting_started:reminder_manual:' || p_tenant || ':' || v_nonce, now(), 'in_app';\n v_payload := v_payload || jsonb_build_object('queue_id', v_queue);\n END IF;\n PERFORM public.plg_getting_started_note(p_tenant, 'reminder_manual', v_payload, v_actor);\n END IF;\n\n v_after := jsonb_build_object('trial_ends_at', v_ends, 'converted_at', v_converted);\n -- audit_log derives the tenant from the session for a signed-in caller; the\n -- customer tenant travels in the metadata. A service_role run names the platform.\n PERFORM public.audit_log('getting_started.' || p_action, 'tenant', p_tenant, v_reason, v_before, v_after, NULL,\n v_payload || jsonb_build_object('customer_tenant_id', p_tenant),\n CASE WHEN v_actor IS NULL THEN v_platform ELSE NULL END);\n\n RETURN jsonb_build_object('ok', v_ok, 'action', p_action, 'tenant_id', p_tenant,\n 'trial_ends_at', v_ends, 'converted_at', v_converted);\nEND $$;\nREVOKE ALL ON FUNCTION public.getting_started_trial_set(uuid, text, integer, text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.getting_started_trial_set(uuid, text, integer, text) TO authenticated, service_role;\n";
|
|
4
|
+
export declare const MIGRATION_003_THE_CLOCK_STARTS_ONCE_AND_ONLY_FOR_WHO_MAY = "-- 003_the_clock_starts_once_and_only_for_who_may.sql \u2014 the gates the audit found open.\n-- Functions and grants only; no new table. Depends on 000, 001 and 002.\n--\n-- * getting_started_start_trial: the tenant row is locked (FOR UPDATE), so two\n-- clicks cannot start twice and emit twice; a tenant with no trial window\n-- (trial_ends_at IS NULL \u2014 legacy or paying, db 044), an already converted\n-- journey or a paid plan is refused instead of being handed a clock that\n-- expires on them.\n-- * getting_started_minimums: a member of the tenant (or the platform\n-- operator of 002, or service_role) \u2014 not any signed-in user probing other\n-- tenants by id.\n-- * plg_getting_started_config: the client reads only the columns it needs;\n-- crm_tenant_slug is the platform's business.\n-- * the event handlers accept only what the RPCs wrote \u2014 plg_emit_event is\n-- open to any member, and a hand-made `trial_started` must not create a lead.\n\n-- \u2500\u2500 the minimums: only for who may ask \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.getting_started_minimums(p_tenant uuid)\nRETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_menu boolean := false;\n v_hours boolean := false;\n v_order boolean := false;\n v_settings jsonb;\n v_operator boolean := false;\nBEGIN\n IF p_tenant IS NULL THEN\n RAISE EXCEPTION 'getting_started_minimums: tenant is required' USING ERRCODE = '22023';\n END IF;\n -- service_role (jobs), a member of the tenant, or the platform operator (002).\n IF auth.role() IS DISTINCT FROM 'service_role' AND NOT app.is_member(p_tenant) THEN\n IF to_regprocedure('public.getting_started_is_platform_operator()') IS NOT NULL THEN\n EXECUTE 'SELECT public.getting_started_is_platform_operator()' INTO v_operator;\n END IF;\n IF NOT coalesce(v_operator, false) THEN\n RAISE EXCEPTION 'getting_started_minimums: not a member of this tenant' USING ERRCODE = '42501';\n END IF;\n END IF;\n\n SELECT t.settings INTO v_settings FROM public.tenants t WHERE t.id = p_tenant;\n v_hours := jsonb_typeof(v_settings #> '{store,hours}') = 'object'\n AND (v_settings #> '{store,hours}') <> '{}'::jsonb;\n -- A pool without plugin-menu must not break the read.\n IF to_regclass('public.plg_menu_items') IS NOT NULL THEN\n EXECUTE $q$SELECT EXISTS (\n SELECT 1 FROM public.plg_menu_items mi\n JOIN public.products p ON p.id = mi.product_id\n WHERE mi.tenant_id = $1 AND mi.status = 'available' AND p.is_active\n AND coalesce(p.metadata->>'demo','') <> 'true')$q$\n INTO v_menu USING p_tenant;\n END IF;\n SELECT EXISTS (\n SELECT 1 FROM public.orders o\n WHERE o.tenant_id = p_tenant AND o.status = 'completed'\n AND o.metadata @> '{\"test\":true}'::jsonb\n ) INTO v_order;\n RETURN jsonb_build_object(\n 'menu', v_menu, 'hours', v_hours, 'test_order', v_order,\n 'ready', v_menu AND v_hours AND v_order\n );\nEND $$;\nREVOKE ALL ON FUNCTION public.getting_started_minimums(uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.getting_started_minimums(uuid) TO authenticated, service_role;\n\n-- \u2500\u2500 start the trial: once, locked, and only for a tenant that can be on trial \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. `trial_ends_at IS NULL` is the legacy/paying tenant\n -- (db 044): no window, no trial.\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 IF v_t.trial_ends_at IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'no_trial_window', 'minimums', v_min);\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;\n";
|
|
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
|
+
export declare const MIGRATIONS: Array<{
|
|
7
|
+
id: string;
|
|
8
|
+
sql: string;
|
|
9
|
+
}>;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAMzD,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// node_modules/.cache/fayz-migration-stubs/main.ts
|
|
2
|
+
var MIGRATION_000_JOURNEY = "";
|
|
3
|
+
var MIGRATION_001_TRIAL_CLOCK = "";
|
|
4
|
+
var MIGRATION_002_PLATFORM_OPERATOR = "";
|
|
5
|
+
var MIGRATION_003_THE_CLOCK_STARTS_ONCE_AND_ONLY_FOR_WHO_MAY = "";
|
|
6
|
+
var MIGRATION_004_LEAD_KEEPS_THE_NAME = "";
|
|
7
|
+
var MIGRATIONS = [];
|
|
8
|
+
|
|
9
|
+
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 };
|
|
10
|
+
//# sourceMappingURL=main.browser.js.map
|
|
11
|
+
//# sourceMappingURL=main.browser.js.map
|
|
@@ -0,0 +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,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 MIGRATIONS: Array<{ id: string; sql: string }> = []\n"]}
|