@fayz-ai/db 0.13.0 → 0.14.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.
Files changed (187) hide show
  1. package/canonical-schema.json +106 -23
  2. package/dist/index.cjs +4 -4
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +4 -4
  7. package/dist/index.js.map +1 -1
  8. package/dist/schema/spine.d.ts +42 -37
  9. package/dist/schema/spine.d.ts.map +1 -1
  10. package/migrations/000_baseline.sql +29667 -0
  11. package/migrations/001_the_tenant_has_apps.sql +314 -0
  12. package/migrations/002_the_spine_drops_the_vendor_and_the_dead.sql +129 -0
  13. package/migrations/003_the_role_rpcs_finish_and_the_mirrors_go.sql +135 -0
  14. package/migrations/004_the_projections_get_a_successor.sql +312 -0
  15. package/migrations/005_the_archetypes_anchor_to_the_unit.sql +90 -0
  16. package/migrations/006_the_helpers_read_the_source.sql +153 -0
  17. package/migrations/007_the_compat_machinery_leaves_with_the_tables.sql +77 -0
  18. package/migrations/008_the_switchers_see_across_accounts.sql +150 -0
  19. package/migrations/009_the_compat_aliases_nobody_adopted.sql +57 -0
  20. package/migrations/010_the_new_tenant_is_born_with_its_trade.sql +201 -0
  21. package/migrations/011_leadcontrol_joins_the_family.sql +116 -0
  22. package/migrations/012_chefcontrol_wears_its_own_mark.sql +28 -0
  23. package/migrations/013_the_workspace_switch_reaches_the_other_half.sql +156 -0
  24. package/migrations/014_the_chooser_can_read_what_it_offers.sql +91 -0
  25. package/migrations/015_the_unit_bindings_belong_to_one_workspace.sql +86 -0
  26. package/migrations/016_the_rule_stops_asking_every_row.sql +543 -0
  27. package/migrations/017_the_pin_is_checked_against_the_right_workspace.sql +139 -0
  28. package/migrations/018_the_bootstrap_says_what_you_reach.sql +114 -0
  29. package/migrations/019_distributing_one_row_stops_costing_the_table.sql +245 -0
  30. package/migrations/020_the_agenda_asks_where_someone_works.sql +81 -0
  31. package/migrations/022_the_line_says_which_document_it_belongs_to.sql +109 -0
  32. package/package.json +1 -1
  33. package/migrations/0000_legacy_quarantine.sql +0 -86
  34. package/migrations/0000b_straggler_sweep.sql +0 -37
  35. package/migrations/000_core_v1_convert.sql +0 -343
  36. package/migrations/001_core.sql +0 -372
  37. package/migrations/002_rls_user_tenant_ids.sql +0 -64
  38. package/migrations/003_invitations.sql +0 -17
  39. package/migrations/004_archetypes.sql +0 -343
  40. package/migrations/005_locations_archetype.sql +0 -18
  41. package/migrations/006_archetype_rls.sql +0 -74
  42. package/migrations/007_archetype_grants.sql +0 -4
  43. package/migrations/008_grants.sql +0 -5
  44. package/migrations/009_anon_hardening.sql +0 -19
  45. package/migrations/010_migration_ledger.sql +0 -46
  46. package/migrations/011_anon_write_revoke.sql +0 -12
  47. package/migrations/012_rename_migration_ledger.sql +0 -61
  48. package/migrations/013_sequences_rls.sql +0 -22
  49. package/migrations/014_plan_entitlements.sql +0 -28
  50. package/migrations/015_agent_guard.sql +0 -108
  51. package/migrations/016_agent_guard_actor.sql +0 -102
  52. package/migrations/017_core_addresses.sql +0 -115
  53. package/migrations/018_global_search.sql +0 -482
  54. package/migrations/019_tenant_members_person_id.sql +0 -20
  55. package/migrations/020_fayz_projects.sql +0 -63
  56. package/migrations/021_tenant_roles.sql +0 -74
  57. package/migrations/022_invite_acceptance.sql +0 -91
  58. package/migrations/023_team_visibility.sql +0 -75
  59. package/migrations/024_analytics_engine.sql +0 -317
  60. package/migrations/025_created_by.sql +0 -75
  61. package/migrations/026_audit_trail.sql +0 -73
  62. package/migrations/027_domain_events.sql +0 -266
  63. package/migrations/028_tenant_scoped_token.sql +0 -127
  64. package/migrations/029_connections.sql +0 -186
  65. package/migrations/030_effect_idempotency.sql +0 -159
  66. package/migrations/031_sync_run_message.sql +0 -39
  67. package/migrations/032_connection_secrets.sql +0 -227
  68. package/migrations/033_sync_schedule.sql +0 -651
  69. package/migrations/034_custom_fields.sql +0 -55
  70. package/migrations/035_field_registry.sql +0 -148
  71. package/migrations/036_analytics_run_batch.sql +0 -84
  72. package/migrations/037_sync_tick_one_at_a_time.sql +0 -256
  73. package/migrations/038_onboarding_responses.sql +0 -103
  74. package/migrations/039_unit_tree.sql +0 -270
  75. package/migrations/040_resource_grants.sql +0 -474
  76. package/migrations/041_scoped_columns.sql +0 -192
  77. package/migrations/042_unit_scope_policies.sql +0 -145
  78. package/migrations/043_view_invoker.sql +0 -90
  79. package/migrations/044_unit_member_facts.sql +0 -47
  80. package/migrations/045_unit_entry.sql +0 -236
  81. package/migrations/046_membership_visible_to_members.sql +0 -85
  82. package/migrations/047_tasks.sql +0 -266
  83. package/migrations/048_every_login_is_a_person.sql +0 -190
  84. package/migrations/049_bookable_people.sql +0 -126
  85. package/migrations/050_authz_core.sql +0 -230
  86. package/migrations/051_authz_helpers.sql +0 -330
  87. package/migrations/052_authz_fanout.sql +0 -253
  88. package/migrations/053_authz_catalog_templates.sql +0 -402
  89. package/migrations/060_tenancy_seam.sql +0 -132
  90. package/migrations/061_create_tenant.sql +0 -125
  91. package/migrations/062_invitations.sql +0 -329
  92. package/migrations/063_legacy_tenancy_retired.sql +0 -568
  93. package/migrations/064_agent_guard_authz.sql +0 -118
  94. package/migrations/064b_supersede_unit_scope.sql +0 -205
  95. package/migrations/065_scaffold_sweep.sql +0 -139
  96. package/migrations/066_shadow_report.sql +0 -122
  97. package/migrations/070_permission_catalog.sql +0 -205
  98. package/migrations/071_v1_permission_map.sql +0 -266
  99. package/migrations/072_role_rpcs.sql +0 -126
  100. package/migrations/073_legacy_rbac_retire.sql +0 -471
  101. package/migrations/075_company_legal_entities.sql +0 -169
  102. package/migrations/076_tenant_settings.sql +0 -251
  103. package/migrations/080_people_roles.sql +0 -269
  104. package/migrations/081_people_identity.sql +0 -271
  105. package/migrations/082_people_membership_link.sql +0 -142
  106. package/migrations/083_people_data_migration.sql +0 -75
  107. package/migrations/084_people_registrations_tokens.sql +0 -282
  108. package/migrations/085_catalog_products.sql +0 -333
  109. package/migrations/086_catalog_pricing.sql +0 -194
  110. package/migrations/087_catalog_packages.sql +0 -134
  111. package/migrations/090_orders_venda.sql +0 -166
  112. package/migrations/091_orders_scaffold.sql +0 -92
  113. package/migrations/095_config_field_rules.sql +0 -261
  114. package/migrations/096_audit_append_only.sql +0 -256
  115. package/migrations/097_sequences_authorized.sql +0 -111
  116. package/migrations/098_holidays.sql +0 -80
  117. package/migrations/099_access_grants.sql +0 -293
  118. package/migrations/100_migration_schema.sql +0 -217
  119. package/migrations/101_migration_allowlist.sql +0 -202
  120. package/migrations/102_migration_rpcs.sql +0 -933
  121. package/migrations/103_migration_reconcile_ponr.sql +0 -132
  122. package/migrations/105_policies_to_authenticated.sql +0 -71
  123. package/migrations/106_function_search_path.sql +0 -55
  124. package/migrations/107_core_table_prefix.sql +0 -361
  125. package/migrations/108_authz_write_guards.sql +0 -149
  126. package/migrations/109_catalog_migration_promoted_columns.sql +0 -126
  127. package/migrations/110_people_kind_vocabulary.sql +0 -120
  128. package/migrations/111_migration_excluded_is_a_decision.sql +0 -259
  129. package/migrations/111_replacing_a_view_keeps_its_dependents.sql +0 -132
  130. package/migrations/112_money_precision_standard.sql +0 -132
  131. package/migrations/113_addresses_units_companies.sql +0 -114
  132. package/migrations/114_migrations_ledger_immutable.sql +0 -65
  133. package/migrations/115_registration_honours_field_rules.sql +0 -172
  134. package/migrations/116_field_rules_check_needs_a_tenant.sql +0 -72
  135. package/migrations/117_authz_reconciliation_schedule.sql +0 -82
  136. package/migrations/118_migration_catalog_writers.sql +0 -450
  137. package/migrations/119_migration_config_writers.sql +0 -193
  138. package/migrations/120_migration_facet_writer.sql +0 -139
  139. package/migrations/121_migration_financial_writers.sql +0 -294
  140. package/migrations/122_migration_inventory_writers.sql +0 -245
  141. package/migrations/123_migration_order_item_writer.sql +0 -144
  142. package/migrations/124_migration_ledger_writers.sql +0 -170
  143. package/migrations/125_migration_authz_people_writers.sql +0 -205
  144. package/migrations/126_owner_scoping_is_configuration.sql +0 -247
  145. package/migrations/127_scaffold_audit_hook.sql +0 -98
  146. package/migrations/128_shadow_tables_enforced.sql +0 -93
  147. package/migrations/129_order_kind_terminal_states.sql +0 -57
  148. package/migrations/130_every_login_is_a_person.sql +0 -196
  149. package/migrations/131_migration_invoice_split.sql +0 -127
  150. package/migrations/132_audit_prune_scheduled.sql +0 -28
  151. package/migrations/133_unit_is_a_session_property.sql +0 -155
  152. package/migrations/134_field_rules_on_read.sql +0 -341
  153. package/migrations/135_distribution_polarity.sql +0 -158
  154. package/migrations/136_unit_id_means_locality.sql +0 -358
  155. package/migrations/137_vendor_named_spine_objects.sql +0 -126
  156. package/migrations/138_grants_inherit_down_the_tree.sql +0 -178
  157. package/migrations/139_distributable_is_a_scaffold_option.sql +0 -109
  158. package/migrations/140_masking_is_a_scaffold_option.sql +0 -85
  159. package/migrations/141_renamed_objects_keep_a_compat_name.sql +0 -192
  160. package/migrations/142_a_person_has_addresses.sql +0 -212
  161. package/migrations/143_a_professional_has_a_rate.sql +0 -75
  162. package/migrations/144_a_product_can_be_sold_in_sizes.sql +0 -57
  163. package/migrations/145_the_app_says_which_tenant_it_is_in.sql +0 -123
  164. package/migrations/146_products_stock_has_one_writer.sql +0 -67
  165. package/migrations/147_every_archetype_has_a_kind_registry.sql +0 -388
  166. package/migrations/148_the_legacy_archetype_stops_being_writable.sql +0 -154
  167. package/migrations/149_the_event_log_gets_a_consumer.sql +0 -331
  168. package/migrations/150_the_app_is_configured_in_the_database.sql +0 -285
  169. package/migrations/151_stock_balance_is_an_archetype.sql +0 -144
  170. package/migrations/152_a_file_is_an_archetype.sql +0 -222
  171. package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +0 -199
  172. package/migrations/154_the_plan_is_a_ceiling.sql +0 -258
  173. package/migrations/155_a_tenant_can_leave.sql +0 -271
  174. package/migrations/156_the_service_door_has_a_server_side.sql +0 -335
  175. package/migrations/157_nobody_creates_objects_in_public.sql +0 -74
  176. package/migrations/158_the_registry_owns_the_status_vocabulary.sql +0 -153
  177. package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +0 -132
  178. package/migrations/160_the_vertical_decides_what_starts_on.sql +0 -246
  179. package/migrations/161_a_tenant_leaves_with_its_data.sql +0 -363
  180. package/migrations/162_every_name_has_one_creator_for_real.sql +0 -126
  181. package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +0 -184
  182. package/migrations/164_the_sequence_table_joins_the_register.sql +0 -26
  183. package/migrations/165_a_view_is_not_a_way_in.sql +0 -90
  184. package/migrations/166_the_platform_billing_leftovers_go.sql +0 -72
  185. package/migrations/167_a_view_gets_back_what_its_table_grants.sql +0 -112
  186. package/migrations/168_the_grant_census_stops_crying_wolf.sql +0 -33
  187. package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +0 -45
