@fayz-ai/db 0.13.0 → 0.15.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/canonical-schema.json +106 -23
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/schema/spine.d.ts +42 -37
- package/dist/schema/spine.d.ts.map +1 -1
- package/migrations/000_baseline.sql +29667 -0
- package/migrations/001_the_tenant_has_apps.sql +314 -0
- package/migrations/002_the_spine_drops_the_vendor_and_the_dead.sql +129 -0
- package/migrations/003_the_role_rpcs_finish_and_the_mirrors_go.sql +135 -0
- package/migrations/004_the_projections_get_a_successor.sql +312 -0
- package/migrations/005_the_archetypes_anchor_to_the_unit.sql +90 -0
- package/migrations/006_the_helpers_read_the_source.sql +153 -0
- package/migrations/007_the_compat_machinery_leaves_with_the_tables.sql +77 -0
- package/migrations/008_the_switchers_see_across_accounts.sql +150 -0
- package/migrations/009_the_compat_aliases_nobody_adopted.sql +57 -0
- package/migrations/010_the_new_tenant_is_born_with_its_trade.sql +201 -0
- package/migrations/011_leadcontrol_joins_the_family.sql +120 -0
- package/migrations/012_chefcontrol_wears_its_own_mark.sql +28 -0
- package/migrations/013_the_workspace_switch_reaches_the_other_half.sql +156 -0
- package/migrations/014_the_chooser_can_read_what_it_offers.sql +91 -0
- package/migrations/015_the_unit_bindings_belong_to_one_workspace.sql +86 -0
- package/migrations/016_the_rule_stops_asking_every_row.sql +543 -0
- package/migrations/017_the_pin_is_checked_against_the_right_workspace.sql +139 -0
- package/migrations/018_the_bootstrap_says_what_you_reach.sql +114 -0
- package/migrations/019_distributing_one_row_stops_costing_the_table.sql +245 -0
- package/migrations/020_the_agenda_asks_where_someone_works.sql +81 -0
- package/migrations/022_the_line_says_which_document_it_belongs_to.sql +109 -0
- package/migrations/029_o_administrador_enxerga_o_que_existe.sql +167 -0
- package/migrations/031_ticketcontrol_joins_the_family.sql +109 -0
- package/migrations/032_a_marca_e_um_conjunto_de_tokens.sql +291 -0
- package/migrations/041_a_entrega_tem_onde_guardar_a_politica.sql +53 -0
- package/migrations/042_o_token_de_marca_fecha_a_porta_anon.sql +17 -0
- package/migrations/043_fiscalcontrol_joins_the_family.sql +75 -0
- package/migrations/044_o_teste_gratis_dura_trinta_dias.sql +15 -0
- package/migrations/046_the_tick_advances_the_flows.sql +77 -0
- package/migrations/047_the_tick_sweeps_the_offers.sql +74 -0
- package/migrations/052_fullcontrol_joins_the_family.sql +213 -0
- package/migrations/053_o_erp_para_de_exigir_o_que_nao_desenha.sql +37 -0
- package/package.json +1 -1
- package/migrations/0000_legacy_quarantine.sql +0 -86
- package/migrations/0000b_straggler_sweep.sql +0 -37
- package/migrations/000_core_v1_convert.sql +0 -343
- package/migrations/001_core.sql +0 -372
- package/migrations/002_rls_user_tenant_ids.sql +0 -64
- package/migrations/003_invitations.sql +0 -17
- package/migrations/004_archetypes.sql +0 -343
- package/migrations/005_locations_archetype.sql +0 -18
- package/migrations/006_archetype_rls.sql +0 -74
- package/migrations/007_archetype_grants.sql +0 -4
- package/migrations/008_grants.sql +0 -5
- package/migrations/009_anon_hardening.sql +0 -19
- package/migrations/010_migration_ledger.sql +0 -46
- package/migrations/011_anon_write_revoke.sql +0 -12
- package/migrations/012_rename_migration_ledger.sql +0 -61
- package/migrations/013_sequences_rls.sql +0 -22
- package/migrations/014_plan_entitlements.sql +0 -28
- package/migrations/015_agent_guard.sql +0 -108
- package/migrations/016_agent_guard_actor.sql +0 -102
- package/migrations/017_core_addresses.sql +0 -115
- package/migrations/018_global_search.sql +0 -482
- package/migrations/019_tenant_members_person_id.sql +0 -20
- package/migrations/020_fayz_projects.sql +0 -63
- package/migrations/021_tenant_roles.sql +0 -74
- package/migrations/022_invite_acceptance.sql +0 -91
- package/migrations/023_team_visibility.sql +0 -75
- package/migrations/024_analytics_engine.sql +0 -317
- package/migrations/025_created_by.sql +0 -75
- package/migrations/026_audit_trail.sql +0 -73
- package/migrations/027_domain_events.sql +0 -266
- package/migrations/028_tenant_scoped_token.sql +0 -127
- package/migrations/029_connections.sql +0 -186
- package/migrations/030_effect_idempotency.sql +0 -159
- package/migrations/031_sync_run_message.sql +0 -39
- package/migrations/032_connection_secrets.sql +0 -227
- package/migrations/033_sync_schedule.sql +0 -651
- package/migrations/034_custom_fields.sql +0 -55
- package/migrations/035_field_registry.sql +0 -148
- package/migrations/036_analytics_run_batch.sql +0 -84
- package/migrations/037_sync_tick_one_at_a_time.sql +0 -256
- package/migrations/038_onboarding_responses.sql +0 -103
- package/migrations/039_unit_tree.sql +0 -270
- package/migrations/040_resource_grants.sql +0 -474
- package/migrations/041_scoped_columns.sql +0 -192
- package/migrations/042_unit_scope_policies.sql +0 -145
- package/migrations/043_view_invoker.sql +0 -90
- package/migrations/044_unit_member_facts.sql +0 -47
- package/migrations/045_unit_entry.sql +0 -236
- package/migrations/046_membership_visible_to_members.sql +0 -85
- package/migrations/047_tasks.sql +0 -266
- package/migrations/048_every_login_is_a_person.sql +0 -190
- package/migrations/049_bookable_people.sql +0 -126
- package/migrations/050_authz_core.sql +0 -230
- package/migrations/051_authz_helpers.sql +0 -330
- package/migrations/052_authz_fanout.sql +0 -253
- package/migrations/053_authz_catalog_templates.sql +0 -402
- package/migrations/060_tenancy_seam.sql +0 -132
- package/migrations/061_create_tenant.sql +0 -125
- package/migrations/062_invitations.sql +0 -329
- package/migrations/063_legacy_tenancy_retired.sql +0 -568
- package/migrations/064_agent_guard_authz.sql +0 -118
- package/migrations/064b_supersede_unit_scope.sql +0 -205
- package/migrations/065_scaffold_sweep.sql +0 -139
- package/migrations/066_shadow_report.sql +0 -122
- package/migrations/070_permission_catalog.sql +0 -205
- package/migrations/071_v1_permission_map.sql +0 -266
- package/migrations/072_role_rpcs.sql +0 -126
- package/migrations/073_legacy_rbac_retire.sql +0 -471
- package/migrations/075_company_legal_entities.sql +0 -169
- package/migrations/076_tenant_settings.sql +0 -251
- package/migrations/080_people_roles.sql +0 -269
- package/migrations/081_people_identity.sql +0 -271
- package/migrations/082_people_membership_link.sql +0 -142
- package/migrations/083_people_data_migration.sql +0 -75
- package/migrations/084_people_registrations_tokens.sql +0 -282
- package/migrations/085_catalog_products.sql +0 -333
- package/migrations/086_catalog_pricing.sql +0 -194
- package/migrations/087_catalog_packages.sql +0 -134
- package/migrations/090_orders_venda.sql +0 -166
- package/migrations/091_orders_scaffold.sql +0 -92
- package/migrations/095_config_field_rules.sql +0 -261
- package/migrations/096_audit_append_only.sql +0 -256
- package/migrations/097_sequences_authorized.sql +0 -111
- package/migrations/098_holidays.sql +0 -80
- package/migrations/099_access_grants.sql +0 -293
- package/migrations/100_migration_schema.sql +0 -217
- package/migrations/101_migration_allowlist.sql +0 -202
- package/migrations/102_migration_rpcs.sql +0 -933
- package/migrations/103_migration_reconcile_ponr.sql +0 -132
- package/migrations/105_policies_to_authenticated.sql +0 -71
- package/migrations/106_function_search_path.sql +0 -55
- package/migrations/107_core_table_prefix.sql +0 -361
- package/migrations/108_authz_write_guards.sql +0 -149
- package/migrations/109_catalog_migration_promoted_columns.sql +0 -126
- package/migrations/110_people_kind_vocabulary.sql +0 -120
- package/migrations/111_migration_excluded_is_a_decision.sql +0 -259
- package/migrations/111_replacing_a_view_keeps_its_dependents.sql +0 -132
- package/migrations/112_money_precision_standard.sql +0 -132
- package/migrations/113_addresses_units_companies.sql +0 -114
- package/migrations/114_migrations_ledger_immutable.sql +0 -65
- package/migrations/115_registration_honours_field_rules.sql +0 -172
- package/migrations/116_field_rules_check_needs_a_tenant.sql +0 -72
- package/migrations/117_authz_reconciliation_schedule.sql +0 -82
- package/migrations/118_migration_catalog_writers.sql +0 -450
- package/migrations/119_migration_config_writers.sql +0 -193
- package/migrations/120_migration_facet_writer.sql +0 -139
- package/migrations/121_migration_financial_writers.sql +0 -294
- package/migrations/122_migration_inventory_writers.sql +0 -245
- package/migrations/123_migration_order_item_writer.sql +0 -144
- package/migrations/124_migration_ledger_writers.sql +0 -170
- package/migrations/125_migration_authz_people_writers.sql +0 -205
- package/migrations/126_owner_scoping_is_configuration.sql +0 -247
- package/migrations/127_scaffold_audit_hook.sql +0 -98
- package/migrations/128_shadow_tables_enforced.sql +0 -93
- package/migrations/129_order_kind_terminal_states.sql +0 -57
- package/migrations/130_every_login_is_a_person.sql +0 -196
- package/migrations/131_migration_invoice_split.sql +0 -127
- package/migrations/132_audit_prune_scheduled.sql +0 -28
- package/migrations/133_unit_is_a_session_property.sql +0 -155
- package/migrations/134_field_rules_on_read.sql +0 -341
- package/migrations/135_distribution_polarity.sql +0 -158
- package/migrations/136_unit_id_means_locality.sql +0 -358
- package/migrations/137_vendor_named_spine_objects.sql +0 -126
- package/migrations/138_grants_inherit_down_the_tree.sql +0 -178
- package/migrations/139_distributable_is_a_scaffold_option.sql +0 -109
- package/migrations/140_masking_is_a_scaffold_option.sql +0 -85
- package/migrations/141_renamed_objects_keep_a_compat_name.sql +0 -192
- package/migrations/142_a_person_has_addresses.sql +0 -212
- package/migrations/143_a_professional_has_a_rate.sql +0 -75
- package/migrations/144_a_product_can_be_sold_in_sizes.sql +0 -57
- package/migrations/145_the_app_says_which_tenant_it_is_in.sql +0 -123
- package/migrations/146_products_stock_has_one_writer.sql +0 -67
- package/migrations/147_every_archetype_has_a_kind_registry.sql +0 -388
- package/migrations/148_the_legacy_archetype_stops_being_writable.sql +0 -154
- package/migrations/149_the_event_log_gets_a_consumer.sql +0 -331
- package/migrations/150_the_app_is_configured_in_the_database.sql +0 -285
- package/migrations/151_stock_balance_is_an_archetype.sql +0 -144
- package/migrations/152_a_file_is_an_archetype.sql +0 -222
- package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +0 -199
- package/migrations/154_the_plan_is_a_ceiling.sql +0 -258
- package/migrations/155_a_tenant_can_leave.sql +0 -271
- package/migrations/156_the_service_door_has_a_server_side.sql +0 -335
- package/migrations/157_nobody_creates_objects_in_public.sql +0 -74
- package/migrations/158_the_registry_owns_the_status_vocabulary.sql +0 -153
- package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +0 -132
- package/migrations/160_the_vertical_decides_what_starts_on.sql +0 -246
- package/migrations/161_a_tenant_leaves_with_its_data.sql +0 -363
- package/migrations/162_every_name_has_one_creator_for_real.sql +0 -126
- package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +0 -184
- package/migrations/164_the_sequence_table_joins_the_register.sql +0 -26
- package/migrations/165_a_view_is_not_a_way_in.sql +0 -90
- package/migrations/166_the_platform_billing_leftovers_go.sql +0 -72
- package/migrations/167_a_view_gets_back_what_its_table_grants.sql +0 -112
- package/migrations/168_the_grant_census_stops_crying_wolf.sql +0 -33
- package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +0 -45
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
-- 032_a_marca_e_um_conjunto_de_tokens.sql
|
|
2
|
+
--
|
|
3
|
+
-- A marca de um tenant tinha DUAS fontes de verdade e TRÊS grafias, e nenhuma
|
|
4
|
+
-- das três conversava com as outras.
|
|
5
|
+
--
|
|
6
|
+
-- `ConnectedBrandingSettings` GRAVA settings.branding.primaryColor (camelo)
|
|
7
|
+
-- `admin-app.tsx` LÊ settings.branding.primaryColor (camelo)
|
|
8
|
+
-- `app.branding_of()` LÊ settings.branding.primary_color (cobra)
|
|
9
|
+
--
|
|
10
|
+
-- Medido antes desta migration: **nenhum** tenant tinha a chave em camelo, e um
|
|
11
|
+
-- tinha a em cobra (posta por script). Ou seja: a cor que o lojista escolhe em
|
|
12
|
+
-- Configurações nunca chegou ao switcher nem à página pública, e a que existia
|
|
13
|
+
-- nunca chegou ao tema. A tela funcionava; o dado ia para uma gaveta que
|
|
14
|
+
-- ninguém abria.
|
|
15
|
+
--
|
|
16
|
+
-- E, por cima disso, o porte de CRM criou uma SEGUNDA marca — `plg_marketing_brand`
|
|
17
|
+
-- — com nome, tom de voz e palavras proibidas, dentro de um plugin. Duas marcas
|
|
18
|
+
-- para uma casa é pior que nenhuma: mudar a cor num lugar não muda no outro, e
|
|
19
|
+
-- ninguém sabe qual das duas o site está usando.
|
|
20
|
+
--
|
|
21
|
+
-- ── O QUE ESTA MIGRATION ESTABELECE ───────────────────────────────────────
|
|
22
|
+
--
|
|
23
|
+
-- Uma marca é UM CONJUNTO DE TOKENS com três camadas, e a terceira é a que
|
|
24
|
+
-- costuma ficar de fora:
|
|
25
|
+
--
|
|
26
|
+
-- identity quem a marca é nome, assinatura, posicionamento, logo
|
|
27
|
+
-- tokens como ela se parece cor e tipografia
|
|
28
|
+
-- voice como ela FALA tom, léxico, chamadas, palavras proibidas
|
|
29
|
+
--
|
|
30
|
+
-- Voz não é "uma funcionalidade de marketing" — é a dimensão VERBAL do mesmo
|
|
31
|
+
-- conjunto. Um sistema de design que descreve a cor do botão e não descreve o
|
|
32
|
+
-- que o produto pode dizer está descrevendo metade da marca. É por isso que ela
|
|
33
|
+
-- sai do plugin e entra aqui, ao lado da cor.
|
|
34
|
+
--
|
|
35
|
+
-- RETROCOMPATIBILIDADE É OBRIGATÓRIA, e não por educação: `app.branding_of` é
|
|
36
|
+
-- lida por `tenant_branding_public(slug, domain)`, que serve loja pública e
|
|
37
|
+
-- domínio próprio a visitante ANÔNIMO. Uma troca de forma que derrube isso tira
|
|
38
|
+
-- o site do ar de quem já vendeu hoje. Por isso o leitor passa a aceitar as
|
|
39
|
+
-- TRÊS grafias e a saída antiga continua idêntica — o documento em camadas vem
|
|
40
|
+
-- ao lado, não no lugar.
|
|
41
|
+
|
|
42
|
+
-- ── o leitor tolerante ─────────────────────────────────────────────────────
|
|
43
|
+
--
|
|
44
|
+
-- Uma chave, três lugares onde ela pode estar. A ordem é deliberada: o formato
|
|
45
|
+
-- NOVO primeiro, porque é para onde tudo caminha; depois cobra, que é o que o
|
|
46
|
+
-- resolvedor sempre leu; por último camelo, que é o que a tela sempre gravou.
|
|
47
|
+
CREATE OR REPLACE FUNCTION app.brand_token(p_branding jsonb, p_path text[], p_snake text, p_camel text)
|
|
48
|
+
RETURNS text
|
|
49
|
+
LANGUAGE sql
|
|
50
|
+
IMMUTABLE
|
|
51
|
+
SET search_path TO ''
|
|
52
|
+
AS $$
|
|
53
|
+
SELECT coalesce(
|
|
54
|
+
nullif(p_branding #>> p_path, ''),
|
|
55
|
+
nullif(p_branding ->> p_snake, ''),
|
|
56
|
+
nullif(p_branding ->> p_camel, '')
|
|
57
|
+
)
|
|
58
|
+
$$;
|
|
59
|
+
|
|
60
|
+
COMMENT ON FUNCTION app.brand_token(jsonb, text[], text, text) IS
|
|
61
|
+
'Lê um token de marca aceitando as três grafias que existiram: o caminho em camadas, cobra e camelo. A tolerância é o que permite mudar a forma sem derrubar a loja pública (032).';
|
|
62
|
+
|
|
63
|
+
-- ── a marca inteira, normalizada ───────────────────────────────────────────
|
|
64
|
+
CREATE OR REPLACE FUNCTION app.brand_of(p_tenant uuid)
|
|
65
|
+
RETURNS jsonb
|
|
66
|
+
LANGUAGE plpgsql
|
|
67
|
+
STABLE
|
|
68
|
+
SECURITY DEFINER
|
|
69
|
+
SET search_path TO ''
|
|
70
|
+
AS $$
|
|
71
|
+
DECLARE t record; b jsonb;
|
|
72
|
+
BEGIN
|
|
73
|
+
SELECT x.id, x.name, x.slug, x.logo_url, x.settings INTO t
|
|
74
|
+
FROM public.tenants x WHERE x.id = p_tenant;
|
|
75
|
+
IF t.id IS NULL THEN RETURN NULL; END IF;
|
|
76
|
+
|
|
77
|
+
b := coalesce(t.settings -> 'branding', '{}'::jsonb);
|
|
78
|
+
|
|
79
|
+
RETURN jsonb_strip_nulls(jsonb_build_object(
|
|
80
|
+
'identity', jsonb_build_object(
|
|
81
|
+
'name', coalesce(app.brand_token(b, '{identity,name}', 'app_name', 'appName'), t.name),
|
|
82
|
+
'tagline', app.brand_token(b, '{identity,tagline}', 'tagline', 'tagline'),
|
|
83
|
+
'mission', app.brand_token(b, '{identity,mission}', 'mission', 'mission'),
|
|
84
|
+
'logoUrl', coalesce(app.brand_token(b, '{identity,logoUrl}', 'logo_url', 'logoUrl'), t.logo_url),
|
|
85
|
+
'faviconUrl',app.brand_token(b, '{identity,faviconUrl}', 'favicon_url', 'faviconUrl'),
|
|
86
|
+
'customDomain', app.brand_token(b, '{identity,customDomain}', 'custom_domain', 'customDomain')
|
|
87
|
+
),
|
|
88
|
+
'tokens', jsonb_build_object(
|
|
89
|
+
'color', jsonb_strip_nulls(jsonb_build_object(
|
|
90
|
+
'primary', app.brand_token(b, '{tokens,color,primary}', 'primary_color', 'primaryColor'),
|
|
91
|
+
'accent', app.brand_token(b, '{tokens,color,accent}', 'accent_color', 'accentColor')
|
|
92
|
+
)),
|
|
93
|
+
'font', jsonb_strip_nulls(jsonb_build_object(
|
|
94
|
+
'heading', app.brand_token(b, '{tokens,font,heading}', 'heading_font', 'headingFont'),
|
|
95
|
+
'body', app.brand_token(b, '{tokens,font,body}', 'body_font', 'bodyFont')
|
|
96
|
+
))
|
|
97
|
+
),
|
|
98
|
+
-- A camada verbal. Arrays e não texto livre: o agente compara termo a
|
|
99
|
+
-- termo, e "evite gírias" não é comparável.
|
|
100
|
+
'voice', jsonb_strip_nulls(jsonb_build_object(
|
|
101
|
+
'tone', app.brand_token(b, '{voice,tone}', 'tone_of_voice', 'toneOfVoice'),
|
|
102
|
+
'lexicon', b #> '{voice,lexicon}',
|
|
103
|
+
'avoid', b #> '{voice,avoid}',
|
|
104
|
+
'ctas', b #> '{voice,ctas}',
|
|
105
|
+
'hashtags', b #> '{voice,hashtags}'
|
|
106
|
+
))
|
|
107
|
+
));
|
|
108
|
+
END $$;
|
|
109
|
+
|
|
110
|
+
COMMENT ON FUNCTION app.brand_of(uuid) IS
|
|
111
|
+
'A marca como conjunto de tokens em três camadas: identity, tokens (cor/tipografia) e voice. Voz é a dimensão VERBAL do mesmo conjunto, não uma funcionalidade de marketing (032).';
|
|
112
|
+
|
|
113
|
+
REVOKE ALL ON FUNCTION app.brand_of(uuid) FROM PUBLIC, anon;
|
|
114
|
+
|
|
115
|
+
-- ── o resolvedor antigo continua idêntico por fora ─────────────────────────
|
|
116
|
+
--
|
|
117
|
+
-- Mesmas chaves, mesma forma. O que muda é que ele passa a ENXERGAR o que a
|
|
118
|
+
-- tela sempre gravou: antes lia só `primary_color`, e a tela gravava
|
|
119
|
+
-- `primaryColor`. É esta linha que faz a cor escolhida em Configurações chegar
|
|
120
|
+
-- ao switcher e à loja pública pela primeira vez.
|
|
121
|
+
CREATE OR REPLACE FUNCTION app.branding_of(p_tenant uuid) RETURNS jsonb
|
|
122
|
+
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
|
123
|
+
SET search_path TO ''
|
|
124
|
+
AS $$
|
|
125
|
+
DECLARE t record; b jsonb;
|
|
126
|
+
BEGIN
|
|
127
|
+
SELECT x.id, x.name, x.slug, x.logo_url, x.settings INTO t FROM public.tenants x WHERE x.id = p_tenant;
|
|
128
|
+
IF t.id IS NULL THEN
|
|
129
|
+
RETURN jsonb_build_object('tenant_id', NULL, 'name', NULL, 'slug', NULL, 'logo_url', NULL, 'primary_color', NULL, 'custom_domain', NULL);
|
|
130
|
+
END IF;
|
|
131
|
+
b := coalesce(t.settings -> 'branding', '{}'::jsonb);
|
|
132
|
+
RETURN jsonb_build_object(
|
|
133
|
+
'tenant_id', t.id,
|
|
134
|
+
'name', coalesce(app.brand_token(b, '{identity,name}', 'app_name', 'appName'), t.name),
|
|
135
|
+
'slug', t.slug,
|
|
136
|
+
'logo_url', coalesce(app.brand_token(b, '{identity,logoUrl}', 'logo_url', 'logoUrl'), t.logo_url),
|
|
137
|
+
'primary_color', app.brand_token(b, '{tokens,color,primary}', 'primary_color', 'primaryColor'),
|
|
138
|
+
'custom_domain', app.brand_token(b, '{identity,customDomain}', 'custom_domain', 'customDomain')
|
|
139
|
+
);
|
|
140
|
+
EXCEPTION WHEN OTHERS THEN
|
|
141
|
+
RETURN jsonb_build_object('tenant_id', p_tenant, 'name', NULL, 'slug', NULL, 'logo_url', NULL, 'primary_color', NULL, 'custom_domain', NULL,
|
|
142
|
+
'error', 'branding unavailable');
|
|
143
|
+
END $$;
|
|
144
|
+
|
|
145
|
+
-- ── a marca, para quem está dentro ─────────────────────────────────────────
|
|
146
|
+
CREATE OR REPLACE FUNCTION public.tenant_brand() RETURNS jsonb
|
|
147
|
+
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
|
148
|
+
SET search_path TO ''
|
|
149
|
+
AS $$
|
|
150
|
+
BEGIN
|
|
151
|
+
RETURN coalesce(app.brand_of(app.current_tenant_id()), '{}'::jsonb);
|
|
152
|
+
EXCEPTION WHEN OTHERS THEN
|
|
153
|
+
RETURN '{}'::jsonb;
|
|
154
|
+
END $$;
|
|
155
|
+
|
|
156
|
+
COMMENT ON FUNCTION public.tenant_brand() IS
|
|
157
|
+
'A marca em camadas do tenant da sessão. Consumidores: tema da shell (tokens.color), agente (voice), páginas públicas (tudo) (032).';
|
|
158
|
+
|
|
159
|
+
REVOKE ALL ON FUNCTION public.tenant_brand() FROM PUBLIC, anon;
|
|
160
|
+
GRANT EXECUTE ON FUNCTION public.tenant_brand() TO authenticated;
|
|
161
|
+
GRANT EXECUTE ON FUNCTION public.tenant_brand() TO service_role;
|
|
162
|
+
|
|
163
|
+
-- ── escrever uma camada sem apagar as outras ───────────────────────────────
|
|
164
|
+
--
|
|
165
|
+
-- A tela salva por SEÇÃO — cores, ou voz, ou identidade. Um `update` que troca
|
|
166
|
+
-- `settings.branding` inteiro faria salvar a aba de cores apagar o tom de voz
|
|
167
|
+
-- que outra pessoa acabou de escrever. Fundir por caminho é o que impede isso.
|
|
168
|
+
CREATE OR REPLACE FUNCTION public.tenant_brand_patch(p_patch jsonb)
|
|
169
|
+
RETURNS jsonb
|
|
170
|
+
LANGUAGE plpgsql
|
|
171
|
+
SECURITY DEFINER
|
|
172
|
+
SET search_path TO ''
|
|
173
|
+
AS $$
|
|
174
|
+
DECLARE v_tenant uuid := app.current_tenant_id();
|
|
175
|
+
BEGIN
|
|
176
|
+
IF v_tenant IS NULL THEN
|
|
177
|
+
RAISE EXCEPTION 'tenant_brand_patch: sem tenant na sessão' USING ERRCODE = '42501';
|
|
178
|
+
END IF;
|
|
179
|
+
-- `settings.edit` e não `tenant.edit`: a marca é configuração da casa, e quem
|
|
180
|
+
-- mexe nela é quem mexe nas configurações.
|
|
181
|
+
IF NOT app.has_permission('settings.edit', NULL::uuid) THEN
|
|
182
|
+
RAISE EXCEPTION 'tenant_brand_patch: exige settings.edit' USING ERRCODE = '42501';
|
|
183
|
+
END IF;
|
|
184
|
+
|
|
185
|
+
UPDATE public.tenants t
|
|
186
|
+
SET settings = jsonb_set(
|
|
187
|
+
coalesce(t.settings, '{}'::jsonb),
|
|
188
|
+
'{branding}',
|
|
189
|
+
-- `||` funde raso, e é o que se quer POR CAMADA: mandar
|
|
190
|
+
-- {"voice":{...}} troca a voz inteira e não toca em tokens. Mandar
|
|
191
|
+
-- {"tokens":{"color":{...}}} trocaria `tokens` inteiro, então a tela
|
|
192
|
+
-- manda a camada `tokens` completa quando mexe em qualquer cor.
|
|
193
|
+
coalesce(t.settings -> 'branding', '{}'::jsonb) || coalesce(p_patch, '{}'::jsonb)
|
|
194
|
+
),
|
|
195
|
+
updated_at = now()
|
|
196
|
+
WHERE t.id = v_tenant;
|
|
197
|
+
|
|
198
|
+
RETURN coalesce(app.brand_of(v_tenant), '{}'::jsonb);
|
|
199
|
+
END $$;
|
|
200
|
+
|
|
201
|
+
COMMENT ON FUNCTION public.tenant_brand_patch(jsonb) IS
|
|
202
|
+
'Funde uma camada da marca sem apagar as outras — salvar a aba de cores não pode apagar o tom de voz de quem escreveu antes (032).';
|
|
203
|
+
|
|
204
|
+
REVOKE ALL ON FUNCTION public.tenant_brand_patch(jsonb) FROM PUBLIC, anon;
|
|
205
|
+
GRANT EXECUTE ON FUNCTION public.tenant_brand_patch(jsonb) TO authenticated;
|
|
206
|
+
GRANT EXECUTE ON FUNCTION public.tenant_brand_patch(jsonb) TO service_role;
|
|
207
|
+
|
|
208
|
+
-- ── a segunda marca vem para casa ──────────────────────────────────────────
|
|
209
|
+
--
|
|
210
|
+
-- `plg_marketing_brand` durou uma onda. Ela nasceu com o kit de marca do porte
|
|
211
|
+
-- do V1, num plugin, sem saber que o núcleo já tinha uma marca com resolvedor e
|
|
212
|
+
-- três consumidores. As linhas dela entram na camada certa e a tabela sai — não
|
|
213
|
+
-- fica como view: uma segunda porta para a mesma coisa é justamente o que esta
|
|
214
|
+
-- migration existe para desfazer.
|
|
215
|
+
--
|
|
216
|
+
-- A fusão preserva o que já estivesse em `settings.branding`: o kit do plugin
|
|
217
|
+
-- perde para a configuração do núcleo em caso de conflito, porque é a do núcleo
|
|
218
|
+
-- que o tema e a loja pública leem hoje.
|
|
219
|
+
DO $$
|
|
220
|
+
DECLARE r record; v_patch jsonb; v_n integer := 0;
|
|
221
|
+
BEGIN
|
|
222
|
+
IF to_regclass('public.plg_marketing_brand') IS NULL THEN RETURN; END IF;
|
|
223
|
+
|
|
224
|
+
FOR r IN SELECT * FROM public.plg_marketing_brand LOOP
|
|
225
|
+
v_patch := jsonb_strip_nulls(jsonb_build_object(
|
|
226
|
+
'identity', jsonb_strip_nulls(jsonb_build_object(
|
|
227
|
+
'name', nullif(btrim(coalesce(r.name, '')), ''),
|
|
228
|
+
'tagline', nullif(btrim(coalesce(r.tagline, '')), ''),
|
|
229
|
+
'mission', nullif(btrim(coalesce(r.mission, '')), ''),
|
|
230
|
+
'logoUrl', nullif(btrim(coalesce(r.logo_url, '')), '')
|
|
231
|
+
)),
|
|
232
|
+
-- A COR DO PLUGIN SÓ ENTRA SE O NÚCLEO NÃO TIVER UMA.
|
|
233
|
+
--
|
|
234
|
+
-- `||` funde raso e não sobrescreve caminho diferente: com a cor do
|
|
235
|
+
-- núcleo em `branding.primaryColor` (plana) e a do plugin em
|
|
236
|
+
-- `branding.tokens.color.primary` (em camadas), AS DUAS sobrevivem — e o
|
|
237
|
+
-- leitor tolerante prefere a forma nova, então a do plugin venceria.
|
|
238
|
+
-- Isto foi pego no container: `#FF0000` do plugin passou na frente do
|
|
239
|
+
-- `#2563EB` do núcleo. Quem manda é o núcleo, porque é o que o tema e a
|
|
240
|
+
-- loja pública leem hoje.
|
|
241
|
+
'tokens', jsonb_strip_nulls(jsonb_build_object(
|
|
242
|
+
'color', CASE
|
|
243
|
+
WHEN app.brand_token((SELECT x.settings -> 'branding' FROM public.tenants x WHERE x.id = r.tenant_id),
|
|
244
|
+
'{tokens,color,primary}', 'primary_color', 'primaryColor') IS NULL
|
|
245
|
+
THEN nullif(coalesce(r.colors, '{}'::jsonb), '{}'::jsonb)
|
|
246
|
+
END,
|
|
247
|
+
'font', nullif(coalesce(r.fonts, '{}'::jsonb), '{}'::jsonb)
|
|
248
|
+
)),
|
|
249
|
+
'voice', jsonb_strip_nulls(jsonb_build_object(
|
|
250
|
+
'tone', nullif(btrim(coalesce(r.tone_of_voice, '')), ''),
|
|
251
|
+
'lexicon', to_jsonb(coalesce(r.keywords, ARRAY[]::text[])),
|
|
252
|
+
'avoid', to_jsonb(coalesce(r.avoid_words, ARRAY[]::text[])),
|
|
253
|
+
'ctas', to_jsonb(coalesce(r.ctas, ARRAY[]::text[])),
|
|
254
|
+
'hashtags', to_jsonb(coalesce(r.hashtags, ARRAY[]::text[]))
|
|
255
|
+
))
|
|
256
|
+
));
|
|
257
|
+
|
|
258
|
+
UPDATE public.tenants t
|
|
259
|
+
SET settings = jsonb_set(
|
|
260
|
+
coalesce(t.settings, '{}'::jsonb), '{branding}',
|
|
261
|
+
-- `patch || existente` deixa o que já estava vencer NO MESMO
|
|
262
|
+
-- caminho. Para caminhos diferentes (plana × camadas) a defesa é
|
|
263
|
+
-- o CASE acima, na montagem do patch.
|
|
264
|
+
v_patch || coalesce(t.settings -> 'branding', '{}'::jsonb))
|
|
265
|
+
WHERE t.id = r.tenant_id;
|
|
266
|
+
v_n := v_n + 1;
|
|
267
|
+
END LOOP;
|
|
268
|
+
|
|
269
|
+
RAISE NOTICE '032: % kit(s) de marca trazidos do plugin para o núcleo', v_n;
|
|
270
|
+
END $$;
|
|
271
|
+
|
|
272
|
+
DROP TABLE IF EXISTS public.plg_marketing_brand;
|
|
273
|
+
DELETE FROM app.scaffold_registry WHERE table_name = 'plg_marketing_brand';
|
|
274
|
+
|
|
275
|
+
-- ── a grafia velha, normalizada de uma vez ─────────────────────────────────
|
|
276
|
+
--
|
|
277
|
+
-- O leitor tolera as três grafias para sempre — é o que protege quem tiver dado
|
|
278
|
+
-- antigo. Mas os tenants que existem HOJE ganham a forma nova agora, para que a
|
|
279
|
+
-- tela edite e leia o mesmo lugar desde o primeiro clique.
|
|
280
|
+
UPDATE public.tenants t
|
|
281
|
+
SET settings = jsonb_set(
|
|
282
|
+
coalesce(t.settings, '{}'::jsonb), '{branding}',
|
|
283
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
284
|
+
'tokens', jsonb_build_object('color', jsonb_strip_nulls(jsonb_build_object(
|
|
285
|
+
'primary', app.brand_token(t.settings -> 'branding', '{tokens,color,primary}', 'primary_color', 'primaryColor'),
|
|
286
|
+
'accent', app.brand_token(t.settings -> 'branding', '{tokens,color,accent}', 'accent_color', 'accentColor')
|
|
287
|
+
)))
|
|
288
|
+
)) || coalesce(t.settings -> 'branding', '{}'::jsonb))
|
|
289
|
+
WHERE t.settings -> 'branding' IS NOT NULL
|
|
290
|
+
AND app.brand_token(t.settings -> 'branding', '{tokens,color,primary}', 'primary_color', 'primaryColor') IS NOT NULL
|
|
291
|
+
AND t.settings #>> '{branding,tokens,color,primary}' IS NULL;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 041_a_entrega_tem_onde_guardar_a_politica.sql — o quadro de Delivery existe
|
|
3
|
+
-- desde sempre e a política dele não tinha onde morar.
|
|
4
|
+
--
|
|
5
|
+
-- ── O defeito ──────────────────────────────────────────────────────────────
|
|
6
|
+
-- Taxa de entrega, pedido mínimo, tempo estimado e raio são decisões da CASA:
|
|
7
|
+
-- mudam com a estação, com o combustível e com o bairro. Não estavam em lugar
|
|
8
|
+
-- nenhum — o app desenhava um interruptor de Delivery em `useState`, que volta
|
|
9
|
+
-- ao padrão a cada F5, e o resto era combinado por telefone.
|
|
10
|
+
--
|
|
11
|
+
-- ── A escolha ──────────────────────────────────────────────────────────────
|
|
12
|
+
-- `tenants.settings` (150/ADR 0023) já é onde a conta guarda o que muda sem
|
|
13
|
+
-- deploy, com `tenant_setting_set` cobrando `settings.manage` e conferindo o
|
|
14
|
+
-- tipo. Só faltava a chave existir: a função recusa chave não registrada, de
|
|
15
|
+
-- propósito, para que um erro de digitação no cliente não vire uma
|
|
16
|
+
-- configuração fantasma que ninguém lê.
|
|
17
|
+
--
|
|
18
|
+
-- Registradas com `reader = 'plugin:orders'`: quem as lê é o plugin de
|
|
19
|
+
-- pedidos, e é ele quem some junto se um dia o app não montar delivery.
|
|
20
|
+
-- ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
SELECT app.seed_setting_keys($$[
|
|
23
|
+
{
|
|
24
|
+
"key": "delivery.enabled", "type": "boolean", "default": true,
|
|
25
|
+
"reader": "plugin:orders",
|
|
26
|
+
"description": "Se a casa entrega. Desligado, o quadro de despacho segue existindo para o histórico e nenhum canal novo nasce como entrega."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"key": "delivery.fee", "type": "number", "default": 0,
|
|
30
|
+
"reader": "plugin:orders",
|
|
31
|
+
"description": "Taxa de entrega padrão, na moeda da casa. Zero = entrega sem taxa."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"key": "delivery.free_over", "type": "number", "default": null,
|
|
35
|
+
"reader": "plugin:orders",
|
|
36
|
+
"description": "Acima deste valor de pedido a taxa é zerada. Nulo = a taxa vale sempre."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"key": "delivery.min_order", "type": "number", "default": 0,
|
|
40
|
+
"reader": "plugin:orders",
|
|
41
|
+
"description": "Pedido mínimo para entregar. Zero = sem mínimo."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"key": "delivery.eta_minutes", "type": "integer", "default": 45,
|
|
45
|
+
"reader": "plugin:orders",
|
|
46
|
+
"description": "Tempo estimado de entrega, em minutos — o que o cliente ouve quando pergunta."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"key": "delivery.radius_km", "type": "number", "default": 0,
|
|
50
|
+
"reader": "plugin:orders",
|
|
51
|
+
"description": "Raio de entrega em quilômetros. Zero = sem limite declarado."
|
|
52
|
+
}
|
|
53
|
+
]$$::jsonb);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 042_o_token_de_marca_fecha_a_porta_anon.sql
|
|
3
|
+
--
|
|
4
|
+
-- A 032 criou app.brand_token como helper interno, mas funções novas do
|
|
5
|
+
-- PostgreSQL nascem executáveis por PUBLIC. Isso deixou `anon` chamar uma
|
|
6
|
+
-- função do schema privado `app` e fez a postura N15c ficar vermelha.
|
|
7
|
+
--
|
|
8
|
+
-- A correção não pode morar retroativamente na 032: os clusters ChefControl
|
|
9
|
+
-- já registraram o checksum original dela em public._migrations. Editar um
|
|
10
|
+
-- arquivo aplicado produz drift e faz `fayz db apply` parar antes de alcançar
|
|
11
|
+
-- qualquer correção posterior. Por isso a 032 mantém seus bytes históricos e
|
|
12
|
+
-- esta migration nova fecha a permissão nos bancos existentes.
|
|
13
|
+
-- ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
REVOKE ALL
|
|
16
|
+
ON FUNCTION app.brand_token(jsonb, text[], text, text)
|
|
17
|
+
FROM PUBLIC, anon;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 043_fiscalcontrol_joins_the_family.sql — a contabilidade ganha uma linha.
|
|
3
|
+
--
|
|
4
|
+
-- O oitavo produto do registro, e o segundo que é FUNÇÃO e não trade (como o
|
|
5
|
+
-- LeadControl, 011): FiscalControl é o escritório de contabilidade da família.
|
|
6
|
+
-- Todo tenant que opera um Control já produz o material contábil sem saber —
|
|
7
|
+
-- partida dobrada em plg_financial_movements, balancete em
|
|
8
|
+
-- v_financial_trial_balance, imposto por linha em plg_financial_invoice_item_taxes.
|
|
9
|
+
-- O que falta é o app que olha esse material como um contador olha: plano de
|
|
10
|
+
-- contas, lançamento, DRE, apuração. Com a reforma tributária tornando o
|
|
11
|
+
-- destaque de IBS/CBS obrigatório em agosto/2026, esse olhar deixa de ser
|
|
12
|
+
-- opcional para qualquer tenant fora do Simples.
|
|
13
|
+
--
|
|
14
|
+
-- ── `serves_verticals` fica VAZIO, de propósito ────────────────────────────
|
|
15
|
+
-- Contabilidade não é um ramo, é uma função que atravessa todos. Semear por
|
|
16
|
+
-- vertical (010) daria um app de contador para todo restaurante novo — uma tela
|
|
17
|
+
-- de balancete que ninguém pediu. Este é opt-in: quem liga é `tenant_app_set`
|
|
18
|
+
-- ou a mão da casa.
|
|
19
|
+
--
|
|
20
|
+
-- ── `requires` nomeia um plugin só ─────────────────────────────────────────
|
|
21
|
+
-- `financial` é o razão inteiro: invoices, movimentos, plano de contas, caixa.
|
|
22
|
+
-- Sem ele o rail é Painel e placeholders. `reports` e `fiscal-br` entram como
|
|
23
|
+
-- opt-in quando os marcos M3/M4 fecharem — a migration que criar as telas é a
|
|
24
|
+
-- que atualiza este array (mesma regra da 031).
|
|
25
|
+
-- ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
-- ── §1 a linha do registro ────────────────────────────────────────────────
|
|
28
|
+
--
|
|
29
|
+
-- #334155 (slate): o único setor sóbrio ainda livre na roda — os oito vizinhos
|
|
30
|
+
-- já tomaram vermelho, ouro, verde, dois roxos, azul e teal. Combina com o que
|
|
31
|
+
-- o app é: o produto sério da família. Branco sobre ele dá 9,6:1.
|
|
32
|
+
--
|
|
33
|
+
-- `url` é a origem de dev enquanto o app não é publicado; a coluna é a
|
|
34
|
+
-- allow-list do handoff, e o coalesce impede que uma reaplicação devolva uma
|
|
35
|
+
-- frota publicada para localhost (regra da 028/031).
|
|
36
|
+
INSERT INTO app.apps (id, name, icon, accent_color, url, requires, serves_verticals, active) VALUES
|
|
37
|
+
('fiscal', 'FiscalControl', 'Calculator', '#334155', 'http://localhost:5313',
|
|
38
|
+
ARRAY['financial'],
|
|
39
|
+
ARRAY[]::text[], true)
|
|
40
|
+
ON CONFLICT (id) DO UPDATE
|
|
41
|
+
SET name = EXCLUDED.name, icon = EXCLUDED.icon, accent_color = EXCLUDED.accent_color,
|
|
42
|
+
requires = EXCLUDED.requires, active = true,
|
|
43
|
+
url = coalesce(app.apps.url, EXCLUDED.url);
|
|
44
|
+
|
|
45
|
+
-- ── §2 quem recebe agora ──────────────────────────────────────────────────
|
|
46
|
+
--
|
|
47
|
+
-- Só as contas do dogfood que JÁ têm o financeiro ativo — a evidência honesta
|
|
48
|
+
-- de que existe um razão para o contador ler. O app está em M0; distribuir para
|
|
49
|
+
-- o cluster inteiro seria entregar um produto pela metade (regra da 031). O
|
|
50
|
+
-- tenant ControlGroup, criado fora desta migration por platform_tenant_create,
|
|
51
|
+
-- entra pelo p_apps da própria chamada.
|
|
52
|
+
INSERT INTO app.tenant_apps (tenant_id, app_id, status, source)
|
|
53
|
+
SELECT DISTINCT m.tenant_id, 'fiscal', 'active', 'manual'
|
|
54
|
+
FROM app.memberships m
|
|
55
|
+
JOIN auth.users u ON u.id = m.user_id
|
|
56
|
+
WHERE u.email = 'creators@fayalabs.com'
|
|
57
|
+
AND m.active
|
|
58
|
+
AND EXISTS (
|
|
59
|
+
SELECT 1 FROM app.tenant_plugins tp
|
|
60
|
+
WHERE tp.tenant_id = m.tenant_id AND tp.plugin_id = 'financial'
|
|
61
|
+
AND tp.facet = '' AND tp.status = 'active'
|
|
62
|
+
)
|
|
63
|
+
ON CONFLICT (tenant_id, app_id) DO NOTHING;
|
|
64
|
+
|
|
65
|
+
-- E o plugin que ele precisa, para exatamente essas contas. O teto do plano
|
|
66
|
+
-- ganha do padrão, sempre.
|
|
67
|
+
INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
|
|
68
|
+
SELECT DISTINCT ta.tenant_id, r.plugin_id, '', 'active', 'default'
|
|
69
|
+
FROM app.tenant_apps ta
|
|
70
|
+
JOIN app.apps a ON a.id = ta.app_id
|
|
71
|
+
CROSS JOIN LATERAL unnest(a.requires) AS r(plugin_id)
|
|
72
|
+
WHERE ta.app_id = 'fiscal'
|
|
73
|
+
AND ta.status = 'active'
|
|
74
|
+
AND public.plan_permits(r.plugin_id, '', ta.tenant_id)
|
|
75
|
+
ON CONFLICT (tenant_id, plugin_id, facet) DO NOTHING;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 044_o_teste_gratis_dura_trinta_dias.sql
|
|
3
|
+
--
|
|
4
|
+
-- A família Control abre para signup público e a promessa comercial passa a
|
|
5
|
+
-- ser "30 dias grátis" — 7 dias não dão para um restaurante montar cardápio,
|
|
6
|
+
-- treinar a equipe e rodar um mês de pedidos.
|
|
7
|
+
--
|
|
8
|
+
-- O prazo vive num único lugar: o DEFAULT da coluna. Só tenants NOVOS são
|
|
9
|
+
-- afetados; quem já tem data estampada mantém a sua, e quem nasceu antes do
|
|
10
|
+
-- trial (NULL) continua sem trial — nunca "expirado". O trigger
|
|
11
|
+
-- tenants_trial_is_readonly segue impedindo escrita client-side na data.
|
|
12
|
+
-- ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
ALTER TABLE public.tenants
|
|
15
|
+
ALTER COLUMN trial_ends_at SET DEFAULT (now() + interval '30 days');
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
-- 046_the_tick_advances_the_flows.sql
|
|
2
|
+
--
|
|
3
|
+
-- A cadence needs a clock, and the database already has one: `fayz_event_runner`
|
|
4
|
+
-- is a pg_cron job calling `event_runner_tick(200)` every minute. What it does
|
|
5
|
+
-- not have is a call to the thing that advances a multi-step flow.
|
|
6
|
+
--
|
|
7
|
+
-- Fifth call, same shape as the other four: guarded by `to_regprocedure`, so a
|
|
8
|
+
-- pool without plugin-automations ticks exactly as it does today. The guard is
|
|
9
|
+
-- the whole reason this belongs in the spine rather than in the plugin — the
|
|
10
|
+
-- tick is one function, and a plugin cannot append to it.
|
|
11
|
+
--
|
|
12
|
+
-- ORDER MATTERS, AND IT IS DELIBERATE. The flows run AFTER `notifications_drain`
|
|
13
|
+
-- and not before. A step enqueues with `scheduled_for = now()`, so draining
|
|
14
|
+
-- first means a step enqueued on this tick waits for the next one — sixty
|
|
15
|
+
-- seconds, once, at the start of a cadence that spans days. Running the flows
|
|
16
|
+
-- first would instead let a step be enqueued and drained in the same tick,
|
|
17
|
+
-- which sounds better and is not: the drain would be doing work the advisory
|
|
18
|
+
-- lock is already holding the whole database's tick for, and a slow provider
|
|
19
|
+
-- would then delay every other plugin's due work behind it. Cheap latency,
|
|
20
|
+
-- bought with predictable tick length.
|
|
21
|
+
--
|
|
22
|
+
-- The limit is 50 rather than the caller's `p_limit`. A cadence step is a write
|
|
23
|
+
-- plus an enqueue; two hundred of them in one tick is a long transaction under
|
|
24
|
+
-- an advisory lock that stops everything else. `marketing_run_due(20)` made the
|
|
25
|
+
-- same trade for the same reason.
|
|
26
|
+
|
|
27
|
+
CREATE OR REPLACE FUNCTION public.event_runner_tick(p_limit integer DEFAULT 200) RETURNS jsonb
|
|
28
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
29
|
+
SET search_path TO ''
|
|
30
|
+
AS $_$
|
|
31
|
+
DECLARE
|
|
32
|
+
v_lock boolean;
|
|
33
|
+
v_events record;
|
|
34
|
+
v_notif record;
|
|
35
|
+
v_out jsonb := '{}'::jsonb;
|
|
36
|
+
v_extra jsonb;
|
|
37
|
+
v_start timestamptz := clock_timestamp();
|
|
38
|
+
BEGIN
|
|
39
|
+
-- 1 tick at a time per database. A second caller returns immediately rather
|
|
40
|
+
-- than waiting, so a cron that fires while the previous one is still going
|
|
41
|
+
-- does not queue up behind it.
|
|
42
|
+
SELECT pg_try_advisory_xact_lock(hashtextextended('fayz.event_runner', 0)) INTO v_lock;
|
|
43
|
+
IF NOT v_lock THEN
|
|
44
|
+
RETURN jsonb_build_object('skipped', 'another tick is running');
|
|
45
|
+
END IF;
|
|
46
|
+
|
|
47
|
+
SELECT * INTO v_events FROM public.consume_event_log(p_limit);
|
|
48
|
+
v_out := v_out || jsonb_build_object('events', to_jsonb(v_events));
|
|
49
|
+
|
|
50
|
+
-- The notification queue drains in its OWN pass. A provider timing out costs
|
|
51
|
+
-- a retry there and must never hold up the event log — which is why these are
|
|
52
|
+
-- two functions and not one loop.
|
|
53
|
+
IF to_regprocedure('public.notifications_drain(integer)') IS NOT NULL THEN
|
|
54
|
+
EXECUTE 'SELECT * FROM public.notifications_drain($1)' INTO v_notif USING p_limit;
|
|
55
|
+
v_out := v_out || jsonb_build_object('notifications', to_jsonb(v_notif));
|
|
56
|
+
END IF;
|
|
57
|
+
|
|
58
|
+
-- Campaigns, scheduled reports and cadence flows ENQUEUE; they never send. So
|
|
59
|
+
-- they run on the same tick as the log and drain through the same
|
|
60
|
+
-- notification queue.
|
|
61
|
+
IF to_regprocedure('public.marketing_run_due(integer)') IS NOT NULL THEN
|
|
62
|
+
EXECUTE 'SELECT public.marketing_run_due(20)' INTO v_extra;
|
|
63
|
+
v_out := v_out || jsonb_build_object('marketing', v_extra);
|
|
64
|
+
END IF;
|
|
65
|
+
|
|
66
|
+
IF to_regprocedure('public.reports_deliver_due(integer)') IS NOT NULL THEN
|
|
67
|
+
EXECUTE 'SELECT public.reports_deliver_due(50)' INTO v_extra;
|
|
68
|
+
v_out := v_out || jsonb_build_object('reports', v_extra);
|
|
69
|
+
END IF;
|
|
70
|
+
|
|
71
|
+
IF to_regprocedure('public.automations_flows_run_due(integer)') IS NOT NULL THEN
|
|
72
|
+
EXECUTE 'SELECT public.automations_flows_run_due(50)' INTO v_extra;
|
|
73
|
+
v_out := v_out || jsonb_build_object('flows', v_extra);
|
|
74
|
+
END IF;
|
|
75
|
+
|
|
76
|
+
RETURN v_out || jsonb_build_object('took_ms', (extract(epoch FROM clock_timestamp() - v_start) * 1000)::integer);
|
|
77
|
+
END $_$;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
-- 047_the_tick_sweeps_the_offers.sql
|
|
2
|
+
--
|
|
3
|
+
-- Sixth call on the minute tick: expiring a lead offer that nobody took.
|
|
4
|
+
--
|
|
5
|
+
-- A five-minute window means the sweep's resolution has to be finer than the
|
|
6
|
+
-- window, and one minute is what the runner already gives. A rep therefore gets
|
|
7
|
+
-- their full five minutes and loses the lead within sixty seconds of the window
|
|
8
|
+
-- closing — which is the behaviour the rota is sold on.
|
|
9
|
+
--
|
|
10
|
+
-- It runs LAST, after the flows. Both write, and the offer sweep can cascade —
|
|
11
|
+
-- one expiry creating the next offer — so it is the call most likely to be slow
|
|
12
|
+
-- on a busy tenant. Last means a slow sweep delays nothing but itself.
|
|
13
|
+
--
|
|
14
|
+
-- The whole function is restated because a plugin cannot append to it, and
|
|
15
|
+
-- `CREATE OR REPLACE` needs the entire body. Migration 046 added the flows call
|
|
16
|
+
-- immediately before this one; if this file is ever reordered ahead of it, the
|
|
17
|
+
-- flows call disappears.
|
|
18
|
+
|
|
19
|
+
CREATE OR REPLACE FUNCTION public.event_runner_tick(p_limit integer DEFAULT 200) RETURNS jsonb
|
|
20
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
21
|
+
SET search_path TO ''
|
|
22
|
+
AS $_$
|
|
23
|
+
DECLARE
|
|
24
|
+
v_lock boolean;
|
|
25
|
+
v_events record;
|
|
26
|
+
v_notif record;
|
|
27
|
+
v_out jsonb := '{}'::jsonb;
|
|
28
|
+
v_extra jsonb;
|
|
29
|
+
v_start timestamptz := clock_timestamp();
|
|
30
|
+
BEGIN
|
|
31
|
+
-- 1 tick at a time per database. A second caller returns immediately rather
|
|
32
|
+
-- than waiting, so a cron that fires while the previous one is still going
|
|
33
|
+
-- does not queue up behind it.
|
|
34
|
+
SELECT pg_try_advisory_xact_lock(hashtextextended('fayz.event_runner', 0)) INTO v_lock;
|
|
35
|
+
IF NOT v_lock THEN
|
|
36
|
+
RETURN jsonb_build_object('skipped', 'another tick is running');
|
|
37
|
+
END IF;
|
|
38
|
+
|
|
39
|
+
SELECT * INTO v_events FROM public.consume_event_log(p_limit);
|
|
40
|
+
v_out := v_out || jsonb_build_object('events', to_jsonb(v_events));
|
|
41
|
+
|
|
42
|
+
-- The notification queue drains in its OWN pass. A provider timing out costs
|
|
43
|
+
-- a retry there and must never hold up the event log — which is why these are
|
|
44
|
+
-- two functions and not one loop.
|
|
45
|
+
IF to_regprocedure('public.notifications_drain(integer)') IS NOT NULL THEN
|
|
46
|
+
EXECUTE 'SELECT * FROM public.notifications_drain($1)' INTO v_notif USING p_limit;
|
|
47
|
+
v_out := v_out || jsonb_build_object('notifications', to_jsonb(v_notif));
|
|
48
|
+
END IF;
|
|
49
|
+
|
|
50
|
+
-- Campaigns, scheduled reports and cadence flows ENQUEUE; they never send. So
|
|
51
|
+
-- they run on the same tick as the log and drain through the same
|
|
52
|
+
-- notification queue.
|
|
53
|
+
IF to_regprocedure('public.marketing_run_due(integer)') IS NOT NULL THEN
|
|
54
|
+
EXECUTE 'SELECT public.marketing_run_due(20)' INTO v_extra;
|
|
55
|
+
v_out := v_out || jsonb_build_object('marketing', v_extra);
|
|
56
|
+
END IF;
|
|
57
|
+
|
|
58
|
+
IF to_regprocedure('public.reports_deliver_due(integer)') IS NOT NULL THEN
|
|
59
|
+
EXECUTE 'SELECT public.reports_deliver_due(50)' INTO v_extra;
|
|
60
|
+
v_out := v_out || jsonb_build_object('reports', v_extra);
|
|
61
|
+
END IF;
|
|
62
|
+
|
|
63
|
+
IF to_regprocedure('public.automations_flows_run_due(integer)') IS NOT NULL THEN
|
|
64
|
+
EXECUTE 'SELECT public.automations_flows_run_due(50)' INTO v_extra;
|
|
65
|
+
v_out := v_out || jsonb_build_object('flows', v_extra);
|
|
66
|
+
END IF;
|
|
67
|
+
|
|
68
|
+
IF to_regprocedure('public.crm_offers_sweep(integer)') IS NOT NULL THEN
|
|
69
|
+
EXECUTE 'SELECT public.crm_offers_sweep(100)' INTO v_extra;
|
|
70
|
+
v_out := v_out || jsonb_build_object('offers', v_extra);
|
|
71
|
+
END IF;
|
|
72
|
+
|
|
73
|
+
RETURN v_out || jsonb_build_object('took_ms', (extract(epoch FROM clock_timestamp() - v_start) * 1000)::integer);
|
|
74
|
+
END $_$;
|