@fayz-ai/db 0.23.2 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export * from 'drizzle-orm/pg-core';
1
+ export { Check, CheckBuilder, DefaultViewBuilderCore, ExtraConfigColumn, ForeignKey, ForeignKeyBuilder, Index, IndexBuilder, IndexBuilderOn, IndexedColumn, ManualMaterializedViewBuilder, ManualViewBuilder, MaterializedViewBuilder, MaterializedViewBuilderCore, PgArray, PgArrayBuilder, PgBigInt53, PgBigInt53Builder, PgBigInt64, PgBigInt64Builder, PgBigSerial53, PgBigSerial53Builder, PgBigSerial64, PgBigSerial64Builder, PgBinaryVector, PgBinaryVectorBuilder, PgBoolean, PgBooleanBuilder, PgChar, PgCharBuilder, PgCidr, PgCidrBuilder, PgColumn, PgColumnBuilder, PgCustomColumn, PgCustomColumnBuilder, PgDatabase, PgDate, PgDateBuilder, PgDateString, PgDateStringBuilder, PgDeleteBase, PgDialect, PgDoublePrecision, PgDoublePrecisionBuilder, PgEnumColumn, PgEnumColumnBuilder, PgGeometry, PgGeometryBuilder, PgGeometryObject, PgGeometryObjectBuilder, PgHalfVector, PgHalfVectorBuilder, PgInet, PgInetBuilder, PgInsertBase, PgInsertBuilder, PgIntColumnBaseBuilder, PgInteger, PgIntegerBuilder, PgInterval, PgIntervalBuilder, PgJson, PgJsonBuilder, PgJsonb, PgJsonbBuilder, PgLineABC, PgLineABCBuilder, PgLineBuilder, PgLineTuple, PgMacaddr, PgMacaddr8, PgMacaddr8Builder, PgMacaddrBuilder, PgMaterializedView, PgMaterializedViewConfig, PgNumeric, PgNumericBuilder, PgPointObject, PgPointObjectBuilder, PgPointTuple, PgPointTupleBuilder, PgPolicy, PgPreparedQuery, PgReal, PgRealBuilder, PgRefreshMaterializedView, PgRole, PgSchema, PgSelectBase, PgSelectBuilder, PgSelectQueryBuilderBase, PgSequence, PgSerial, PgSerialBuilder, PgSession, PgSmallInt, PgSmallIntBuilder, PgSmallSerial, PgSmallSerialBuilder, PgSparseVector, PgSparseVectorBuilder, PgTable, PgText, PgTextBuilder, PgTime, PgTimeBuilder, PgTimestamp, PgTimestampBuilder, PgTimestampString, PgTimestampStringBuilder, PgTransaction, PgUUID, PgUUIDBuilder, PgUpdateBase, PgUpdateBuilder, PgVarchar, PgVarcharBuilder, PgVector, PgVectorBuilder, PgView, PgViewConfig, PrimaryKey, PrimaryKeyBuilder, QueryBuilder, UniqueConstraint, UniqueConstraintBuilder, UniqueOnConstraintBuilder, ViewBuilder, alias, bigint, bigserial, bit, boolean, char, check, cidr, customType, date, decimal, doublePrecision, except, exceptAll, foreignKey, geometry, getMaterializedViewConfig, getTableConfig, getViewConfig, halfvec, index, inet, integer, intersect, intersectAll, interval, isPgEnum, isPgMaterializedView, isPgSchema, isPgSequence, isPgView, json, jsonb, line, macaddr, macaddr8, makePgArray, numeric, parsePgArray, parsePgNestedArray, pgEnum, pgMaterializedView, pgPolicy, pgRole, pgSchema, pgSequence, pgTable, pgTableCreator, pgView, point, primaryKey, real, serial, smallint, smallserial, sparsevec, text, time, timestamp, union, unionAll, unique, uniqueIndex, uniqueKeyName, uuid, varchar, vector, withReplicas, } from 'drizzle-orm/pg-core';
2
+ export type { AnyForeignKeyBuilder, AnyIndexBuilder, AnyPgColumn, AnyPgDeleteBase, AnyPgInsert, AnyPgSelect, AnyPgSelectQueryBuilder, AnyPgSetOperatorInterface, AnyPgTable, AnyPgUpdate, BuildAliasTable, ColumnsWithTable, ConvertCustomConfig, CreatePgSelectFromBuilderMode, CustomTypeParams, CustomTypeValues, GetColumnsTableName, GetPgSetOperators, IndexColumn, IndexedExtraConfigType, IntervalConfig, LockConfig, LockStrength, PgBigInt53BuilderInitial, PgBigInt64BuilderInitial, PgBigIntConfig, PgBigSerial53BuilderInitial, PgBigSerial64BuilderInitial, PgBigSerialConfig, PgBinaryVectorBuilderInitial, PgBinaryVectorConfig, PgBooleanBuilderInitial, PgCharBuilderInitial, PgCharConfig, PgCidrBuilderInitial, PgColumnBuilderBase, PgCreateSetOperatorFn, PgCustomColumnInnerConfig, PgDateBuilderInitial, PgDateConfig, PgDateStringBuilderInitial, PgDelete, PgDeleteConfig, PgDeleteDynamic, PgDeletePrepare, PgDeleteReturning, PgDeleteReturningAll, PgDeleteWithout, PgDialectConfig, PgDoublePrecisionBuilderInitial, PgEnum, PgEnumColumnBuilderInitial, PgGeometryBuilderInitial, PgGeometryConfig, PgGeometryObjectBuilderInitial, PgHalfVectorBuilderInitial, PgHalfVectorConfig, PgIndexMethod, PgIndexOpClass, PgInetBuilderInitial, PgInsert, PgInsertConfig, PgInsertDynamic, PgInsertOnConflictDoUpdateConfig, PgInsertPrepare, PgInsertReturning, PgInsertReturningAll, PgInsertSelectQueryBuilder, PgInsertValue, PgInsertWithout, PgIntegerBuilderInitial, PgIntervalBuilderInitial, PgJsonBuilderInitial, PgJsonbBuilderInitial, PgLineABCBuilderInitial, PgLineBuilderInitial, PgLineTypeConfig, PgMacaddr8BuilderInitial, PgMacaddrBuilderInitial, PgMaterializedViewWithConfig, PgMaterializedViewWithSelection, PgNumericBuilderInitial, PgNumericConfig, PgPointConfig, PgPointObjectBuilderInitial, PgPointTupleBuilderInitial, PgPolicyConfig, PgPolicyToOption, PgQueryResultHKT, PgQueryResultKind, PgRealBuilderInitial, PgRoleConfig, PgSelect, PgSelectConfig, PgSelectDynamic, PgSelectHKT, PgSelectHKTBase, PgSelectJoin, PgSelectJoinConfig, PgSelectJoinFn, PgSelectKind, PgSelectPrepare, PgSelectQueryBuilder, PgSelectQueryBuilderHKT, PgSelectWithout, PgSequenceOptions, PgSerialBuilderInitial, PgSetOperator, PgSetOperatorExcludedMethods, PgSetOperatorInterface, PgSetOperatorWithResult, PgSmallIntBuilderInitial, PgSmallSerialBuilderInitial, PgSparseVectorBuilderInitial, PgSparseVectorConfig, PgTableExtraConfig, PgTableExtraConfigValue, PgTableFn, PgTableWithColumns, PgTextConfig, PgTimeBuilderInitial, PgTimestampBuilderInitial, PgTimestampConfig, PgTimestampStringBuilderInitial, PgTransactionConfig, PgUUIDBuilderInitial, PgUpdate, PgUpdateConfig, PgUpdateDynamic, PgUpdateJoin, PgUpdateJoinFn, PgUpdatePrepare, PgUpdateReturning, PgUpdateReturningAll, PgUpdateSetSource, PgUpdateWithJoins, PgUpdateWithout, PgVarcharBuilderInitial, PgVarcharConfig, PgVectorBuilderInitial, PgVectorConfig, PgViewWithSelection, PgWithReplicas, Precision, PreparedQueryConfig, Reference, ReferenceConfig, SelectedFields, SelectedFieldsFlat, SelectedFieldsOrdered, SetOperatorRestSelect, SetOperatorRightSelect, SubqueryWithSelection, TableConfig, TimeConfig, UpdateDeleteAction, ViewWithConfig, WithSubqueryWithSelection, } from 'drizzle-orm/pg-core';
2
3
  export { tenants, people, orders, appointments, products, items, plgConnections, plgSyncRuns } from './schema/spine';