@@ -1,193 +0,0 @@
1
- -- 119_migration_config_writers.sql
2
- --
3
- -- FieldRules and holidays get their writers; the QR rows get a decision. #144 / #148.
4
- --
5
- -- 091 pointed both at a `config.*` schema that never shipped — the tables are in
6
- -- `public` — and gave neither a writer, so every rule and every holiday landed
7
- -- `pending`.
8
- --
9
- -- THE THREE qr_access_* ROWS ARE DEFERRED, and the reason is worth stating
10
- -- because the allowlist's own note promised something that cannot be delivered:
11
- -- "V1 issued codes become access_grants on the 'qr' channel with a freshly
12
- -- minted secret". Two problems with that.
13
- --
14
- -- 1. `public.access_grants` requires a unit_id, a role_id and an expiry. V1's
15
- -- `qr_access_configs` carries `enabled`, `hours_valid` and
16
- -- `scan_window_minutes` — tenant-level and opaque. The unit and the role a
17
- -- V2 grant is FOR are not in the source, so a migrated row would have to
18
- -- invent them.
19
- -- 2. A freshly minted secret invalidates every QR code already printed and
20
- -- stuck to a wall. If the codes stop working either way, importing them
21
- -- creates rows nobody can use rather than preserving access.
22
- --
23
- -- So a tenant re-issues its access grants in V2, which takes a minute per unit
24
- -- and is honest about what changed. Sessions were never going to migrate — they
25
- -- expire — and the allowlist already said so.
26
- --
27
- -- V1 `visibility` -> V2 `state`: `required_field_rules` is a list of fields that
28
- -- ARE required, so its writer_args pin state='required'; `field_visibility_rules`
29
- -- carries the vocabulary and is translated, with anything outside it quarantined
30
- -- rather than defaulted (#148).
31
- --
32
- -- Replay-safe: `_allow` upserts under the documented unlock, writers are
33
- -- CREATE OR REPLACE.
34
-
35
- ALTER TABLE migration.allowlist DROP CONSTRAINT IF EXISTS allowlist_writer_check;
36
- ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check
37
- CHECK (writer IS NULL OR writer IN ('person', 'unit', 'category', 'product', 'order', 'invoice',
38
- 'price_table', 'price_table_item', 'package', 'package_item',
39
- 'field_rule', 'holiday'));
40
-
41
- DO $unlock$
42
- BEGIN
43
- PERFORM set_config('migration.allowlist_unlock', 'on', true);
44
-
45
- PERFORM migration._allow('required_field_rules', 'public.field_rules', 'config', 'field_rule', '{"state":"required"}', '{}',
46
- '{"uuid":"id","context":"context","entity":"reference","field_key":"field_key","unit_id":"unit_id@companies"}',
47
- 'PRD 07 / #144: a row here IS a required field, so the state is the writer''s argument rather than a column (#109).');
48
-
49
- PERFORM migration._allow('field_visibility_rules', 'public.field_rules', 'config', 'field_rule', '{}', '{}',
50
- '{"uuid":"id","context":"context","entity":"reference","field_key":"field_key","visibility":"state","unit_id":"unit_id@companies"}',
51
- 'PRD 07 / #144: folded into FieldRules; `visibility` is translated to the 3-state vocabulary and quarantined when it is outside it (#109).');
52
-
53
- PERFORM migration._allow('holidays', 'public.holidays', 'config', 'holiday', '{}', '{}',
54
- '{"uuid":"id","date":"date","name":"name","recurring":"recurring","unit_id":"unit_id@companies","units":"jsonb:units"}',
55
- 'PRD 07: tenant/unit holidays.');
56
-
57
- PERFORM migration._allow('central_holidays', 'public.holidays', 'config', 'holiday', '{"kind":"national"}', '{}',
58
- '{"uuid":"id","date":"date","name":"name"}',
59
- 'National calendar; platform-level rows land as tenant-wide holidays of kind national (#109).');
60
-
61
- DELETE FROM migration.allowlist WHERE source_table IN ('qr_access_configs', 'qr_access_grants', 'qr_access_sessions');
62
- END $unlock$;
63
-
64
- INSERT INTO migration.excluded_tables (source_table, reason) VALUES
65
- ('qr_access_configs', 'Deferred: public.access_grants needs a unit, a role and an expiry; V1 qr_access_configs is tenant-level and opaque (enabled / hours_valid / scan_window_minutes), so a migrated grant would have to invent the unit and role it is FOR. A tenant re-issues its access grants in V2 (#109).'),
66
- ('qr_access_grants', 'Deferred: issued codes are credentials. Migrating them means minting a new secret, which invalidates every QR already printed — so the row would be unusable either way, and importing it creates dead rows rather than preserving access (#109).'),
67
- ('qr_access_sessions', 'Not migrated: live sessions expire. 091 already said so; this makes it a recorded decision instead of a pending row (#109).')
68
- ON CONFLICT (source_table) DO UPDATE SET reason = EXCLUDED.reason;
69
-
70
- -- ── field rules ─────────────────────────────────────────────────────────
71
- CREATE OR REPLACE FUNCTION migration.upsert_field_rule(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
72
- RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
73
- DECLARE
74
- v_state_raw text := coalesce(p_args ->> 'state', nullif(btrim(lower(coalesce(p_row ->> 'state', ''))), ''));
75
- v_state text := CASE v_state_raw
76
- WHEN 'required' THEN 'required'
77
- WHEN 'visible' THEN 'visible'
78
- WHEN 'hidden' THEN 'hidden'
79
- -- the V1 vocabulary, translated
80
- WHEN 'optional' THEN 'visible'
81
- WHEN 'shown' THEN 'visible'
82
- WHEN 'visivel' THEN 'visible'
83
- WHEN 'oculto' THEN 'hidden'
84
- WHEN 'obrigatorio' THEN 'required'
85
- END;
86
- v_context text := nullif(btrim(coalesce(p_row ->> 'context', '')), '');
87
- v_ref text := lower(nullif(btrim(coalesce(p_row ->> 'reference', '')), ''));
88
- v_key text := nullif(btrim(coalesce(p_row ->> 'field_key', '')), '');
89
- v_existing_tenant uuid;
90
- v_created boolean := false;
91
- v_standalone boolean := migration._standalone();
92
- BEGIN
93
- IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
94
- IF p_id IS NULL OR v_context IS NULL OR v_ref IS NULL OR v_key IS NULL THEN
95
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a field rule needs an id, a context, a reference and a field key');
96
- END IF;
97
- IF v_state IS NULL THEN
98
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
99
- 'reason', format('unknown visibility %L (expected hidden|visible|required)', coalesce(v_state_raw, '<null>')));
100
- END IF;
101
- -- the context is an FK: an unknown one is a broken reference, not a new context
102
- IF NOT EXISTS (SELECT 1 FROM public.field_rule_contexts c WHERE c.key = v_context) THEN
103
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
104
- 'reason', format('context %L is not registered in public.field_rule_contexts', v_context));
105
- END IF;
106
- -- One rule per (tenant, unit, context, reference, field_key, role). A V1 row
107
- -- with a different id for a scope that already has a rule is a genuine
108
- -- conflict — two sources disagreeing about the same field — so it quarantines
109
- -- with that sentence rather than leaking `duplicate key value violates unique
110
- -- constraint "field_rules_one_per_scope"` through the generic handler.
111
- IF EXISTS (SELECT 1 FROM public.field_rules r
112
- WHERE r.tenant_id = p_tenant AND r.id <> p_id
113
- AND r.unit_id IS NOT DISTINCT FROM (p_row ->> 'unit_id')::uuid
114
- AND r.context = v_context AND r.reference = v_ref AND r.field_key = v_key
115
- AND r.role_key IS NOT DISTINCT FROM nullif(btrim(coalesce(p_row ->> 'role_key', '')), '')) THEN
116
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
117
- 'reason', format('a field rule already exists for %s/%s/%s in this scope — two sources disagree about the same field', v_context, v_ref, v_key));
118
- END IF;
119
- SELECT tenant_id INTO v_existing_tenant FROM public.field_rules WHERE id = p_id;
120
- IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN
121
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('field rule %s already exists in another tenant', p_id));
122
- END IF;
123
- IF v_existing_tenant IS NULL THEN
124
- INSERT INTO public.field_rules (id, tenant_id, unit_id, context, reference, field_key, state, sort_order)
125
- VALUES (p_id, p_tenant, (p_row ->> 'unit_id')::uuid, v_context, v_ref, v_key, v_state,
126
- coalesce((p_row ->> 'sort_order')::integer, 0));
127
- v_created := true;
128
- ELSE
129
- UPDATE public.field_rules SET
130
- unit_id = CASE WHEN p_row ? 'unit_id' THEN (p_row ->> 'unit_id')::uuid ELSE unit_id END,
131
- context = v_context, reference = v_ref, field_key = v_key, state = v_state
132
- WHERE id = p_id;
133
- END IF;
134
- IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.field_rules', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
135
- RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
136
- END $$;
137
-
138
- -- ── holidays ────────────────────────────────────────────────────────────
139
- CREATE OR REPLACE FUNCTION migration.upsert_holiday(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
140
- RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
141
- DECLARE
142
- v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
143
- v_kind text := coalesce(p_args ->> 'kind', 'company');
144
- v_date date;
145
- v_name text := nullif(btrim(coalesce(p_row ->> 'name', '')), '');
146
- v_existing_tenant uuid;
147
- v_created boolean := false;
148
- v_standalone boolean := migration._standalone();
149
- BEGIN
150
- IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
151
- BEGIN
152
- v_date := (p_row ->> 'date')::date;
153
- EXCEPTION WHEN OTHERS THEN
154
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('%L is not a date', p_row ->> 'date'));
155
- END;
156
- IF p_id IS NULL OR v_date IS NULL OR v_name IS NULL THEN
157
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a holiday needs an id, a date and a name');
158
- END IF;
159
- IF v_kind NOT IN ('national', 'state', 'city', 'company') THEN
160
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown holiday kind %L', v_kind));
161
- END IF;
162
- SELECT tenant_id INTO v_existing_tenant FROM public.holidays WHERE id = p_id;
163
- IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN
164
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('holiday %s already exists in another tenant', p_id));
165
- END IF;
166
- IF v_existing_tenant IS NULL THEN
167
- INSERT INTO public.holidays (id, tenant_id, unit_id, date, name, kind, recurring, description, is_active, metadata)
168
- VALUES (p_id, p_tenant, (p_row ->> 'unit_id')::uuid, v_date, v_name, v_kind,
169
- coalesce((p_row ->> 'recurring')::boolean, false), p_row ->> 'description',
170
- coalesce((p_row ->> 'is_active')::boolean, true), v_meta);
171
- v_created := true;
172
- ELSE
173
- UPDATE public.holidays SET
174
- unit_id = CASE WHEN p_row ? 'unit_id' THEN (p_row ->> 'unit_id')::uuid ELSE unit_id END,
175
- date = v_date, name = v_name, kind = v_kind,
176
- recurring = CASE WHEN p_row ? 'recurring' THEN coalesce((p_row ->> 'recurring')::boolean, false) ELSE recurring END,
177
- description = CASE WHEN p_row ? 'description' THEN p_row ->> 'description' ELSE description END,
178
- metadata = migration._merge_meta(metadata, v_meta)
179
- WHERE id = p_id;
180
- END IF;
181
- IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.holidays', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
182
- RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
183
- END $$;
184
-
185
- DO $$
186
- DECLARE r record;
187
- BEGIN
188
- FOR r IN SELECT p.oid::regprocedure AS sig FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
189
- WHERE n.nspname = 'migration' AND p.proname IN ('upsert_field_rule', 'upsert_holiday') LOOP
190
- EXECUTE format('REVOKE ALL ON FUNCTION %s FROM public, anon, authenticated', r.sig);
191
- EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', r.sig);
192
- END LOOP;
193
- END $$;
@@ -1,139 +0,0 @@
1
- -- 120_migration_facet_writer.sql
2
- --
3
- -- One writer for every facet, instead of twenty that each create nothing. #148.
4
- --
5
- -- A large share of the allowlist is not "a table that becomes a table" — it is a
6
- -- FACET: a V1 side-table that enriches a row which already migrated. Barcodes,
7
- -- sale info, fiscal info, sale channels, asset and ingredient info all hang off
8
- -- `products`; general_settings, modules, software profiles and domains all hang
9
- -- off the tenant. `_upsert_row_impl` had no shape for that, so all of them landed
10
- -- `pending` and each would otherwise need a near-identical writer whose only real
11
- -- job is `metadata = metadata || <mapped jsonb>`.
12
- --
13
- -- `upsert_facet` is that shape, parameterised from the allowlist:
14
- --
15
- -- {"target":"public.products"} -- parent id comes from the
16
- -- mapped `id` (an @ref the
17
- -- ledger resolved)
18
- -- {"target":"public.tenants","parent":"tenant", -- parent IS the batch's
19
- -- "column":"settings"} tenant; merge into
20
- -- `settings`, not metadata
21
- --
22
- -- It refuses rather than creates. A facet whose parent is not in the destination
23
- -- is an fk quarantine, not a new row: enriching something that does not exist
24
- -- would mean inventing the thing being enriched, and the parent's own table is
25
- -- what owns its creation. That also makes offer ORDER not matter — a facet
26
- -- offered before its parent quarantines, and the next offer of the same row
27
- -- (after the parent lands) migrates it.
28
- --
29
- -- TWO REFERENCE LISTS ARE DEFERRED. `origins` and `partnerships` are V1 lookup
30
- -- tables that people POINT AT; 091 mapped them onto `public.people` with a
31
- -- `jsonb:customer.origin` path and no person reference at all, which cannot be
32
- -- executed — there is no row to enrich. The values reach V2 inside the person
33
- -- that references them, which `upsert_person` already carries.
34
- --
35
- -- Replay-safe: `_allow` upserts under the documented unlock, writer is
36
- -- CREATE OR REPLACE, and the merge is idempotent by construction.
37
-
38
- ALTER TABLE migration.allowlist DROP CONSTRAINT IF EXISTS allowlist_writer_check;
39
- ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check
40
- CHECK (writer IS NULL OR writer IN ('person', 'unit', 'category', 'product', 'order', 'invoice',
41
- 'price_table', 'price_table_item', 'package', 'package_item',
42
- 'field_rule', 'holiday', 'facet'));
43
-
44
- CREATE OR REPLACE FUNCTION migration.upsert_facet(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
45
- RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
46
- DECLARE
47
- v_target text := p_args ->> 'target';
48
- v_column text := coalesce(p_args ->> 'column', 'metadata');
49
- v_parent uuid;
50
- v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
51
- v_found boolean;
52
- v_standalone boolean := migration._standalone();
53
- BEGIN
54
- IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
55
- IF v_target IS NULL OR to_regclass(v_target) IS NULL THEN
56
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
57
- 'reason', format('the facet writer needs a target table that exists; got %L', coalesce(v_target, '<null>')));
58
- END IF;
59
- -- Nothing mapped: the facet carries no column this contract keeps. Every
60
- -- migrated row is stamped with its origin (`metadata.migration`), so the test
61
- -- is "anything BESIDES the marker" — otherwise a facet that maps nothing would
62
- -- report a write that changed nothing but the provenance of its parent.
63
- IF (v_meta - 'migration') = '{}'::jsonb THEN
64
- RETURN jsonb_build_object('status', 'skipped', 'reason', 'the facet maps no column into the destination');
65
- END IF;
66
-
67
- IF coalesce(p_args ->> 'parent', 'row') = 'tenant' THEN
68
- v_parent := p_tenant;
69
- ELSE
70
- BEGIN
71
- v_parent := (p_row ->> 'id')::uuid;
72
- EXCEPTION WHEN OTHERS THEN v_parent := NULL; END;
73
- IF v_parent IS NULL THEN
74
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
75
- 'reason', format('a facet of %s needs its parent''s id; the mapped reference did not resolve', v_target));
76
- END IF;
77
- END IF;
78
-
79
- -- public.tenants has no tenant_id column — its own id IS the tenant — so the
80
- -- scope predicate differs by parent kind. Every other target is a tenant-scoped
81
- -- business table and must be checked in scope, or a facet could enrich a row of
82
- -- another tenant that happens to share an id.
83
- IF coalesce(p_args ->> 'parent', 'row') = 'tenant' THEN
84
- EXECUTE format('SELECT EXISTS (SELECT 1 FROM %s WHERE id = $1)', v_target) INTO v_found USING v_parent;
85
- ELSE
86
- EXECUTE format('SELECT EXISTS (SELECT 1 FROM %s WHERE id = $1 AND tenant_id = $2)', v_target)
87
- INTO v_found USING v_parent, p_tenant;
88
- END IF;
89
- IF NOT v_found THEN
90
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',
91
- 'reason', format('%s %s is not in this tenant yet — a facet enriches a row, it does not create one', v_target, v_parent));
92
- END IF;
93
-
94
- IF coalesce(p_args ->> 'parent', 'row') = 'tenant' THEN
95
- EXECUTE format('UPDATE %s SET %I = migration._merge_meta(%I, $1), updated_at = now() WHERE id = $2', v_target, v_column, v_column)
96
- USING v_meta, v_parent;
97
- ELSE
98
- EXECUTE format('UPDATE %s SET %I = migration._merge_meta(%I, $1), updated_at = now() WHERE id = $2 AND tenant_id = $3',
99
- v_target, v_column, v_column)
100
- USING v_meta, v_parent, p_tenant;
101
- END IF;
102
-
103
- IF v_standalone THEN PERFORM migration._audit(p_tenant, v_target, v_parent, jsonb_build_object('reason', 'facet merged')); END IF;
104
- RETURN jsonb_build_object('status', 'migrated', 'target_id', v_parent, 'reason', 'facet merged');
105
- END $$;
106
- REVOKE ALL ON FUNCTION migration.upsert_facet(uuid, uuid, jsonb, jsonb) FROM public, anon, authenticated;
107
- GRANT EXECUTE ON FUNCTION migration.upsert_facet(uuid, uuid, jsonb, jsonb) TO service_role;
108
-
109
- DO $unlock$
110
- DECLARE r record;
111
- BEGIN
112
- PERFORM set_config('migration.allowlist_unlock', 'on', true);
113
-
114
- -- facets of a row: the parent's id is the mapped @ref
115
- FOR r IN SELECT * FROM (VALUES
116
- ('product_barcodes', 'public.products'),
117
- ('product_sale_info', 'public.products'),
118
- ('product_sale_channels', 'public.products'),
119
- ('product_fiscal_info', 'public.products'),
120
- ('product_asset_info', 'public.products'),
121
- ('product_ingredient_info', 'public.products')
122
- ) AS t(src, tgt)
123
- LOOP
124
- UPDATE migration.allowlist SET writer = 'facet', writer_args = jsonb_build_object('target', r.tgt)
125
- WHERE source_table = r.src;
126
- END LOOP;
127
-
128
- -- facets of the tenant: general_settings lands in `settings` (it IS the
129
- -- settings registry #138 reads), the rest describe the tenant and land there too
130
- UPDATE migration.allowlist SET writer = 'facet', writer_args = jsonb_build_object('target', 'public.tenants', 'parent', 'tenant', 'column', 'settings')
131
- WHERE source_table IN ('general_settings', 'modules', 'software_profiles', 'software_profile_modules', 'tenant_domains', 'licenses_admin');
132
-
133
- DELETE FROM migration.allowlist WHERE source_table IN ('origins', 'partnerships');
134
- END $unlock$;
135
-
136
- INSERT INTO migration.excluded_tables (source_table, reason) VALUES
137
- ('origins', 'Deferred: a V1 lookup table that clients POINT AT. 091 mapped it onto public.people with a jsonb:customer.origin path and no person reference, which cannot be executed — there is no row to enrich. The value reaches V2 inside the person that references it (#110).'),
138
- ('partnerships', 'Deferred: same shape as origins — a reference list, not a row of public.people (#110).')
139
- ON CONFLICT (source_table) DO UPDATE SET reason = EXCLUDED.reason;
@@ -1,294 +0,0 @@
1
- -- 121_migration_financial_writers.sql
2
- --
3
- -- The financial reference tables get their writers. #142 / #148.
4
- --
5
- -- Every financial row in the allowlist had `writer = NULL`, so accounts, payment
6
- -- methods, card brands, the chart of accounts and cost centres all landed
7
- -- `pending`. These are the reference data a tenant's money hangs off — without
8
- -- them no movement can resolve its account, so they come before the ledger
9
- -- itself (112).
10
- --
11
- -- FOUR MAPPINGS NAMED COLUMNS THAT DO NOT EXIST, the same silent-drop shape as
12
- -- #198 and the `counterparty_id` bug. Corrected here rather than left for a
13
- -- writer to ignore:
14
- --
15
- -- chart_of_accounts.account_type -> node_type (there is no `kind` column)
16
- -- cost_centers.description -> dropped (no column; V1's free text has
17
- -- no home and inventing one for
18
- -- a cost centre's description is
19
- -- not worth a JSONB bag)
20
- -- account_central/bank_accounts.account_type -> kind, and VALIDATED: the
21
- -- ledger's eight kinds are a closed vocabulary (bank, cash_physical,
22
- -- cash_session, card_receivable_credit/_debit, card_payable, person, system),
23
- -- and a V1 account_type outside it quarantines rather than defaulting to
24
- -- 'bank' — an account posted to the wrong kind puts money in the wrong place
25
- -- and nothing downstream would notice.
26
- --
27
- -- `cash_session_reviewers` becomes a facet of the session (110's writer), which
28
- -- is what 091 already called it.
29
- --
30
- -- Replay-safe: `_allow` upserts under the documented unlock; writers are
31
- -- CREATE OR REPLACE.
32
-
33
- ALTER TABLE migration.allowlist DROP CONSTRAINT IF EXISTS allowlist_writer_check;
34
- ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check
35
- CHECK (writer IS NULL OR writer IN ('person', 'unit', 'category', 'product', 'order', 'invoice',
36
- 'price_table', 'price_table_item', 'package', 'package_item',
37
- 'field_rule', 'holiday', 'facet',
38
- 'fin_account', 'fin_payment_method_type', 'fin_payment_method',
39
- 'fin_card_brand', 'fin_chart_account', 'fin_cost_center'));
40
-
41
- DO $unlock$
42
- BEGIN
43
- PERFORM set_config('migration.allowlist_unlock', 'on', true);
44
-
45
- UPDATE migration.allowlist SET writer = 'fin_account' WHERE source_table IN ('account_central', 'bank_accounts');
46
- UPDATE migration.allowlist SET writer = 'fin_payment_method_type' WHERE source_table = 'payment_method_types';
47
- UPDATE migration.allowlist SET writer = 'fin_payment_method' WHERE source_table = 'payment_methods';
48
- UPDATE migration.allowlist SET writer = 'fin_card_brand' WHERE source_table = 'card_brands';
49
-
50
- -- account_type is node_type here; there is no `kind` column on the chart
51
- UPDATE migration.allowlist SET writer = 'fin_chart_account',
52
- columns = '{"uuid":"id","code":"code","name":"name","account_type":"node_type","parent_id":"parent_id@chart_of_accounts","active":"is_active"}'::jsonb,
53
- notes = 'PRD 05. V1 account_type is the node_type of the chart, not a `kind` — the column named in 091 does not exist (#111).'
54
- WHERE source_table = 'chart_of_accounts';
55
-
56
- -- a cost centre has code, name and is_active in V2; the V1 description has no home
57
- UPDATE migration.allowlist SET writer = 'fin_cost_center',
58
- columns = '{"uuid":"id","code":"code","name":"name","description":"-","active":"is_active"}'::jsonb,
59
- notes = 'PRD 05. V1 `description` is explicitly not migrated: public.plg_financial_cost_centers has no column for it and a JSONB bag for one free-text field is not worth the drift (#111).'
60
- WHERE source_table = 'cost_centers';
61
-
62
- UPDATE migration.allowlist SET writer = 'facet',
63
- writer_args = jsonb_build_object('target', 'public.plg_financial_cash_register_sessions')
64
- WHERE source_table = 'cash_session_reviewers';
65
- END $unlock$;
66
-
67
- CREATE OR REPLACE FUNCTION migration.upsert_fin_account(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
68
- RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
69
- DECLARE
70
- v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
71
- v_existing_tenant uuid;
72
- v_created boolean := false;
73
- v_standalone boolean := migration._standalone();
74
- BEGIN
75
- IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
76
- IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN
77
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');
78
- END IF;
79
- IF coalesce(p_row ->> 'kind', 'bank') NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',
80
- 'card_receivable_debit', 'card_payable', 'person', 'system') THEN
81
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
82
- 'reason', format('unknown account kind %L — V1 account_type does not map onto the ledger''s eight', p_row ->> 'kind'));
83
- END IF;
84
- SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;
85
- IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN
86
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));
87
- END IF;
88
- IF v_existing_tenant IS NULL THEN
89
- INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, metadata)
90
- VALUES (p_id, p_tenant,
91
- (p_row ->> 'unit_id')::uuid,
92
- coalesce(p_row ->> 'kind', 'bank'),
93
- p_row ->> 'code',
94
- p_row ->> 'name',
95
- p_row ->> 'bank_name',
96
- (p_row ->> 'settlement_days')::integer,
97
- coalesce((p_row ->> 'is_active')::boolean, true),
98
- v_meta);
99
- v_created := true;
100
- ELSE
101
- UPDATE public.plg_financial_accounts SET
102
- unit_id = CASE WHEN p_row ? 'unit_id' THEN (p_row ->> 'unit_id')::uuid ELSE unit_id END,
103
- kind = CASE WHEN p_row ? 'kind' THEN coalesce(p_row ->> 'kind', 'bank') ELSE kind END,
104
- code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,
105
- name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,
106
- bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,
107
- settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,
108
- is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,
109
- metadata = migration._merge_meta(metadata, v_meta)
110
- WHERE id = p_id;
111
- END IF;
112
- IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
113
- RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
114
- END $$;
115
-
116
- CREATE OR REPLACE FUNCTION migration.upsert_fin_payment_method_type(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
117
- RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
118
- DECLARE
119
- v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
120
- v_existing_tenant uuid;
121
- v_created boolean := false;
122
- v_standalone boolean := migration._standalone();
123
- BEGIN
124
- IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
125
- IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN
126
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');
127
- END IF;
128
- SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_payment_method_types WHERE id = p_id;
129
- IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN
130
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));
131
- END IF;
132
- IF v_existing_tenant IS NULL THEN
133
- INSERT INTO public.plg_financial_payment_method_types (id, tenant_id, name, is_active)
134
- VALUES (p_id, p_tenant,
135
- p_row ->> 'name',
136
- coalesce((p_row ->> 'is_active')::boolean, true));
137
- v_created := true;
138
- ELSE
139
- UPDATE public.plg_financial_payment_method_types SET
140
- name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,
141
- is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END
142
- WHERE id = p_id;
143
- END IF;
144
- IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_payment_method_types', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
145
- RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
146
- END $$;
147
-
148
- CREATE OR REPLACE FUNCTION migration.upsert_fin_payment_method(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
149
- RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
150
- DECLARE
151
- v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
152
- v_existing_tenant uuid;
153
- v_created boolean := false;
154
- v_standalone boolean := migration._standalone();
155
- BEGIN
156
- IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
157
- IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN
158
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');
159
- END IF;
160
- SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_payment_methods WHERE id = p_id;
161
- IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN
162
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));
163
- END IF;
164
- IF v_existing_tenant IS NULL THEN
165
- INSERT INTO public.plg_financial_payment_methods (id, tenant_id, name, payment_method_type_id, is_active)
166
- VALUES (p_id, p_tenant,
167
- p_row ->> 'name',
168
- (p_row ->> 'type_id')::uuid,
169
- coalesce((p_row ->> 'is_active')::boolean, true));
170
- v_created := true;
171
- ELSE
172
- UPDATE public.plg_financial_payment_methods SET
173
- name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,
174
- payment_method_type_id = CASE WHEN p_row ? 'payment_method_type_id' THEN (p_row ->> 'type_id')::uuid ELSE payment_method_type_id END,
175
- is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END
176
- WHERE id = p_id;
177
- END IF;
178
- IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_payment_methods', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
179
- RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
180
- END $$;
181
-
182
- CREATE OR REPLACE FUNCTION migration.upsert_fin_card_brand(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
183
- RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
184
- DECLARE
185
- v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
186
- v_existing_tenant uuid;
187
- v_created boolean := false;
188
- v_standalone boolean := migration._standalone();
189
- BEGIN
190
- IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
191
- IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN
192
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');
193
- END IF;
194
- SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_card_brands WHERE id = p_id;
195
- IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN
196
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));
197
- END IF;
198
- IF v_existing_tenant IS NULL THEN
199
- INSERT INTO public.plg_financial_card_brands (id, tenant_id, name, is_active)
200
- VALUES (p_id, p_tenant,
201
- p_row ->> 'name',
202
- coalesce((p_row ->> 'is_active')::boolean, true));
203
- v_created := true;
204
- ELSE
205
- UPDATE public.plg_financial_card_brands SET
206
- name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,
207
- is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END
208
- WHERE id = p_id;
209
- END IF;
210
- IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_card_brands', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
211
- RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
212
- END $$;
213
-
214
- CREATE OR REPLACE FUNCTION migration.upsert_fin_chart_account(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
215
- RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
216
- DECLARE
217
- v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
218
- v_existing_tenant uuid;
219
- v_created boolean := false;
220
- v_standalone boolean := migration._standalone();
221
- BEGIN
222
- IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
223
- IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN
224
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');
225
- END IF;
226
- SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_chart_of_accounts WHERE id = p_id;
227
- IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN
228
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));
229
- END IF;
230
- IF v_existing_tenant IS NULL THEN
231
- INSERT INTO public.plg_financial_chart_of_accounts (id, tenant_id, code, name, node_type, parent_id, is_active)
232
- VALUES (p_id, p_tenant,
233
- p_row ->> 'code',
234
- p_row ->> 'name',
235
- coalesce(p_row ->> 'node_type', 'account'),
236
- (p_row ->> 'parent_id')::uuid,
237
- coalesce((p_row ->> 'is_active')::boolean, true));
238
- v_created := true;
239
- ELSE
240
- UPDATE public.plg_financial_chart_of_accounts SET
241
- code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,
242
- name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,
243
- node_type = CASE WHEN p_row ? 'node_type' THEN coalesce(p_row ->> 'node_type', 'account') ELSE node_type END,
244
- parent_id = CASE WHEN p_row ? 'parent_id' THEN (p_row ->> 'parent_id')::uuid ELSE parent_id END,
245
- is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END
246
- WHERE id = p_id;
247
- END IF;
248
- IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_chart_of_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
249
- RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
250
- END $$;
251
-
252
- CREATE OR REPLACE FUNCTION migration.upsert_fin_cost_center(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
253
- RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
254
- DECLARE
255
- v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
256
- v_existing_tenant uuid;
257
- v_created boolean := false;
258
- v_standalone boolean := migration._standalone();
259
- BEGIN
260
- IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
261
- IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN
262
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');
263
- END IF;
264
- SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_cost_centers WHERE id = p_id;
265
- IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN
266
- RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));
267
- END IF;
268
- IF v_existing_tenant IS NULL THEN
269
- INSERT INTO public.plg_financial_cost_centers (id, tenant_id, code, name, is_active)
270
- VALUES (p_id, p_tenant,
271
- p_row ->> 'code',
272
- p_row ->> 'name',
273
- coalesce((p_row ->> 'is_active')::boolean, true));
274
- v_created := true;
275
- ELSE
276
- UPDATE public.plg_financial_cost_centers SET
277
- code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,
278
- name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,
279
- is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END
280
- WHERE id = p_id;
281
- END IF;
282
- IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_cost_centers', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
283
- RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
284
- END $$;
285
-
286
- DO $$
287
- DECLARE r record;
288
- BEGIN
289
- FOR r IN SELECT p.oid::regprocedure AS sig FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
290
- WHERE n.nspname = 'migration' AND p.proname LIKE 'upsert\_fin\_%' LOOP
291
- EXECUTE format('REVOKE ALL ON FUNCTION %s FROM public, anon, authenticated', r.sig);
292
- EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', r.sig);
293
- END LOOP;
294
- END $$;