@fayz-ai/db 0.11.0 → 0.13.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 +49 -0
- package/migrations/010_migration_ledger.sql +11 -1
- package/migrations/037_sync_tick_one_at_a_time.sql +256 -0
- package/migrations/038_onboarding_responses.sql +103 -0
- package/migrations/039_unit_tree.sql +270 -0
- package/migrations/040_resource_grants.sql +474 -0
- package/migrations/041_scoped_columns.sql +192 -0
- package/migrations/042_unit_scope_policies.sql +145 -0
- package/migrations/043_view_invoker.sql +90 -0
- package/migrations/044_unit_member_facts.sql +47 -0
- package/migrations/045_unit_entry.sql +236 -0
- package/migrations/046_membership_visible_to_members.sql +85 -0
- package/migrations/047_tasks.sql +266 -0
- package/migrations/048_every_login_is_a_person.sql +190 -0
- package/migrations/049_bookable_people.sql +126 -0
- package/migrations/050_authz_core.sql +230 -0
- package/migrations/051_authz_helpers.sql +330 -0
- package/migrations/052_authz_fanout.sql +253 -0
- package/migrations/053_authz_catalog_templates.sql +402 -0
- package/migrations/060_tenancy_seam.sql +132 -0
- package/migrations/061_create_tenant.sql +125 -0
- package/migrations/062_invitations.sql +329 -0
- package/migrations/063_legacy_tenancy_retired.sql +568 -0
- package/migrations/064_agent_guard_authz.sql +118 -0
- package/migrations/064b_supersede_unit_scope.sql +205 -0
- package/migrations/065_scaffold_sweep.sql +139 -0
- package/migrations/066_shadow_report.sql +122 -0
- package/migrations/070_permission_catalog.sql +205 -0
- package/migrations/071_v1_permission_map.sql +266 -0
- package/migrations/072_role_rpcs.sql +126 -0
- package/migrations/073_legacy_rbac_retire.sql +471 -0
- package/migrations/075_company_legal_entities.sql +169 -0
- package/migrations/076_tenant_settings.sql +251 -0
- package/migrations/080_people_roles.sql +269 -0
- package/migrations/081_people_identity.sql +271 -0
- package/migrations/082_people_membership_link.sql +142 -0
- package/migrations/083_people_data_migration.sql +75 -0
- package/migrations/084_people_registrations_tokens.sql +282 -0
- package/migrations/085_catalog_products.sql +333 -0
- package/migrations/086_catalog_pricing.sql +194 -0
- package/migrations/087_catalog_packages.sql +134 -0
- package/migrations/090_orders_venda.sql +166 -0
- package/migrations/091_orders_scaffold.sql +92 -0
- package/migrations/095_config_field_rules.sql +261 -0
- package/migrations/096_audit_append_only.sql +256 -0
- package/migrations/097_sequences_authorized.sql +111 -0
- package/migrations/098_holidays.sql +80 -0
- package/migrations/099_access_grants.sql +293 -0
- package/migrations/100_migration_schema.sql +217 -0
- package/migrations/101_migration_allowlist.sql +202 -0
- package/migrations/102_migration_rpcs.sql +933 -0
- package/migrations/103_migration_reconcile_ponr.sql +132 -0
- package/migrations/105_policies_to_authenticated.sql +71 -0
- package/migrations/106_function_search_path.sql +55 -0
- package/migrations/107_core_table_prefix.sql +361 -0
- package/migrations/108_authz_write_guards.sql +149 -0
- package/migrations/109_catalog_migration_promoted_columns.sql +126 -0
- package/migrations/110_people_kind_vocabulary.sql +120 -0
- package/migrations/111_migration_excluded_is_a_decision.sql +259 -0
- package/migrations/111_replacing_a_view_keeps_its_dependents.sql +132 -0
- package/migrations/112_money_precision_standard.sql +132 -0
- package/migrations/113_addresses_units_companies.sql +114 -0
- package/migrations/114_migrations_ledger_immutable.sql +65 -0
- package/migrations/115_registration_honours_field_rules.sql +172 -0
- package/migrations/116_field_rules_check_needs_a_tenant.sql +72 -0
- package/migrations/117_authz_reconciliation_schedule.sql +82 -0
- package/migrations/118_migration_catalog_writers.sql +450 -0
- package/migrations/119_migration_config_writers.sql +193 -0
- package/migrations/120_migration_facet_writer.sql +139 -0
- package/migrations/121_migration_financial_writers.sql +294 -0
- package/migrations/122_migration_inventory_writers.sql +245 -0
- package/migrations/123_migration_order_item_writer.sql +144 -0
- package/migrations/124_migration_ledger_writers.sql +170 -0
- package/migrations/125_migration_authz_people_writers.sql +205 -0
- package/migrations/126_owner_scoping_is_configuration.sql +247 -0
- package/migrations/127_scaffold_audit_hook.sql +98 -0
- package/migrations/128_shadow_tables_enforced.sql +93 -0
- package/migrations/129_order_kind_terminal_states.sql +57 -0
- package/migrations/130_every_login_is_a_person.sql +196 -0
- package/migrations/131_migration_invoice_split.sql +127 -0
- package/migrations/132_audit_prune_scheduled.sql +28 -0
- package/migrations/133_unit_is_a_session_property.sql +155 -0
- package/migrations/134_field_rules_on_read.sql +341 -0
- package/migrations/135_distribution_polarity.sql +158 -0
- package/migrations/136_unit_id_means_locality.sql +358 -0
- package/migrations/137_vendor_named_spine_objects.sql +126 -0
- package/migrations/138_grants_inherit_down_the_tree.sql +178 -0
- package/migrations/139_distributable_is_a_scaffold_option.sql +109 -0
- package/migrations/140_masking_is_a_scaffold_option.sql +85 -0
- package/migrations/141_renamed_objects_keep_a_compat_name.sql +192 -0
- package/migrations/142_a_person_has_addresses.sql +212 -0
- package/migrations/143_a_professional_has_a_rate.sql +75 -0
- package/migrations/144_a_product_can_be_sold_in_sizes.sql +57 -0
- package/migrations/145_the_app_says_which_tenant_it_is_in.sql +123 -0
- package/migrations/146_products_stock_has_one_writer.sql +67 -0
- package/migrations/147_every_archetype_has_a_kind_registry.sql +388 -0
- package/migrations/148_the_legacy_archetype_stops_being_writable.sql +154 -0
- package/migrations/149_the_event_log_gets_a_consumer.sql +331 -0
- package/migrations/150_the_app_is_configured_in_the_database.sql +285 -0
- package/migrations/151_stock_balance_is_an_archetype.sql +144 -0
- package/migrations/152_a_file_is_an_archetype.sql +222 -0
- package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +199 -0
- package/migrations/154_the_plan_is_a_ceiling.sql +258 -0
- package/migrations/155_a_tenant_can_leave.sql +271 -0
- package/migrations/156_the_service_door_has_a_server_side.sql +335 -0
- package/migrations/157_nobody_creates_objects_in_public.sql +74 -0
- package/migrations/158_the_registry_owns_the_status_vocabulary.sql +153 -0
- package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +132 -0
- package/migrations/160_the_vertical_decides_what_starts_on.sql +246 -0
- package/migrations/161_a_tenant_leaves_with_its_data.sql +363 -0
- package/migrations/162_every_name_has_one_creator_for_real.sql +126 -0
- package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +184 -0
- package/migrations/164_the_sequence_table_joins_the_register.sql +26 -0
- package/migrations/165_a_view_is_not_a_way_in.sql +90 -0
- package/migrations/166_the_platform_billing_leftovers_go.sql +72 -0
- package/migrations/167_a_view_gets_back_what_its_table_grants.sql +112 -0
- package/migrations/168_the_grant_census_stops_crying_wolf.sql +33 -0
- package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +45 -0
- package/package.json +6 -5
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 057_tasks.sql — public.tasks: the one table admin work lives in.
|
|
3
|
+
--
|
|
4
|
+
-- WHY THIS IS SPINE AND NOT A PLUGIN TABLE. plugin-tasks has carried
|
|
5
|
+
-- plg_tasks_tasks since its first release, with almost exactly these columns.
|
|
6
|
+
-- But only three of the shipped apps install that plugin, and the shell itself
|
|
7
|
+
-- now writes tasks: the workspace setup wizard turns its "getting started"
|
|
8
|
+
-- checklist into rows, assigned to whoever ran it. A checklist that silently
|
|
9
|
+
-- writes nothing on the apps without the plugin is not a feature. So the table
|
|
10
|
+
-- moves to the spine, where every app has it, and plugin-tasks becomes one of
|
|
11
|
+
-- several UIs over it (plugin migration 002 carries its rows across).
|
|
12
|
+
--
|
|
13
|
+
-- WHY `source` / `source_key` AND NOT A LABEL. plg_tasks_tasks marks a
|
|
14
|
+
-- machine-made task by pushing a string into `labels` (the notes bridge writes
|
|
15
|
+
-- 'note:<id>'). That works only because nothing renders it: TaskLabelBadge
|
|
16
|
+
-- returns null for any id that is not a real label row. A column that says who
|
|
17
|
+
-- put the row here is filterable, indexable and visible, and the unique index
|
|
18
|
+
-- below is what makes seeding idempotent — a wizard run twice writes the same
|
|
19
|
+
-- N rows, not 2N.
|
|
20
|
+
--
|
|
21
|
+
-- Idempotent.
|
|
22
|
+
-- ============================================================================
|
|
23
|
+
|
|
24
|
+
CREATE TABLE IF NOT EXISTS public.tasks (
|
|
25
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
26
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
27
|
+
|
|
28
|
+
title text NOT NULL,
|
|
29
|
+
description text,
|
|
30
|
+
|
|
31
|
+
status text NOT NULL DEFAULT 'todo'
|
|
32
|
+
CHECK (status IN ('todo', 'in_progress', 'done', 'cancelled')),
|
|
33
|
+
priority text NOT NULL DEFAULT 'medium'
|
|
34
|
+
CHECK (priority IN ('low', 'medium', 'high', 'urgent')),
|
|
35
|
+
|
|
36
|
+
due_date date,
|
|
37
|
+
-- When it was closed, as distinct from `status`. The onboarding checklist
|
|
38
|
+
-- reads it as a latch: a step whose sensor once fired is never re-probed, so
|
|
39
|
+
-- reopening a task by hand actually sticks.
|
|
40
|
+
completed_at timestamptz,
|
|
41
|
+
|
|
42
|
+
-- Who answers for it. Kept alongside a denormalised name because the panel
|
|
43
|
+
-- draws initials for people the reader may not be allowed to look up.
|
|
44
|
+
assigned_to_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
45
|
+
assigned_to_name text,
|
|
46
|
+
|
|
47
|
+
parent_id uuid REFERENCES public.tasks(id) ON DELETE CASCADE,
|
|
48
|
+
labels jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
49
|
+
position integer NOT NULL DEFAULT 0,
|
|
50
|
+
|
|
51
|
+
-- Which feature put the row here ('onboarding', 'agent', …) and its stable
|
|
52
|
+
-- key within that feature. Both NULL when a person typed the task.
|
|
53
|
+
source text,
|
|
54
|
+
source_key text,
|
|
55
|
+
|
|
56
|
+
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
57
|
+
created_by_name text,
|
|
58
|
+
|
|
59
|
+
-- The 041 signature. See the scoped_resources block at the foot of this file.
|
|
60
|
+
unit_id uuid REFERENCES public.locations(id) ON DELETE SET NULL,
|
|
61
|
+
owner_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
62
|
+
|
|
63
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
64
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE INDEX IF NOT EXISTS tasks_tenant_status_idx ON public.tasks (tenant_id, status);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS tasks_parent_idx ON public.tasks (parent_id);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS tasks_assignee_idx ON public.tasks (tenant_id, assigned_to_id);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS tasks_due_idx ON public.tasks (tenant_id, due_date) WHERE due_date IS NOT NULL;
|
|
71
|
+
CREATE INDEX IF NOT EXISTS tasks_unit_idx ON public.tasks (tenant_id, unit_id);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS tasks_owner_idx ON public.tasks (tenant_id, owner_id) WHERE owner_id IS NOT NULL;
|
|
73
|
+
|
|
74
|
+
-- DELIBERATELY NOT PARTIAL. PostgREST's upsert emits a bare `ON CONFLICT
|
|
75
|
+
-- (tenant_id, source, source_key)` with no index predicate, and a partial
|
|
76
|
+
-- unique index cannot be inferred from that — the seed would fail at runtime
|
|
77
|
+
-- and every wizard run would duplicate the checklist. NULLs are distinct in a
|
|
78
|
+
-- unique index, so hand-typed tasks (source IS NULL) never collide.
|
|
79
|
+
CREATE UNIQUE INDEX IF NOT EXISTS tasks_source_uniq
|
|
80
|
+
ON public.tasks (tenant_id, source, source_key);
|
|
81
|
+
|
|
82
|
+
-- ── Stamps ─────────────────────────────────────────────────────────────────
|
|
83
|
+
DROP TRIGGER IF EXISTS tasks_updated_at ON public.tasks;
|
|
84
|
+
CREATE TRIGGER tasks_updated_at BEFORE UPDATE ON public.tasks
|
|
85
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
86
|
+
|
|
87
|
+
-- COALESCEs, so the seed's explicit author survives (025).
|
|
88
|
+
--
|
|
89
|
+
-- Checked rather than assumed, for the same reason 041 checks for `created_by`:
|
|
90
|
+
-- a pool assembled by hand may never have run 025, and a migration that fails
|
|
91
|
+
-- on a drifted pool is a migration nobody can apply. Without the trigger a
|
|
92
|
+
-- hand-typed task simply has no author until 025 lands — the seeded rows carry
|
|
93
|
+
-- theirs explicitly, and stamp_scope_tasks below falls back to auth.uid().
|
|
94
|
+
DROP TRIGGER IF EXISTS tasks_created_by ON public.tasks;
|
|
95
|
+
DO $$
|
|
96
|
+
BEGIN
|
|
97
|
+
IF to_regprocedure('public.handle_created_by()') IS NOT NULL THEN
|
|
98
|
+
CREATE TRIGGER tasks_created_by BEFORE INSERT ON public.tasks
|
|
99
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_created_by();
|
|
100
|
+
ELSE
|
|
101
|
+
RAISE NOTICE '047: public.handle_created_by() is missing (025 not applied) — tasks.created_by will not be stamped';
|
|
102
|
+
END IF;
|
|
103
|
+
END $$;
|
|
104
|
+
|
|
105
|
+
-- 041 generates one of these per table it knows about, but it is applied and
|
|
106
|
+
-- checksum-frozen: it only ever iterates the list it shipped with. A table
|
|
107
|
+
-- added afterwards brings its own copy. Keep this body in step with the
|
|
108
|
+
-- generator in 051_scoped_columns.sql — including the fact that IT MUST NEVER
|
|
109
|
+
-- RAISE (SECURITY DEFINER RPCs insert here and a complaining BEFORE INSERT
|
|
110
|
+
-- trigger would take them down; validation is 052's WITH CHECK).
|
|
111
|
+
CREATE OR REPLACE FUNCTION public.stamp_scope_tasks() RETURNS trigger
|
|
112
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $body$
|
|
113
|
+
BEGIN
|
|
114
|
+
IF NEW.unit_id IS NULL THEN
|
|
115
|
+
NEW.unit_id := public.requested_unit();
|
|
116
|
+
END IF;
|
|
117
|
+
IF NEW.owner_id IS NULL THEN
|
|
118
|
+
NEW.owner_id := COALESCE(NEW.created_by, auth.uid());
|
|
119
|
+
END IF;
|
|
120
|
+
RETURN NEW;
|
|
121
|
+
EXCEPTION WHEN OTHERS THEN
|
|
122
|
+
RETURN NEW;
|
|
123
|
+
END $body$;
|
|
124
|
+
|
|
125
|
+
REVOKE ALL ON FUNCTION public.stamp_scope_tasks() FROM public, anon;
|
|
126
|
+
|
|
127
|
+
DROP TRIGGER IF EXISTS tasks_scope_stamp ON public.tasks;
|
|
128
|
+
-- AFTER tasks_created_by alphabetically is not a guarantee — Postgres fires
|
|
129
|
+
-- BEFORE triggers in name order, and 'tasks_created_by' < 'tasks_scope_stamp',
|
|
130
|
+
-- which is what lets the COALESCE above see the author it just stamped.
|
|
131
|
+
CREATE TRIGGER tasks_scope_stamp BEFORE INSERT ON public.tasks
|
|
132
|
+
FOR EACH ROW EXECUTE FUNCTION public.stamp_scope_tasks();
|
|
133
|
+
|
|
134
|
+
-- ── RLS ─────────────────────────────────────────────────────────────────────
|
|
135
|
+
-- 011 closed `anon` on new public tables but not `authenticated`, which is born
|
|
136
|
+
-- with the full grant. Stripped first, granted back deliberately. The blanket
|
|
137
|
+
-- tenant_isolation sweep in 002 ran long before this table existed, so the
|
|
138
|
+
-- policies below are written out rather than inherited.
|
|
139
|
+
REVOKE ALL ON public.tasks FROM anon, authenticated;
|
|
140
|
+
|
|
141
|
+
ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY;
|
|
142
|
+
|
|
143
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.tasks TO authenticated;
|
|
144
|
+
|
|
145
|
+
DROP POLICY IF EXISTS tasks_member_read ON public.tasks;
|
|
146
|
+
CREATE POLICY tasks_member_read ON public.tasks
|
|
147
|
+
FOR SELECT TO authenticated
|
|
148
|
+
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
149
|
+
|
|
150
|
+
DROP POLICY IF EXISTS tasks_member_insert ON public.tasks;
|
|
151
|
+
CREATE POLICY tasks_member_insert ON public.tasks
|
|
152
|
+
FOR INSERT TO authenticated
|
|
153
|
+
WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
154
|
+
|
|
155
|
+
-- USING and WITH CHECK both: without the second, a member could move a task
|
|
156
|
+
-- into a tenant they do not belong to.
|
|
157
|
+
DROP POLICY IF EXISTS tasks_member_update ON public.tasks;
|
|
158
|
+
CREATE POLICY tasks_member_update ON public.tasks
|
|
159
|
+
FOR UPDATE TO authenticated
|
|
160
|
+
USING (tenant_id IN (SELECT public.user_tenant_ids()))
|
|
161
|
+
WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
162
|
+
|
|
163
|
+
DROP POLICY IF EXISTS tasks_member_delete ON public.tasks;
|
|
164
|
+
CREATE POLICY tasks_member_delete ON public.tasks
|
|
165
|
+
FOR DELETE TO authenticated
|
|
166
|
+
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
167
|
+
|
|
168
|
+
-- ── Unit scoping ────────────────────────────────────────────────────────────
|
|
169
|
+
-- The registry row 042 loops over and the settings screen reads.
|
|
170
|
+
--
|
|
171
|
+
-- 'tenant' visibility, not 'unit': a task is assigned to a person, and a
|
|
172
|
+
-- checklist the owner is working through has to stay visible to the team that
|
|
173
|
+
-- is helping. owner_id is still stamped, so a tenant that switches tasks to
|
|
174
|
+
-- 'owner' in the visibility settings gets private lists without a migration.
|
|
175
|
+
-- Guarded on the registry existing at all: this pool proved drift is real and
|
|
176
|
+
-- not in file order (it had 039-042 but never ran 025), so "the spine applies
|
|
177
|
+
-- in sequence" is not something to bet the migration on.
|
|
178
|
+
DO $$
|
|
179
|
+
BEGIN
|
|
180
|
+
IF to_regclass('public.scoped_resources') IS NULL THEN
|
|
181
|
+
RAISE NOTICE '047: scoped_resources is missing (040 not applied) — tasks will not be unit-scoped';
|
|
182
|
+
RETURN;
|
|
183
|
+
END IF;
|
|
184
|
+
|
|
185
|
+
INSERT INTO public.scoped_resources
|
|
186
|
+
(resource_table, unit_column, owner_column, shareable, default_visibility, label)
|
|
187
|
+
VALUES
|
|
188
|
+
('tasks', 'unit_id', 'owner_id', true, 'tenant', 'Tarefas')
|
|
189
|
+
ON CONFLICT (resource_table) DO UPDATE
|
|
190
|
+
SET unit_column = EXCLUDED.unit_column,
|
|
191
|
+
owner_column = EXCLUDED.owner_column,
|
|
192
|
+
shareable = EXCLUDED.shareable,
|
|
193
|
+
label = EXCLUDED.label;
|
|
194
|
+
-- default_visibility is NOT updated on conflict — same reason as 041: it is
|
|
195
|
+
-- the factory setting, and re-running must not walk back an operator's choice.
|
|
196
|
+
END $$;
|
|
197
|
+
|
|
198
|
+
-- The restrictive policy, from 052's generator. Same reason as the trigger
|
|
199
|
+
-- above: 042 is frozen and only saw the registry as it stood then. The predicate
|
|
200
|
+
-- text is read straight out of the registry so the two stay identical in shape;
|
|
201
|
+
-- if 042 is ever superseded, this block must be revisited with it.
|
|
202
|
+
DO $$
|
|
203
|
+
DECLARE
|
|
204
|
+
r record;
|
|
205
|
+
v_read text;
|
|
206
|
+
v_write text;
|
|
207
|
+
BEGIN
|
|
208
|
+
IF to_regclass('public.scoped_resources') IS NULL THEN
|
|
209
|
+
RETURN; -- no registry, no unit scoping — see the block above
|
|
210
|
+
END IF;
|
|
211
|
+
|
|
212
|
+
FOR r IN SELECT resource_table, unit_column FROM public.scoped_resources
|
|
213
|
+
WHERE resource_table = 'tasks' LOOP
|
|
214
|
+
CONTINUE WHEN to_regclass('public.' || r.resource_table) IS NULL;
|
|
215
|
+
|
|
216
|
+
v_read := format($p$
|
|
217
|
+
NOT (SELECT public.unit_scoping_active())
|
|
218
|
+
OR tenant_id NOT IN (SELECT public.unit_scoped_tenants())
|
|
219
|
+
OR tenant_id NOT IN (SELECT public.user_tenant_ids())
|
|
220
|
+
OR tenant_id IN (SELECT public.user_admin_tenant_ids())
|
|
221
|
+
OR %3$I = (SELECT auth.uid())
|
|
222
|
+
OR id IN (SELECT public.granted_record_ids(%1$L))
|
|
223
|
+
OR (
|
|
224
|
+
tenant_id IN (SELECT public.resource_mode_tenants(%1$L, 'tenant'))
|
|
225
|
+
AND id NOT IN (SELECT public.restricted_record_ids(%1$L))
|
|
226
|
+
)
|
|
227
|
+
OR (
|
|
228
|
+
(
|
|
229
|
+
tenant_id IN (SELECT public.resource_mode_tenants(%1$L, 'unit'))
|
|
230
|
+
OR (%3$I IS NULL AND tenant_id IN (SELECT public.resource_mode_tenants(%1$L, 'owner')))
|
|
231
|
+
)
|
|
232
|
+
AND (
|
|
233
|
+
%2$I IN (SELECT public.user_unit_ids())
|
|
234
|
+
OR (%2$I IS NULL AND id NOT IN (SELECT public.restricted_record_ids(%1$L)))
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
$p$, r.resource_table, r.unit_column, 'owner_id');
|
|
238
|
+
|
|
239
|
+
v_write := format($p$
|
|
240
|
+
NOT (SELECT public.unit_scoping_active())
|
|
241
|
+
OR tenant_id NOT IN (SELECT public.unit_scoped_tenants())
|
|
242
|
+
OR tenant_id NOT IN (SELECT public.user_tenant_ids())
|
|
243
|
+
OR tenant_id IN (SELECT public.user_admin_tenant_ids())
|
|
244
|
+
OR (
|
|
245
|
+
(%2$I IS NULL OR %2$I IN (SELECT public.user_unit_ids()))
|
|
246
|
+
AND (%3$I IS NULL OR %3$I = (SELECT auth.uid()))
|
|
247
|
+
)
|
|
248
|
+
$p$, r.resource_table, r.unit_column, 'owner_id');
|
|
249
|
+
|
|
250
|
+
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', r.resource_table);
|
|
251
|
+
EXECUTE format('DROP POLICY IF EXISTS unit_scope ON public.%I', r.resource_table);
|
|
252
|
+
-- `AS RESTRICTIVE` is the word this whole block depends on: without it the
|
|
253
|
+
-- policy ORs with the member policies above and grants MORE access, not
|
|
254
|
+
-- less. negative-control.sh deletes exactly this line and demands red.
|
|
255
|
+
EXECUTE format(
|
|
256
|
+
'CREATE POLICY unit_scope ON public.%I '
|
|
257
|
+
|| ' AS RESTRICTIVE '
|
|
258
|
+
|| ' FOR ALL TO authenticated USING (%s) WITH CHECK (%s)',
|
|
259
|
+
r.resource_table, v_read, v_write);
|
|
260
|
+
END LOOP;
|
|
261
|
+
END $$;
|
|
262
|
+
|
|
263
|
+
COMMENT ON TABLE public.tasks IS
|
|
264
|
+
'Admin tasks: assignments, status, due date. Spine-owned — the shell''s '
|
|
265
|
+
'getting-started checklist seeds rows here with source = ''onboarding''. '
|
|
266
|
+
'plugin-tasks renders this table; it no longer owns one.';
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 058_every_login_is_a_person.sql — quem entra no sistema é uma pessoa.
|
|
3
|
+
--
|
|
4
|
+
-- A 019 deixou o vínculo opcional nos dois sentidos, e documentou o porquê:
|
|
5
|
+
-- "login-only account (e.g. the owner who signed up) -> person_id NULL". Na
|
|
6
|
+
-- prática isso virou o estado NORMAL, não a exceção — em pool vivo, ZERO
|
|
7
|
+
-- memberships tinham ficha. E a consequência não é cosmética:
|
|
8
|
+
--
|
|
9
|
+
-- * a mesma pessoa existe duas vezes, uma como login e outra como ficha, e
|
|
10
|
+
-- nada no sistema diz que são a mesma;
|
|
11
|
+
-- * a tela de equipe lista as duas como se fossem duas pessoas;
|
|
12
|
+
-- * o perfil de quem entra não tem escala, comissão, documento nem endereço,
|
|
13
|
+
-- porque essas coisas moram na ficha e o login não alcança nenhuma.
|
|
14
|
+
--
|
|
15
|
+
-- O lado opcional continua verdadeiro e é legítimo: metade da equipe de um
|
|
16
|
+
-- salão nunca faz login, e essas fichas seguem sem membership. O que deixa de
|
|
17
|
+
-- ser opcional é o outro lado — **todo login tem ficha**.
|
|
18
|
+
--
|
|
19
|
+
-- POR QUE UM GATILHO, E NÃO CONSERTAR CADA CAMINHO. Membership nasce em pelo
|
|
20
|
+
-- menos três lugares: a criação da conta, a aceitação de convite (022, que
|
|
21
|
+
-- insere sem person_id) e o que cada app fizer por fora. Consertar os três
|
|
22
|
+
-- deixa o quarto para depois. BEFORE INSERT vale para todos, inclusive para o
|
|
23
|
+
-- SQL que alguém rodar à mão daqui a um ano.
|
|
24
|
+
--
|
|
25
|
+
-- POR QUE O GATILHO NUNCA LEVANTA EXCEÇÃO. Ele roda dentro da aceitação de
|
|
26
|
+
-- convite. Uma falha aqui derrubaria a entrada de alguém que acabou de aceitar
|
|
27
|
+
-- um convite legítimo — e ficar sem ficha é chato, ficar sem acesso é o
|
|
28
|
+
-- chamado de sábado. Falhou, a linha entra sem ficha e o backfill pega depois.
|
|
29
|
+
--
|
|
30
|
+
-- POR QUE `person_id` NÃO VIROU NOT NULL. A coluna é `ON DELETE SET NULL`: com
|
|
31
|
+
-- NOT NULL, apagar uma ficha passaria a falhar com erro de chave estrangeira em
|
|
32
|
+
-- cima de quem tem login, e a tela de cadastros não tem o que fazer com isso.
|
|
33
|
+
-- Trocar para CASCADE seria pior — apagar a ficha revogaria o acesso em
|
|
34
|
+
-- silêncio. A garantia fica onde ela é barata e completa: na criação.
|
|
35
|
+
--
|
|
36
|
+
-- Aditiva e idempotente.
|
|
37
|
+
-- ============================================================================
|
|
38
|
+
|
|
39
|
+
-- Qual `kind` uma ficha criada automaticamente recebe.
|
|
40
|
+
--
|
|
41
|
+
-- Configurável por conta porque cada vertical chama a própria equipe de um
|
|
42
|
+
-- jeito, e uma ficha com kind que o app não lista não aparece no cadastro dele.
|
|
43
|
+
-- 'staff' como padrão: é o que a frota usa. Um palpite adaptativo (olhar o kind
|
|
44
|
+
-- mais comum da conta) foi descartado — esperteza dentro de gatilho falha em
|
|
45
|
+
-- silêncio, e o custo de errar aqui é uma ficha no lugar errado.
|
|
46
|
+
CREATE OR REPLACE FUNCTION public.team_person_kind(p_tenant_id uuid)
|
|
47
|
+
RETURNS text LANGUAGE sql STABLE SECURITY DEFINER
|
|
48
|
+
SET search_path = public, pg_temp AS $$
|
|
49
|
+
SELECT COALESCE(
|
|
50
|
+
NULLIF(trim((SELECT t.settings ->> 'team_person_kind' FROM public.tenants t WHERE t.id = p_tenant_id)), ''),
|
|
51
|
+
'staff'
|
|
52
|
+
);
|
|
53
|
+
$$;
|
|
54
|
+
|
|
55
|
+
COMMENT ON FUNCTION public.team_person_kind(uuid) IS
|
|
56
|
+
'O kind que uma ficha criada para um login recebe. `settings.team_person_kind`, ou staff.';
|
|
57
|
+
|
|
58
|
+
-- ── A ficha de quem entra ───────────────────────────────────────────────────
|
|
59
|
+
CREATE OR REPLACE FUNCTION public.ensure_member_person()
|
|
60
|
+
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER
|
|
61
|
+
SET search_path = public, pg_temp AS $$
|
|
62
|
+
DECLARE
|
|
63
|
+
v_email text;
|
|
64
|
+
v_name text;
|
|
65
|
+
v_kind text;
|
|
66
|
+
v_id uuid;
|
|
67
|
+
BEGIN
|
|
68
|
+
IF NEW.person_id IS NOT NULL THEN
|
|
69
|
+
RETURN NEW;
|
|
70
|
+
END IF;
|
|
71
|
+
|
|
72
|
+
SELECT u.email INTO v_email FROM auth.users u WHERE u.id = NEW.user_id;
|
|
73
|
+
SELECT p.full_name INTO v_name FROM public.profiles p WHERE p.id = NEW.user_id;
|
|
74
|
+
|
|
75
|
+
-- O nome que a ficha nasce com. Sem nome no perfil, o pedaço antes do @ é
|
|
76
|
+
-- melhor do que "Sem nome": ele é reconhecível e a pessoa corrige em um
|
|
77
|
+
-- clique no próprio perfil.
|
|
78
|
+
v_name := COALESCE(NULLIF(trim(v_name), ''), NULLIF(split_part(COALESCE(v_email, ''), '@', 1), ''), 'Sem nome');
|
|
79
|
+
v_kind := public.team_person_kind(NEW.tenant_id);
|
|
80
|
+
|
|
81
|
+
-- Primeiro procura: a ficha da pessoa provavelmente já existe, cadastrada
|
|
82
|
+
-- por quem montou a equipe antes de ela ganhar login. Criar uma segunda seria
|
|
83
|
+
-- fabricar exatamente a duplicata que este arquivo existe para acabar.
|
|
84
|
+
--
|
|
85
|
+
-- `NOT EXISTS` contra memberships porque uma ficha só pertence a um login:
|
|
86
|
+
-- o índice único de 019 recusaria a segunda, e aí o INSERT inteiro cairia.
|
|
87
|
+
IF v_email IS NOT NULL AND v_email <> '' THEN
|
|
88
|
+
SELECT pe.id INTO v_id
|
|
89
|
+
FROM public.people pe
|
|
90
|
+
WHERE pe.tenant_id = NEW.tenant_id
|
|
91
|
+
AND lower(pe.email) = lower(v_email)
|
|
92
|
+
AND pe.kind = v_kind
|
|
93
|
+
AND NOT EXISTS (
|
|
94
|
+
SELECT 1 FROM public.tenant_members tm
|
|
95
|
+
WHERE tm.tenant_id = NEW.tenant_id AND tm.person_id = pe.id
|
|
96
|
+
)
|
|
97
|
+
ORDER BY pe.created_at
|
|
98
|
+
LIMIT 1;
|
|
99
|
+
END IF;
|
|
100
|
+
|
|
101
|
+
IF v_id IS NULL THEN
|
|
102
|
+
INSERT INTO public.people (tenant_id, kind, name, email)
|
|
103
|
+
VALUES (NEW.tenant_id, v_kind, v_name, v_email)
|
|
104
|
+
RETURNING id INTO v_id;
|
|
105
|
+
END IF;
|
|
106
|
+
|
|
107
|
+
NEW.person_id := v_id;
|
|
108
|
+
RETURN NEW;
|
|
109
|
+
EXCEPTION WHEN OTHERS THEN
|
|
110
|
+
-- Ver o cabeçalho: entrar sem ficha é recuperável, não entrar não é.
|
|
111
|
+
RETURN NEW;
|
|
112
|
+
END $$;
|
|
113
|
+
|
|
114
|
+
DROP TRIGGER IF EXISTS tenant_members_ensure_person ON public.tenant_members;
|
|
115
|
+
CREATE TRIGGER tenant_members_ensure_person
|
|
116
|
+
BEFORE INSERT ON public.tenant_members
|
|
117
|
+
FOR EACH ROW EXECUTE FUNCTION public.ensure_member_person();
|
|
118
|
+
|
|
119
|
+
-- ── E quem já estava dentro ─────────────────────────────────────────────────
|
|
120
|
+
-- O gatilho só alcança o futuro. Sem isto, todo mundo que já usa o sistema
|
|
121
|
+
-- continuaria sendo duas pessoas — que é o estado que motivou o arquivo.
|
|
122
|
+
--
|
|
123
|
+
-- Em função, e não solto, para poder ser chamado de novo num pool que receber
|
|
124
|
+
-- membership por fora depois. Devolve quantos ligou.
|
|
125
|
+
CREATE OR REPLACE FUNCTION public.backfill_member_persons()
|
|
126
|
+
RETURNS integer LANGUAGE plpgsql SECURITY DEFINER
|
|
127
|
+
SET search_path = public, pg_temp AS $$
|
|
128
|
+
DECLARE
|
|
129
|
+
r record;
|
|
130
|
+
v_email text;
|
|
131
|
+
v_name text;
|
|
132
|
+
v_kind text;
|
|
133
|
+
v_id uuid;
|
|
134
|
+
v_count integer := 0;
|
|
135
|
+
BEGIN
|
|
136
|
+
FOR r IN
|
|
137
|
+
SELECT tm.id, tm.tenant_id, tm.user_id
|
|
138
|
+
FROM public.tenant_members tm
|
|
139
|
+
WHERE tm.person_id IS NULL
|
|
140
|
+
ORDER BY tm.created_at
|
|
141
|
+
LOOP
|
|
142
|
+
BEGIN
|
|
143
|
+
SELECT u.email INTO v_email FROM auth.users u WHERE u.id = r.user_id;
|
|
144
|
+
SELECT p.full_name INTO v_name FROM public.profiles p WHERE p.id = r.user_id;
|
|
145
|
+
v_name := COALESCE(NULLIF(trim(v_name), ''), NULLIF(split_part(COALESCE(v_email, ''), '@', 1), ''), 'Sem nome');
|
|
146
|
+
v_kind := public.team_person_kind(r.tenant_id);
|
|
147
|
+
v_id := NULL;
|
|
148
|
+
|
|
149
|
+
IF v_email IS NOT NULL AND v_email <> '' THEN
|
|
150
|
+
SELECT pe.id INTO v_id
|
|
151
|
+
FROM public.people pe
|
|
152
|
+
WHERE pe.tenant_id = r.tenant_id
|
|
153
|
+
AND lower(pe.email) = lower(v_email)
|
|
154
|
+
AND pe.kind = v_kind
|
|
155
|
+
AND NOT EXISTS (
|
|
156
|
+
SELECT 1 FROM public.tenant_members tm2
|
|
157
|
+
WHERE tm2.tenant_id = r.tenant_id AND tm2.person_id = pe.id
|
|
158
|
+
)
|
|
159
|
+
ORDER BY pe.created_at
|
|
160
|
+
LIMIT 1;
|
|
161
|
+
END IF;
|
|
162
|
+
|
|
163
|
+
IF v_id IS NULL THEN
|
|
164
|
+
INSERT INTO public.people (tenant_id, kind, name, email)
|
|
165
|
+
VALUES (r.tenant_id, v_kind, v_name, v_email)
|
|
166
|
+
RETURNING id INTO v_id;
|
|
167
|
+
END IF;
|
|
168
|
+
|
|
169
|
+
UPDATE public.tenant_members SET person_id = v_id WHERE id = r.id;
|
|
170
|
+
v_count := v_count + 1;
|
|
171
|
+
EXCEPTION WHEN OTHERS THEN
|
|
172
|
+
-- Uma linha que não dá para ligar não pode impedir as outras.
|
|
173
|
+
CONTINUE;
|
|
174
|
+
END;
|
|
175
|
+
END LOOP;
|
|
176
|
+
RETURN v_count;
|
|
177
|
+
END $$;
|
|
178
|
+
|
|
179
|
+
SELECT public.backfill_member_persons();
|
|
180
|
+
|
|
181
|
+
-- Quem sobrou. Uma conta com número diferente de zero aqui tem gente que a tela
|
|
182
|
+
-- de equipe ainda mostra duas vezes, e é isso que o doctor deve olhar.
|
|
183
|
+
CREATE OR REPLACE FUNCTION public.unlinked_member_count()
|
|
184
|
+
RETURNS integer LANGUAGE sql STABLE SECURITY DEFINER
|
|
185
|
+
SET search_path = public, pg_temp AS $$
|
|
186
|
+
SELECT count(*)::integer FROM public.tenant_members WHERE person_id IS NULL;
|
|
187
|
+
$$;
|
|
188
|
+
|
|
189
|
+
GRANT EXECUTE ON FUNCTION public.team_person_kind(uuid) TO authenticated;
|
|
190
|
+
GRANT EXECUTE ON FUNCTION public.unlinked_member_count() TO authenticated;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 059_bookable_people.sql — trabalhar aqui não é atender cliente.
|
|
3
|
+
--
|
|
4
|
+
-- A 048 passou a criar uma ficha para todo login, e a agenda lista como
|
|
5
|
+
-- profissional toda pessoa de `kind = staff` que esteja ativa. O resultado
|
|
6
|
+
-- apareceu na hora: o contador e o administrador viraram colunas na grade
|
|
7
|
+
-- semanal, com horário livre para alguém marcar em cima.
|
|
8
|
+
--
|
|
9
|
+
-- O sinal que faltava não é o cargo — é uma pergunta só, e ela é binária:
|
|
10
|
+
-- **esta pessoa atende cliente?** Um "tipo de staff" (recepção, profissional,
|
|
11
|
+
-- administrativo) responderia isso e mais coisas, mas exigiria uma taxonomia
|
|
12
|
+
-- por vertical — o que a clínica chama de recepção a escola chama de
|
|
13
|
+
-- secretaria — e uma taxonomia errada é mais difícil de desfazer do que um
|
|
14
|
+
-- campo a mais. Quando o tipo existir, ele deriva esta coluna; enquanto não
|
|
15
|
+
-- existe, esta coluna já responde o que a agenda precisa saber.
|
|
16
|
+
--
|
|
17
|
+
-- O PADRÃO SEGUE COMO A FICHA NASCEU, e é isso que evita as duas regressões
|
|
18
|
+
-- opostas:
|
|
19
|
+
--
|
|
20
|
+
-- * `DEFAULT true` para tudo que já existe — ninguém que já atende some da
|
|
21
|
+
-- agenda quando esta migration sobe;
|
|
22
|
+
-- * `false` para a ficha que a 048 cria — quem só entrou no sistema não vira
|
|
23
|
+
-- profissional por ter feito login.
|
|
24
|
+
--
|
|
25
|
+
-- Aditiva e idempotente.
|
|
26
|
+
-- ============================================================================
|
|
27
|
+
|
|
28
|
+
ALTER TABLE public.people
|
|
29
|
+
ADD COLUMN IF NOT EXISTS is_bookable boolean NOT NULL DEFAULT true;
|
|
30
|
+
|
|
31
|
+
COMMENT ON COLUMN public.people.is_bookable IS
|
|
32
|
+
'Esta pessoa atende cliente e aparece na agenda. Ficha criada a partir de um login nasce false.';
|
|
33
|
+
|
|
34
|
+
-- ── A 048, agora sabendo a diferença ────────────────────────────────────────
|
|
35
|
+
-- Reescrita inteira em vez de um ALTER: a função é a mesma da 048 com uma
|
|
36
|
+
-- coluna a mais no INSERT, e duas versões parciais do mesmo corpo espalhadas em
|
|
37
|
+
-- dois arquivos é como uma delas fica para trás.
|
|
38
|
+
CREATE OR REPLACE FUNCTION public.ensure_member_person()
|
|
39
|
+
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER
|
|
40
|
+
SET search_path = public, pg_temp AS $$
|
|
41
|
+
DECLARE
|
|
42
|
+
v_email text; v_name text; v_kind text; v_id uuid;
|
|
43
|
+
BEGIN
|
|
44
|
+
IF NEW.person_id IS NOT NULL THEN RETURN NEW; END IF;
|
|
45
|
+
|
|
46
|
+
SELECT u.email INTO v_email FROM auth.users u WHERE u.id = NEW.user_id;
|
|
47
|
+
SELECT p.full_name INTO v_name FROM public.profiles p WHERE p.id = NEW.user_id;
|
|
48
|
+
v_name := COALESCE(NULLIF(trim(v_name), ''), NULLIF(split_part(COALESCE(v_email, ''), '@', 1), ''), 'Sem nome');
|
|
49
|
+
v_kind := public.team_person_kind(NEW.tenant_id);
|
|
50
|
+
|
|
51
|
+
IF v_email IS NOT NULL AND v_email <> '' THEN
|
|
52
|
+
SELECT pe.id INTO v_id FROM public.people pe
|
|
53
|
+
WHERE pe.tenant_id = NEW.tenant_id AND lower(pe.email) = lower(v_email) AND pe.kind = v_kind
|
|
54
|
+
AND NOT EXISTS (SELECT 1 FROM public.tenant_members tm
|
|
55
|
+
WHERE tm.tenant_id = NEW.tenant_id AND tm.person_id = pe.id)
|
|
56
|
+
ORDER BY pe.created_at LIMIT 1;
|
|
57
|
+
END IF;
|
|
58
|
+
|
|
59
|
+
IF v_id IS NULL THEN
|
|
60
|
+
-- `false` aqui é a linha inteira desta migration: entrar no sistema não é
|
|
61
|
+
-- passar a atender. Quem de fato atende é marcado por quem sabe.
|
|
62
|
+
INSERT INTO public.people (tenant_id, kind, name, email, is_bookable)
|
|
63
|
+
VALUES (NEW.tenant_id, v_kind, v_name, v_email, false)
|
|
64
|
+
RETURNING id INTO v_id;
|
|
65
|
+
END IF;
|
|
66
|
+
|
|
67
|
+
NEW.person_id := v_id;
|
|
68
|
+
RETURN NEW;
|
|
69
|
+
EXCEPTION WHEN OTHERS THEN
|
|
70
|
+
RETURN NEW;
|
|
71
|
+
END $$;
|
|
72
|
+
|
|
73
|
+
CREATE OR REPLACE FUNCTION public.backfill_member_persons()
|
|
74
|
+
RETURNS integer LANGUAGE plpgsql SECURITY DEFINER
|
|
75
|
+
SET search_path = public, pg_temp AS $$
|
|
76
|
+
DECLARE
|
|
77
|
+
r record; v_email text; v_name text; v_kind text; v_id uuid; v_count integer := 0;
|
|
78
|
+
BEGIN
|
|
79
|
+
FOR r IN SELECT tm.id, tm.tenant_id, tm.user_id FROM public.tenant_members tm
|
|
80
|
+
WHERE tm.person_id IS NULL ORDER BY tm.created_at
|
|
81
|
+
LOOP
|
|
82
|
+
BEGIN
|
|
83
|
+
SELECT u.email INTO v_email FROM auth.users u WHERE u.id = r.user_id;
|
|
84
|
+
SELECT p.full_name INTO v_name FROM public.profiles p WHERE p.id = r.user_id;
|
|
85
|
+
v_name := COALESCE(NULLIF(trim(v_name), ''), NULLIF(split_part(COALESCE(v_email, ''), '@', 1), ''), 'Sem nome');
|
|
86
|
+
v_kind := public.team_person_kind(r.tenant_id);
|
|
87
|
+
v_id := NULL;
|
|
88
|
+
|
|
89
|
+
IF v_email IS NOT NULL AND v_email <> '' THEN
|
|
90
|
+
SELECT pe.id INTO v_id FROM public.people pe
|
|
91
|
+
WHERE pe.tenant_id = r.tenant_id AND lower(pe.email) = lower(v_email) AND pe.kind = v_kind
|
|
92
|
+
AND NOT EXISTS (SELECT 1 FROM public.tenant_members tm2
|
|
93
|
+
WHERE tm2.tenant_id = r.tenant_id AND tm2.person_id = pe.id)
|
|
94
|
+
ORDER BY pe.created_at LIMIT 1;
|
|
95
|
+
END IF;
|
|
96
|
+
|
|
97
|
+
IF v_id IS NULL THEN
|
|
98
|
+
INSERT INTO public.people (tenant_id, kind, name, email, is_bookable)
|
|
99
|
+
VALUES (r.tenant_id, v_kind, v_name, v_email, false) RETURNING id INTO v_id;
|
|
100
|
+
END IF;
|
|
101
|
+
|
|
102
|
+
UPDATE public.tenant_members SET person_id = v_id WHERE id = r.id;
|
|
103
|
+
v_count := v_count + 1;
|
|
104
|
+
EXCEPTION WHEN OTHERS THEN CONTINUE;
|
|
105
|
+
END;
|
|
106
|
+
END LOOP;
|
|
107
|
+
RETURN v_count;
|
|
108
|
+
END $$;
|
|
109
|
+
|
|
110
|
+
-- ── E as fichas que a 048 já criou antes desta coluna existir ───────────────
|
|
111
|
+
-- Elas nasceram com o DEFAULT true e estão na agenda agora. A correção precisa
|
|
112
|
+
-- alcançar ESSAS e nenhuma outra, então ela não pergunta "quem tem login" — a
|
|
113
|
+
-- profissional dona da clínica também tem — e sim "quem tem login E nunca
|
|
114
|
+
-- apareceu em atendimento nenhum": sem agendamento, sem escala e sem pedido.
|
|
115
|
+
--
|
|
116
|
+
-- Uma profissional de verdade recém-contratada, ainda sem histórico, também cai
|
|
117
|
+
-- aqui e é desmarcada. É o lado certo para errar: ela some de uma grade onde
|
|
118
|
+
-- ainda não tinha nada, e volta com um clique — enquanto o contador marcado
|
|
119
|
+
-- como profissional recebe cliente.
|
|
120
|
+
UPDATE public.people pe
|
|
121
|
+
SET is_bookable = false
|
|
122
|
+
WHERE pe.is_bookable
|
|
123
|
+
AND EXISTS (SELECT 1 FROM public.tenant_members tm WHERE tm.person_id = pe.id)
|
|
124
|
+
AND NOT EXISTS (SELECT 1 FROM public.appointments a WHERE a.assignee_id = pe.id)
|
|
125
|
+
AND NOT EXISTS (SELECT 1 FROM public.schedules s WHERE s.assignee_id = pe.id)
|
|
126
|
+
AND NOT EXISTS (SELECT 1 FROM public.orders o WHERE o.assignee_id = pe.id);
|