3
4
  export { tenantId, timestamps, createdAt } from './lib/helpers';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,qBAAqB,CAAA;AAEnC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACpH,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { pgTable, uuid, timestamp, jsonb, text, boolean, integer } from 'drizzle-orm/pg-core';
2
- export * from 'drizzle-orm/pg-core';
2
+ export { Check, CheckBuilder, DefaultViewBuilderCore, ExtraConfigColumn, ForeignKey, ForeignKeyBuilder, Index, IndexBuilder, IndexBuilderOn, IndexedColumn, ManualMaterializedViewBuilder, ManualViewBuilder, MaterializedViewBuilder, MaterializedViewBuilderCore, PgArray, PgArrayBuilder, PgBigInt53, PgBigInt53Builder, PgBigInt64, PgBigInt64Builder, PgBigSerial53, PgBigSerial53Builder, PgBigSerial64, PgBigSerial64Builder, PgBinaryVector, PgBinaryVectorBuilder, PgBoolean, PgBooleanBuilder, PgChar, PgCharBuilder, PgCidr, PgCidrBuilder, PgColumn, PgColumnBuilder, PgCustomColumn, PgCustomColumnBuilder, PgDatabase, PgDate, PgDateBuilder, PgDateString, PgDateStringBuilder, PgDeleteBase, PgDialect, PgDoublePrecision, PgDoublePrecisionBuilder, PgEnumColumn, PgEnumColumnBuilder, PgGeometry, PgGeometryBuilder, PgGeometryObject, PgGeometryObjectBuilder, PgHalfVector, PgHalfVectorBuilder, PgInet, PgInetBuilder, PgInsertBase, PgInsertBuilder, PgIntColumnBaseBuilder, PgInteger, PgIntegerBuilder, PgInterval, PgIntervalBuilder, PgJson, PgJsonBuilder, PgJsonb, PgJsonbBuilder, PgLineABC, PgLineABCBuilder, PgLineBuilder, PgLineTuple, PgMacaddr, PgMacaddr8, PgMacaddr8Builder, PgMacaddrBuilder, PgMaterializedView, PgMaterializedViewConfig, PgNumeric, PgNumericBuilder, PgPointObject, PgPointObjectBuilder, PgPointTuple, PgPointTupleBuilder, PgPolicy, PgPreparedQuery, PgReal, PgRealBuilder, PgRefreshMaterializedView, PgRole, PgSchema, PgSelectBase, PgSelectBuilder, PgSelectQueryBuilderBase, PgSequence, PgSerial, PgSerialBuilder, PgSession, PgSmallInt, PgSmallIntBuilder, PgSmallSerial, PgSmallSerialBuilder, PgSparseVector, PgSparseVectorBuilder, PgTable, PgText, PgTextBuilder, PgTime, PgTimeBuilder, PgTimestamp, PgTimestampBuilder, PgTimestampString, PgTimestampStringBuilder, PgTransaction, PgUUID, PgUUIDBuilder, PgUpdateBase, PgUpdateBuilder, PgVarchar, PgVarcharBuilder, PgVector, PgVectorBuilder, PgView, PgViewConfig, PrimaryKey, PrimaryKeyBuilder, QueryBuilder, UniqueConstraint, UniqueConstraintBuilder, UniqueOnConstraintBuilder, ViewBuilder, alias, bigint, bigserial, bit, boolean, char, check, cidr, customType, date, decimal, doublePrecision, except, exceptAll, foreignKey, geometry, getMaterializedViewConfig, getTableConfig, getViewConfig, halfvec, index, inet, integer, intersect, intersectAll, interval, isPgEnum, isPgMaterializedView, isPgSchema, isPgSequence, isPgView, json, jsonb, line, macaddr, macaddr8, makePgArray, numeric, parsePgArray, parsePgNestedArray, pgEnum, pgMaterializedView, pgPolicy, pgRole, pgSchema, pgSequence, pgTable, pgTableCreator, pgView, point, primaryKey, real, serial, smallint, smallserial, sparsevec, text, time, timestamp, union, unionAll, unique, uniqueIndex, uniqueKeyName, uuid, varchar, vector, withReplicas } from 'drizzle-orm/pg-core';
3
3
 
4
4
  // src/index.ts
5
5
  var tenants = pgTable("tenants", {
@@ -0,0 +1,93 @@
1
+ -- ============================================================================
2
+ -- 197 — a unit has its own currency
3
+ --
4
+ -- One company, two shops: Rio sells in BRL, Barcelona in EUR. The `currency`
5
+ -- setting already existed, but only for the whole tenant, and nothing read it:
6
+ -- every order was born 'BRL' from a column default.
7
+ --
8
+ -- The currency is now a UNIT setting (the tenant's value stays the fallback,
9
+ -- which is what "the head office's currency" means), and an order with no
10
+ -- currency takes its unit's. A document keeps the currency it was written in:
11
+ -- changing the shop's currency later does not rewrite what was already sold.
12
+ --
13
+ -- A price list already carries its currency, so "one price per currency" is a
14
+ -- price list per currency. What it could not do was price a variant, and a
15
+ -- shirt is sold by size.
16
+ -- ============================================================================
17
+
18
+ SELECT app.seed_setting_keys($$[
19
+ { "key": "currency", "type": "string", "default": "BRL", "scope": "unit",
20
+ "description": "Currency this unit sells, buys and keeps its books in (ISO 4217). The tenant's value is the default for every unit." }
21
+ ]$$::jsonb);
22
+
23
+ CREATE OR REPLACE FUNCTION app.unit_currency(p_tenant uuid, p_unit uuid)
24
+ RETURNS text
25
+ LANGUAGE sql
26
+ STABLE SECURITY DEFINER
27
+ SET search_path TO ''
28
+ AS $function$
29
+ SELECT upper(coalesce(nullif(btrim(app.unit_setting_value(p_tenant, p_unit, 'currency') #>> '{}'), ''), 'BRL'));
30
+ $function$;
31
+ -- Takes the tenant as an argument, so it is not a client door.
32
+ REVOKE ALL ON FUNCTION app.unit_currency(uuid, uuid) FROM PUBLIC, anon, authenticated;
33
+ GRANT EXECUTE ON FUNCTION app.unit_currency(uuid, uuid) TO service_role;
34
+
35
+ -- What the signed-in user asks: "which currency is this unit of mine in?"
36
+ CREATE OR REPLACE FUNCTION public.my_unit_currency(p_unit uuid DEFAULT NULL)
37
+ RETURNS text
38
+ LANGUAGE sql
39
+ STABLE SECURITY DEFINER
40
+ SET search_path TO ''
41
+ AS $function$
42
+ SELECT app.unit_currency(app.current_tenant_id(), p_unit);
43
+ $function$;
44
+ REVOKE ALL ON FUNCTION public.my_unit_currency(uuid) FROM PUBLIC, anon;
45
+ GRANT EXECUTE ON FUNCTION public.my_unit_currency(uuid) TO authenticated, service_role;
46
+
47
+ -- ── an order is born in its unit's currency ────────────────────────────────
48
+ ALTER TABLE public.orders ALTER COLUMN currency DROP DEFAULT;
49
+
50
+ CREATE OR REPLACE FUNCTION public.trg_orders_currency_default()
51
+ RETURNS trigger
52
+ LANGUAGE plpgsql
53
+ SECURITY DEFINER
54
+ SET search_path TO ''
55
+ AS $function$
56
+ BEGIN
57
+ IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN
58
+ NEW.currency := app.unit_currency(NEW.tenant_id, NEW.unit_id);
59
+ ELSE
60
+ NEW.currency := upper(btrim(NEW.currency));
61
+ END IF;
62
+ RETURN NEW;
63
+ END $function$;
64
+
65
+ -- "zz": BEFORE triggers fire in name order, and the unit is stamped by
66
+ -- `orders_scope_stamp`. This one has to read the unit after that.
67
+ DROP TRIGGER IF EXISTS orders_zz_currency_default ON public.orders;
68
+ CREATE TRIGGER orders_zz_currency_default
69
+ BEFORE INSERT ON public.orders
70
+ FOR EACH ROW EXECUTE FUNCTION public.trg_orders_currency_default();
71
+
72
+ -- ── a price list can price a variant ───────────────────────────────────────
73
+ ALTER TABLE public.price_table_items
74
+ ADD COLUMN IF NOT EXISTS variant_id uuid REFERENCES public.product_variants(id) ON DELETE CASCADE;
75
+ COMMENT ON COLUMN public.price_table_items.variant_id IS
76
+ 'Price for one variant when it differs; NULL = the price of the whole product.';
77
+
78
+ -- Same constraint, one more column: NULLS NOT DISTINCT keeps "one product-wide
79
+ -- price per quantity break" unique, which a plain UNIQUE would stop enforcing.
80
+ DO $$
81
+ BEGIN
82
+ IF NOT EXISTS (
83
+ SELECT 1 FROM pg_constraint c
84
+ WHERE c.conrelid = 'public.price_table_items'::regclass AND c.conname = 'price_table_items_unique'
85
+ AND pg_get_constraintdef(c.oid) LIKE '%variant_id%') THEN
86
+ ALTER TABLE public.price_table_items DROP CONSTRAINT IF EXISTS price_table_items_unique;
87
+ ALTER TABLE public.price_table_items
88
+ ADD CONSTRAINT price_table_items_unique
89
+ UNIQUE NULLS NOT DISTINCT (price_table_id, product_id, variant_id, min_qty);
90
+ END IF;
91
+ END $$;
92
+ CREATE INDEX IF NOT EXISTS price_table_items_variant_idx
93
+ ON public.price_table_items (tenant_id, variant_id) WHERE variant_id IS NOT NULL;
@@ -0,0 +1,136 @@
1
+ -- ============================================================================
2
+ -- 198 — permission holes close
3
+ --
4
+ -- Three holes found while mapping V1 permissions onto V2:
5
+ --
6
+ -- 1. `role_set_permissions` checked only `roles.manage`. Admin holds it, so an
7
+ -- admin could hand `billing.*` / `admin.*` (owner_only, 029) to any role,
8
+ -- including their own. Now only the owner may GRANT an owner_only key;
9
+ -- keys a role already holds survive an edit (029: owner_only governs what is
10
+ -- granted from now on, it does not revoke).
11
+ --
12
+ -- 2. `public.rooms` (153) let any tenant member write. The screen is gated by
13
+ -- `services.*` (148), so the table is too. Reading stays open to members:
14
+ -- the agenda needs every room to place an appointment.
15
+ --
16
+ -- 3. Screens gated by keys nobody could hold — the catalog lacked them, so
17
+ -- only the owner (who reads the catalog) saw them, and `role_set_permissions`
18
+ -- refused them as unknown. `workforce.*` mirrors `workforce_hr.*` (same
19
+ -- audience: who runs the team's time); `reservations.*` mirrors `tables.*`
20
+ -- (who runs the floor). Mirroring grants nobody anything new in spirit.
21
+ --
22
+ -- Idempotent.
23
+ -- ============================================================================
24
+
25
+ CREATE OR REPLACE FUNCTION public.role_set_permissions(p_role_id uuid, p_keys text[]) RETURNS integer
26
+ LANGUAGE plpgsql SECURITY DEFINER
27
+ SET search_path TO ''
28
+ AS $$
29
+ DECLARE
30
+ v_tenant uuid := app.current_tenant_id();
31
+ v_role app.roles%ROWTYPE;
32
+ v_unknown text;
33
+ v_reserved text;
34
+ v_keys text[] := coalesce(p_keys, '{}');
35
+ v_n integer;
36
+ BEGIN
37
+ IF auth.uid() IS NULL OR v_tenant IS NULL THEN
38
+ RAISE EXCEPTION 'role_set_permissions: no active tenant for this session' USING ERRCODE = '42501';
39
+ END IF;
40
+ IF NOT app.has_permission('roles.manage') THEN
41
+ RAISE EXCEPTION 'role_set_permissions: roles.manage is required' USING ERRCODE = '42501';
42
+ END IF;
43
+ SELECT * INTO v_role FROM app.roles r WHERE r.tenant_id = v_tenant AND r.id = p_role_id;
44
+ IF NOT FOUND THEN
45
+ RAISE EXCEPTION 'role_set_permissions: role not found in the active tenant' USING ERRCODE = 'P0002';
46
+ END IF;
47
+ IF v_role.is_system AND v_role.key = 'owner' THEN
48
+ RAISE EXCEPTION 'role_set_permissions: the owner role is immutable (it always holds the whole catalog)' USING ERRCODE = '42501';
49
+ END IF;
50
+ SELECT string_agg(k, ', ' ORDER BY k) INTO v_unknown
51
+ FROM unnest(v_keys) k WHERE NOT EXISTS (SELECT 1 FROM app.permissions p WHERE p.key = k);
52
+ IF v_unknown IS NOT NULL THEN
53
+ RAISE EXCEPTION 'role_set_permissions: unknown permission key(s): %', v_unknown USING ERRCODE = '23503';
54
+ END IF;
55
+
56
+ IF NOT EXISTS (
57
+ SELECT 1 FROM app.memberships m
58
+ JOIN app.roles r ON r.id = m.role_id AND r.tenant_id = m.tenant_id
59
+ WHERE m.tenant_id = v_tenant AND m.user_id = auth.uid() AND m.active AND r.key = 'owner'
60
+ ) THEN
61
+ SELECT string_agg(k, ', ' ORDER BY k) INTO v_reserved
62
+ FROM unnest(v_keys) k
63
+ JOIN app.permissions p ON p.key = k AND p.owner_only
64
+ WHERE NOT EXISTS (
65
+ SELECT 1 FROM app.role_permissions rp
66
+ WHERE rp.tenant_id = v_tenant AND rp.role_id = p_role_id AND rp.permission = k);
67
+ IF v_reserved IS NOT NULL THEN
68
+ RAISE EXCEPTION 'role_set_permissions: only the owner can grant %', v_reserved USING ERRCODE = '42501';
69
+ END IF;
70
+ END IF;
71
+
72
+ DELETE FROM app.role_permissions rp
73
+ WHERE rp.tenant_id = v_tenant AND rp.role_id = p_role_id AND rp.permission <> ALL (v_keys);
74
+ INSERT INTO app.role_permissions (tenant_id, role_id, permission)
75
+ SELECT v_tenant, p_role_id, k FROM unnest(v_keys) k
76
+ ON CONFLICT DO NOTHING;
77
+
78
+ SELECT count(*) INTO v_n FROM app.role_permissions rp WHERE rp.tenant_id = v_tenant AND rp.role_id = p_role_id;
79
+ RETURN v_n;
80
+ END $$;
81
+
82
+ -- ── 2. rooms ────────────────────────────────────────────────────────────────
83
+ DROP POLICY IF EXISTS rooms_member_all ON public.rooms;
84
+ DROP POLICY IF EXISTS rooms_member_read ON public.rooms;
85
+ DROP POLICY IF EXISTS rooms_insert ON public.rooms;
86
+ DROP POLICY IF EXISTS rooms_update ON public.rooms;
87
+ DROP POLICY IF EXISTS rooms_delete ON public.rooms;
88
+
89
+ CREATE POLICY rooms_member_read ON public.rooms FOR SELECT TO authenticated
90
+ USING (tenant_id IN (SELECT public.user_tenant_ids()));
91
+ CREATE POLICY rooms_insert ON public.rooms FOR INSERT TO authenticated
92
+ WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids())
93
+ AND (SELECT app.has_permission('services.create', unit_id)));
94
+ CREATE POLICY rooms_update ON public.rooms FOR UPDATE TO authenticated
95
+ USING (tenant_id IN (SELECT public.user_tenant_ids())
96
+ AND (SELECT app.has_permission('services.edit', unit_id)))
97
+ WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids())
98
+ AND (SELECT app.has_permission('services.edit', unit_id)));
99
+ CREATE POLICY rooms_delete ON public.rooms FOR DELETE TO authenticated
100
+ USING (tenant_id IN (SELECT public.user_tenant_ids())
101
+ AND (SELECT app.has_permission('services.delete', unit_id)));
102
+
103
+ -- ── 3. keys the screens already ask for ─────────────────────────────────────
104
+ INSERT INTO app.permissions (key, plugin, description)
105
+ VALUES
106
+ ('workforce.read', 'workforce', 'Ver escalas, jornadas e batidas de ponto'),
107
+ ('workforce.create', 'workforce', 'Criar escala, turno ou lançamento de ponto'),
108
+ ('workforce.edit', 'workforce', 'Alterar escala, turno ou lançamento de ponto'),
109
+ ('workforce.delete', 'workforce', 'Excluir escala, turno ou lançamento de ponto'),
110
+ ('workforce.manage', 'workforce', 'Coringa de escalas e ponto'),
111
+ ('reservations.read', 'reservations', 'Ver reservas'),
112
+ ('reservations.create', 'reservations', 'Registrar reserva'),
113
+ ('reservations.edit', 'reservations', 'Alterar ou confirmar reserva'),
114
+ ('reservations.delete', 'reservations', 'Cancelar ou excluir reserva'),
115
+ ('reservations.manage', 'reservations', 'Coringa de reservas')
116
+ ON CONFLICT (key) DO NOTHING;
117
+
118
+ INSERT INTO app.role_permissions (role_id, permission, tenant_id)
119
+ SELECT p.role_id, replace(p.permission, 'workforce_hr.', 'workforce.'), p.tenant_id
120
+ FROM app.role_permissions p
121
+ WHERE p.permission IN ('workforce_hr.read', 'workforce_hr.create', 'workforce_hr.edit', 'workforce_hr.delete')
122
+ ON CONFLICT DO NOTHING;
123
+
124
+ INSERT INTO app.role_permissions (role_id, permission, tenant_id)
125
+ SELECT p.role_id, replace(p.permission, 'tables.', 'reservations.'), p.tenant_id
126
+ FROM app.role_permissions p
127
+ WHERE p.permission IN ('tables.read', 'tables.create', 'tables.edit', 'tables.delete', 'tables.manage')
128
+ ON CONFLICT DO NOTHING;
129
+
130
+ -- Administrator reads the catalog minus owner_only (029): new keys reach it too.
131
+ INSERT INTO app.role_permissions (tenant_id, role_id, permission)
132
+ SELECT r.tenant_id, r.id, p.key
133
+ FROM app.roles r
134
+ JOIN app.permissions p ON p.key ~ '^(workforce|reservations)\.(read|create|edit|delete|manage)$'
135
+ WHERE r.key = 'admin' AND p.owner_only = false
136
+ ON CONFLICT DO NOTHING;
@@ -0,0 +1,82 @@
1
+ -- ============================================================================
2
+ -- 199 — field rules speak the CRUD entity
3
+ --
4
+ -- V1's required_field_rules came across with two errors:
5
+ -- 1. the allowlist pinned state='required', so V1 rows that only said
6
+ -- "visible" landed as required (Espaço Facial: 8 required became 12);
7
+ -- 2. they kept V1's vocabulary — reference 'clients', field 'cpf' — which no
8
+ -- V2 screen reads. V2 keys a rule by the CRUD entity (archetype:kind,
9
+ -- stored snake: `person_customer`) and the entity's own FieldDef keys.
10
+ --
11
+ -- The writer now takes V1's `visibility` and translates entity and field on
12
+ -- the way in. A V1 field V2 has no place for is skipped with its reason
13
+ -- instead of landing as a rule nothing reads.
14
+ --
15
+ -- Rows already imported are corrected by a re-run of the field-rule import,
16
+ -- not here: the V1 values live in the V1 database.
17
+ --
18
+ -- Idempotent.
19
+ -- ============================================================================
20
+
21
+ DO $do$
22
+ BEGIN
23
+ IF to_regprocedure('migration.upsert_field_rule_v1row(uuid, uuid, jsonb, jsonb)') IS NULL THEN
24
+ ALTER FUNCTION migration.upsert_field_rule(uuid, uuid, jsonb, jsonb) RENAME TO upsert_field_rule_v1row;
25
+ END IF;
26
+ END $do$;
27
+
28
+ CREATE OR REPLACE FUNCTION migration.upsert_field_rule(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
29
+ RETURNS jsonb
30
+ LANGUAGE plpgsql SECURITY DEFINER
31
+ SET search_path TO ''
32
+ AS $$
33
+ DECLARE
34
+ v_entities jsonb := '{"clients":"person_customer","professionals":"person_staff","employees":"person_staff","suppliers":"person_supplier"}';
35
+ v_fields jsonb := '{
36
+ "person_customer": {"name":"name","cpf":"documentNumber","rg":"rg","birth_date":"dateOfBirth","gender":"gender",
37
+ "phone":"phone","mobile":"mobile","email":"email","social_name":"socialName",
38
+ "profession":"profession","origin":"originId","warnings":"warnings","address":"address"},
39
+ "person_staff": {"name":"name","cpf":"documentNumber","email":"email","phone":"phone",
40
+ "specialty":"profession","commission_rate":"commissionRate"},
41
+ "person_supplier": {"name":"name","cpf_cnpj":"documentNumber","phone":"phone","mobile":"mobile","email":"email","address":"address"}
42
+ }';
43
+ v_ref_in text := lower(nullif(btrim(coalesce(p_row ->> 'reference', '')), ''));
44
+ v_ref text := coalesce(v_entities ->> v_ref_in, v_ref_in);
45
+ v_key_in text := nullif(btrim(coalesce(p_row ->> 'field_key', '')), '');
46
+ v_key text;
47
+ BEGIN
48
+ IF v_fields ? v_ref THEN
49
+ v_key := v_fields -> v_ref ->> v_key_in;
50
+ IF v_key IS NULL THEN
51
+ RETURN jsonb_build_object('status', 'skipped', 'kind', 'value',
52
+ 'reason', format('V1 field %L of %L has no field in the V2 %s form (V2 keeps the address as one field, ruled by `address`)', v_key_in, v_ref_in, v_ref));
53
+ END IF;
54
+ ELSE
55
+ v_key := v_key_in;
56
+ END IF;
57
+ RETURN migration.upsert_field_rule_v1row(
58
+ p_tenant, p_id,
59
+ p_row || jsonb_build_object('reference', v_ref, 'field_key', v_key,
60
+ 'state', coalesce(p_row ->> 'visibility', p_row ->> 'state', p_args ->> 'state', p_args ->> 'default_state')),
61
+ p_args - 'state' - 'default_state');
62
+ END $$;
63
+
64
+ REVOKE ALL ON FUNCTION migration.upsert_field_rule(uuid, uuid, jsonb, jsonb) FROM PUBLIC;
65
+ GRANT ALL ON FUNCTION migration.upsert_field_rule(uuid, uuid, jsonb, jsonb) TO service_role;
66
+ REVOKE ALL ON FUNCTION migration.upsert_field_rule_v1row(uuid, uuid, jsonb, jsonb) FROM PUBLIC;
67
+
68
+ DO $unlock$
69
+ BEGIN
70
+ PERFORM set_config('migration.allowlist_unlock', 'on', true);
71
+ -- A V1 row with no `visibility` (older exports) still means required.
72
+ PERFORM migration._allow('required_field_rules', 'public.field_rules', 'config', 'field_rule', '{"default_state":"required"}', '{}',
73
+ '{"uuid":"id","context":"context","entity":"reference","field_key":"field_key","visibility":"visibility","unit_id":"unit_id@companies"}',
74
+ '199: V1 `visibility` is carried, not pinned to required; entity and field are translated to the V2 CRUD entity by the writer.');
75
+ END $unlock$;
76
+
77
+ -- V1's two form contexts. The live cluster already has them (its rules use
78
+ -- them); the chain did not.
79
+ INSERT INTO public.field_rule_contexts (key, plugin, description) VALUES
80
+ ('main_form', 'base', 'The full registry form (V1 "Cadastro principal")'),
81
+ ('quick_search', 'base', 'The quick add from a search box (V1 "Busca rápida")')
82
+ ON CONFLICT (key) DO NOTHING;
@@ -0,0 +1,20 @@
1
+ -- ============================================================================
2
+ -- 200 — team members have a name
3
+ --
4
+ -- Choosing "who can authorize" needs a list of logins with names. Memberships
5
+ -- carry the login, people carry the name, and no readable view joined the two:
6
+ -- v_team_members has user_id and person_id but no name.
7
+ --
8
+ -- security_invoker: it reads with the caller's own RLS on people and
9
+ -- memberships, so it shows exactly the members the caller may already see.
10
+ --
11
+ -- Idempotent.
12
+ -- ============================================================================
13
+ CREATE OR REPLACE VIEW public.v_team_member_names
14
+ WITH (security_invoker = true) AS
15
+ SELECT m.tenant_id, m.user_id, m.person_id, coalesce(nullif(p.name, ''), 'Sem nome') AS name
16
+ FROM app.memberships m
17
+ LEFT JOIN public.people p ON p.id = m.person_id
18
+ WHERE m.active;
19
+
20
+ GRANT SELECT ON public.v_team_member_names TO authenticated;
@@ -0,0 +1,39 @@
1
+ -- 201: features that were an on/off setting become facets a plan sells apart
2
+ -- (time clock, AtendeAI, cashback club, kiosks, waitlist, table tablets, scale,
3
+ -- QR menu). A plan that already lists any grant denies an unlisted facet
4
+ -- (`plan_permits` reads silence per plan, not per facet — see 164), so each
5
+ -- facet is granted as `optional` wherever its plugin is granted: the account
6
+ -- may turn it on in Módulos, and pricing counts it once it does.
7
+ -- Additive and idempotent; plans that do not grant the plugin stay as they are.
8
+
9
+ DO $$
10
+ DECLARE
11
+ v record;
12
+ BEGIN
13
+ IF to_regprocedure('public.register_plan_grant(text,text,text,text,text)') IS NULL THEN
14
+ RAISE NOTICE '201: register_plan_grant missing — skipped (pool without app.plan_grants)';
15
+ RETURN;
16
+ END IF;
17
+
18
+ FOR v IN
19
+ SELECT g.plan_id, f.plugin_id, f.facet
20
+ FROM (VALUES
21
+ ('workforce', 'time_clock'),
22
+ ('conversations', 'atendeai'),
23
+ ('crm', 'loyalty'),
24
+ ('agenda', 'kiosk'),
25
+ ('tables', 'waitlist'),
26
+ ('tables', 'tablets'),
27
+ ('orders', 'scale'),
28
+ ('menu', 'qr_menu')
29
+ ) AS f(plugin_id, facet)
30
+ JOIN app.plan_grants g ON g.plugin_id = f.plugin_id AND g.facet = ''
31
+ WHERE NOT EXISTS (
32
+ SELECT 1 FROM app.plan_grants x
33
+ WHERE x.plan_id = g.plan_id AND x.plugin_id = f.plugin_id AND x.facet = f.facet
34
+ )
35
+ LOOP
36
+ PERFORM public.register_plan_grant(v.plan_id, v.plugin_id, v.facet, 'optional',
37
+ 'Was an on/off setting; now a paid part the account turns on in Módulos.');
38
+ END LOOP;
39
+ END $$;
@@ -0,0 +1,173 @@
1
+ -- 202: a unit can switch a module off for itself. A network sells at the
2
+ -- counter in three units and not in its services unit: the account has Pedidos,
3
+ -- that unit does not. Absence inherits the account; only an explicit 'disabled' row
4
+ -- narrows, so a unit can never turn on what the account (and its plan) does not
5
+ -- have — which is why the write needs no plan check.
6
+ -- Additive and idempotent. app_config() is re-emitted from the FILE of its last
7
+ -- definition (138) with one more key; nothing else in it changes.
8
+
9
+ CREATE TABLE IF NOT EXISTS app.unit_plugins (
10
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
11
+ unit_id uuid NOT NULL,
12
+ plugin_id text NOT NULL CHECK (plugin_id ~ '^[a-z][a-z0-9-]*$'),
13
+ facet text NOT NULL DEFAULT '' CHECK (facet = '' OR facet ~ '^[a-z][a-z0-9_-]*$'),
14
+ status text NOT NULL DEFAULT 'disabled' CHECK (status IN ('active', 'disabled')),
15
+ updated_by uuid,
16
+ updated_at timestamptz NOT NULL DEFAULT now(),
17
+ PRIMARY KEY (tenant_id, unit_id, plugin_id, facet),
18
+ FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE CASCADE
19
+ );
20
+
21
+ ALTER TABLE app.unit_plugins ENABLE ROW LEVEL SECURITY;
22
+ ALTER TABLE app.unit_plugins FORCE ROW LEVEL SECURITY;
23
+
24
+ DO $$
25
+ BEGIN
26
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'app' AND tablename = 'unit_plugins' AND policyname = 'unit_plugins_read') THEN
27
+ -- Same reach as the account's own switches: every member renders the menu
28
+ -- of whichever unit they stand in. Writes go through unit_plugin_set only.
29
+ CREATE POLICY unit_plugins_read ON app.unit_plugins FOR SELECT TO authenticated
30
+ USING (tenant_id IN (SELECT public.user_tenant_ids()));
31
+ END IF;
32
+ END $$;
33
+
34
+ REVOKE ALL ON app.unit_plugins FROM PUBLIC, anon;
35
+ GRANT SELECT ON app.unit_plugins TO authenticated;
36
+ GRANT ALL ON app.unit_plugins TO service_role;
37
+
38
+ -- Whether a module (or one facet of it) may run in a unit of the caller's
39
+ -- account: not switched off by the account, nor by that unit. A plugin the
40
+ -- account never mentions counts as on — the manifest default — exactly as the
41
+ -- shell reads it. Meant for RLS and RPC guards.
42
+ CREATE OR REPLACE FUNCTION app.plugin_enabled_for_unit(p_plugin text, p_unit uuid, p_facet text DEFAULT '')
43
+ RETURNS boolean
44
+ LANGUAGE sql STABLE SECURITY DEFINER
45
+ SET search_path TO ''
46
+ AS $$
47
+ SELECT NOT EXISTS (
48
+ SELECT 1 FROM app.tenant_plugins t
49
+ WHERE t.tenant_id = app.current_tenant_id() AND t.plugin_id = p_plugin
50
+ AND t.facet IN ('', coalesce(p_facet, ''))
51
+ AND (t.status = 'disabled' OR (t.expires_at IS NOT NULL AND t.expires_at <= now())))
52
+ AND NOT EXISTS (
53
+ SELECT 1 FROM app.unit_plugins u
54
+ WHERE u.tenant_id = app.current_tenant_id() AND u.unit_id = p_unit AND u.plugin_id = p_plugin
55
+ AND u.facet IN ('', coalesce(p_facet, '')) AND u.status = 'disabled')
56
+ $$;
57
+ REVOKE ALL ON FUNCTION app.plugin_enabled_for_unit(text, uuid, text) FROM PUBLIC;
58
+ GRANT EXECUTE ON FUNCTION app.plugin_enabled_for_unit(text, uuid, text) TO authenticated, service_role;
59
+
60
+ -- The only door: switch a module off in one unit, or back to what the account has.
61
+ CREATE OR REPLACE FUNCTION public.unit_plugin_set(p_unit uuid, p_plugin text, p_facet text DEFAULT '', p_status text DEFAULT 'disabled')
62
+ RETURNS jsonb
63
+ LANGUAGE plpgsql SECURITY DEFINER
64
+ SET search_path TO ''
65
+ AS $$
66
+ DECLARE
67
+ v_tenant uuid := app.current_tenant_id();
68
+ BEGIN
69
+ IF v_tenant IS NULL THEN
70
+ RAISE EXCEPTION 'no tenant in scope' USING ERRCODE = '42501';
71
+ END IF;
72
+ IF NOT app.has_permission('settings.manage') THEN
73
+ RAISE EXCEPTION 'settings.manage is required to change what a unit has' USING ERRCODE = '42501';
74
+ END IF;
75
+ IF p_status NOT IN ('active', 'disabled') THEN
76
+ RAISE EXCEPTION 'status must be active or disabled' USING ERRCODE = '22023';
77
+ END IF;
78
+ IF NOT EXISTS (SELECT 1 FROM app.units WHERE tenant_id = v_tenant AND id = p_unit) THEN
79
+ RAISE EXCEPTION 'unit % is not in this account', p_unit USING ERRCODE = '42501';
80
+ END IF;
81
+
82
+ IF p_status = 'active' THEN
83
+ -- Back to the account's choice: the row goes, the unit inherits.
84
+ DELETE FROM app.unit_plugins
85
+ WHERE tenant_id = v_tenant AND unit_id = p_unit AND plugin_id = p_plugin AND facet = coalesce(p_facet, '');
86
+ ELSE
87
+ INSERT INTO app.unit_plugins (tenant_id, unit_id, plugin_id, facet, status, updated_by)
88
+ VALUES (v_tenant, p_unit, p_plugin, coalesce(p_facet, ''), 'disabled', auth.uid())
89
+ ON CONFLICT (tenant_id, unit_id, plugin_id, facet)
90
+ DO UPDATE SET status = 'disabled', updated_by = EXCLUDED.updated_by, updated_at = now();
91
+ END IF;
92
+
93
+ RETURN jsonb_build_object('unit', p_unit, 'plugin', p_plugin, 'facet', nullif(coalesce(p_facet, ''), ''),
94
+ 'status', CASE WHEN p_status = 'active' THEN 'inherited' ELSE 'disabled' END);
95
+ END $$;
96
+ REVOKE ALL ON FUNCTION public.unit_plugin_set(uuid, text, text, text) FROM PUBLIC;
97
+ GRANT EXECUTE ON FUNCTION public.unit_plugin_set(uuid, text, text, text) TO authenticated, service_role;
98
+
99
+ CREATE OR REPLACE FUNCTION public.app_config()
100
+ RETURNS jsonb
101
+ LANGUAGE plpgsql
102
+ STABLE SECURITY DEFINER
103
+ SET search_path TO ''
104
+ AS $function$
105
+ DECLARE
106
+ v_tenant uuid := app.current_tenant_id();
107
+ v_staff text;
108
+ BEGIN
109
+ -- `platform` é do CLUSTER, não da cadeia canônica: um banco montado do zero
110
+ -- (o db-from-zero, um pool novo) não tem esse schema, e uma chamada direta
111
+ -- estoura na inicialização da variável — antes de qualquer linha do corpo.
112
+ IF pg_catalog.to_regproc('platform.staff_role(uuid)') IS NOT NULL THEN
113
+ EXECUTE 'SELECT platform.staff_role($1)' INTO v_staff USING (SELECT auth.uid());
114
+ END IF;
115
+
116
+ IF v_tenant IS NULL THEN
117
+ RETURN jsonb_build_object('tenant_id', NULL, 'plugins', '[]'::jsonb, 'nav', '[]'::jsonb,
118
+ 'apps', '[]'::jsonb, 'permissions', '[]'::jsonb, 'unit_plugins', '[]'::jsonb, 'generated_at', now());
119
+ END IF;
120
+
121
+ RETURN jsonb_build_object(
122
+ 'tenant_id', v_tenant,
123
+ 'branding', coalesce(app.brand_of(v_tenant), '{}'::jsonb),
124
+ 'settings', public.tenant_settings(),
125
+ 'plugins', coalesce((
126
+ SELECT jsonb_agg(jsonb_build_object('plugin', t.plugin_id, 'facet', nullif(t.facet, ''),
127
+ 'status', CASE WHEN t.expires_at IS NOT NULL AND t.expires_at <= now() THEN 'disabled' ELSE t.status END,
128
+ 'source', t.source, 'config', t.config, 'expires_at', t.expires_at)
129
+ ORDER BY t.plugin_id, t.facet)
130
+ FROM app.tenant_plugins t WHERE t.tenant_id = v_tenant), '[]'::jsonb),
131
+ -- `trade` ordena a lista: quem roda dois aplicativos tem UM ofício, e o
132
+ -- outro é retaguarda (084). Entre dois SEM ofício, quem declara
133
+ -- `preset.rail` vence quem não declara (115).
134
+ --
135
+ -- O PRIMEIRO desta lista é quem empresta o rail (`useAppPreset`) e agora
136
+ -- também quem empresta a COR (`useAppTheme`) — as duas leituras têm de ser
137
+ -- o mesmo aplicativo, senão a casa abre com o menu de um produto e a cara
138
+ -- de outro.
139
+ 'apps', coalesce((
140
+ SELECT jsonb_agg(x ORDER BY x ->> 'house',
141
+ (x ->> 'trade')::boolean DESC,
142
+ (x -> 'preset' -> 'rail' IS NOT NULL) DESC,
143
+ x ->> 'name') FROM (
144
+ SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
145
+ 'url', a.url, 'house', false, 'preset', a.preset, 'theme', a.theme, 'vertical_id', a.vertical_id,
146
+ 'trade', a.vertical_id IS NOT NULL AND a.vertical_id NOT IN ('accounting','generic')) AS x
147
+ FROM app.tenant_apps ta JOIN app.apps a ON a.id = ta.app_id
148
+ WHERE ta.tenant_id = v_tenant AND ta.status = 'active' AND a.active
149
+ UNION ALL
150
+ SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
151
+ 'url', a.url, 'house', true, 'preset', a.preset, 'theme', a.theme, 'vertical_id', a.vertical_id,
152
+ 'trade', false) AS x
153
+ FROM app.apps a WHERE v_staff IS NOT NULL AND a.id = 'control-admin' AND a.active AND a.url IS NOT NULL) apps), '[]'::jsonb),
154
+ 'nav', coalesce((
155
+ SELECT jsonb_agg(jsonb_build_object('key', n.entry_key, 'hidden', n.hidden, 'label', n.label,
156
+ 'section', n.section, 'position', n.position) ORDER BY n.entry_key)
157
+ FROM app.tenant_nav n WHERE n.tenant_id = v_tenant), '[]'::jsonb),
158
+ 'permissions', coalesce((
159
+ SELECT jsonb_agg(DISTINCT rp.permission)
160
+ FROM app.memberships m
161
+ JOIN app.role_permissions rp ON rp.role_id = m.role_id AND rp.tenant_id = m.tenant_id
162
+ WHERE m.tenant_id = v_tenant AND m.user_id = (SELECT auth.uid()) AND m.active
163
+ ), '[]'::jsonb),
164
+ -- 202: what each unit switched off. Only the 'disabled' rows exist — a unit
165
+ -- with no row inherits the account — so the shell gets the whole map in
166
+ -- this same read and never asks per page.
167
+ 'unit_plugins', coalesce((
168
+ SELECT jsonb_agg(jsonb_build_object('unit_id', u.unit_id, 'plugin', u.plugin_id,
169
+ 'facet', nullif(u.facet, ''), 'status', u.status) ORDER BY u.unit_id, u.plugin_id, u.facet)
170
+ FROM app.unit_plugins u WHERE u.tenant_id = v_tenant), '[]'::jsonb),
171
+ 'generated_at', now());
172
+ END $function$;
173
+
@@ -0,0 +1,31 @@
1
+ -- 203: delivery (own fleet + iFood and other marketplaces) becomes the orders
2
+ -- facet `delivery`, turned on in Módulos and priced on its own. Two additive,
3
+ -- idempotent steps so nobody loses what they use today:
4
+ -- 1. every plan that grants orders also permits the facet (optional);
5
+ -- 2. every account with orders on gets the facet on — the delivery settings
6
+ -- were always there for them, and a new facet defaults to off.
7
+
8
+ DO $$
9
+ DECLARE
10
+ v record;
11
+ BEGIN
12
+ IF to_regprocedure('public.register_plan_grant(text,text,text,text,text)') IS NOT NULL THEN
13
+ FOR v IN
14
+ SELECT g.plan_id FROM app.plan_grants g
15
+ WHERE g.plugin_id = 'orders' AND g.facet = ''
16
+ AND NOT EXISTS (SELECT 1 FROM app.plan_grants x
17
+ WHERE x.plan_id = g.plan_id AND x.plugin_id = 'orders' AND x.facet = 'delivery')
18
+ LOOP
19
+ PERFORM public.register_plan_grant(v.plan_id, 'orders', 'delivery', 'optional',
20
+ 'Was always on with orders; now a module the account turns on in Módulos.');
21
+ END LOOP;
22
+ END IF;
23
+
24
+ IF to_regclass('app.tenant_plugins') IS NOT NULL THEN
25
+ INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, config, source)
26
+ SELECT tp.tenant_id, 'orders', 'delivery', 'active', '{}'::jsonb, 'default'
27
+ FROM app.tenant_plugins tp
28
+ WHERE tp.plugin_id = 'orders' AND tp.facet = '' AND tp.status = 'active'
29
+ ON CONFLICT (tenant_id, plugin_id, facet) DO NOTHING;
30
+ END IF;
31
+ END $$;