@fayz-ai/db 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/canonical-schema.json +106 -23
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/schema/spine.d.ts +42 -37
- package/dist/schema/spine.d.ts.map +1 -1
- package/migrations/000_baseline.sql +29667 -0
- package/migrations/001_the_tenant_has_apps.sql +314 -0
- package/migrations/002_the_spine_drops_the_vendor_and_the_dead.sql +129 -0
- package/migrations/003_the_role_rpcs_finish_and_the_mirrors_go.sql +135 -0
- package/migrations/004_the_projections_get_a_successor.sql +312 -0
- package/migrations/005_the_archetypes_anchor_to_the_unit.sql +90 -0
- package/migrations/006_the_helpers_read_the_source.sql +153 -0
- package/migrations/007_the_compat_machinery_leaves_with_the_tables.sql +77 -0
- package/migrations/008_the_switchers_see_across_accounts.sql +150 -0
- package/migrations/009_the_compat_aliases_nobody_adopted.sql +57 -0
- package/migrations/010_the_new_tenant_is_born_with_its_trade.sql +201 -0
- package/migrations/011_leadcontrol_joins_the_family.sql +120 -0
- package/migrations/012_chefcontrol_wears_its_own_mark.sql +28 -0
- package/migrations/013_the_workspace_switch_reaches_the_other_half.sql +156 -0
- package/migrations/014_the_chooser_can_read_what_it_offers.sql +91 -0
- package/migrations/015_the_unit_bindings_belong_to_one_workspace.sql +86 -0
- package/migrations/016_the_rule_stops_asking_every_row.sql +543 -0
- package/migrations/017_the_pin_is_checked_against_the_right_workspace.sql +139 -0
- package/migrations/018_the_bootstrap_says_what_you_reach.sql +114 -0
- package/migrations/019_distributing_one_row_stops_costing_the_table.sql +245 -0
- package/migrations/020_the_agenda_asks_where_someone_works.sql +81 -0
- package/migrations/022_the_line_says_which_document_it_belongs_to.sql +109 -0
- package/migrations/029_o_administrador_enxerga_o_que_existe.sql +167 -0
- package/migrations/031_ticketcontrol_joins_the_family.sql +109 -0
- package/migrations/032_a_marca_e_um_conjunto_de_tokens.sql +291 -0
- package/migrations/041_a_entrega_tem_onde_guardar_a_politica.sql +53 -0
- package/migrations/042_o_token_de_marca_fecha_a_porta_anon.sql +17 -0
- package/migrations/043_fiscalcontrol_joins_the_family.sql +75 -0
- package/migrations/044_o_teste_gratis_dura_trinta_dias.sql +15 -0
- package/migrations/046_the_tick_advances_the_flows.sql +77 -0
- package/migrations/047_the_tick_sweeps_the_offers.sql +74 -0
- package/migrations/052_fullcontrol_joins_the_family.sql +213 -0
- package/migrations/053_o_erp_para_de_exigir_o_que_nao_desenha.sql +37 -0
- package/package.json +1 -1
- package/migrations/0000_legacy_quarantine.sql +0 -86
- package/migrations/0000b_straggler_sweep.sql +0 -37
- package/migrations/000_core_v1_convert.sql +0 -343
- package/migrations/001_core.sql +0 -372
- package/migrations/002_rls_user_tenant_ids.sql +0 -64
- package/migrations/003_invitations.sql +0 -17
- package/migrations/004_archetypes.sql +0 -343
- package/migrations/005_locations_archetype.sql +0 -18
- package/migrations/006_archetype_rls.sql +0 -74
- package/migrations/007_archetype_grants.sql +0 -4
- package/migrations/008_grants.sql +0 -5
- package/migrations/009_anon_hardening.sql +0 -19
- package/migrations/010_migration_ledger.sql +0 -46
- package/migrations/011_anon_write_revoke.sql +0 -12
- package/migrations/012_rename_migration_ledger.sql +0 -61
- package/migrations/013_sequences_rls.sql +0 -22
- package/migrations/014_plan_entitlements.sql +0 -28
- package/migrations/015_agent_guard.sql +0 -108
- package/migrations/016_agent_guard_actor.sql +0 -102
- package/migrations/017_core_addresses.sql +0 -115
- package/migrations/018_global_search.sql +0 -482
- package/migrations/019_tenant_members_person_id.sql +0 -20
- package/migrations/020_fayz_projects.sql +0 -63
- package/migrations/021_tenant_roles.sql +0 -74
- package/migrations/022_invite_acceptance.sql +0 -91
- package/migrations/023_team_visibility.sql +0 -75
- package/migrations/024_analytics_engine.sql +0 -317
- package/migrations/025_created_by.sql +0 -75
- package/migrations/026_audit_trail.sql +0 -73
- package/migrations/027_domain_events.sql +0 -266
- package/migrations/028_tenant_scoped_token.sql +0 -127
- package/migrations/029_connections.sql +0 -186
- package/migrations/030_effect_idempotency.sql +0 -159
- package/migrations/031_sync_run_message.sql +0 -39
- package/migrations/032_connection_secrets.sql +0 -227
- package/migrations/033_sync_schedule.sql +0 -651
- package/migrations/034_custom_fields.sql +0 -55
- package/migrations/035_field_registry.sql +0 -148
- package/migrations/036_analytics_run_batch.sql +0 -84
- package/migrations/037_sync_tick_one_at_a_time.sql +0 -256
- package/migrations/038_onboarding_responses.sql +0 -103
- package/migrations/039_unit_tree.sql +0 -270
- package/migrations/040_resource_grants.sql +0 -474
- package/migrations/041_scoped_columns.sql +0 -192
- package/migrations/042_unit_scope_policies.sql +0 -145
- package/migrations/043_view_invoker.sql +0 -90
- package/migrations/044_unit_member_facts.sql +0 -47
- package/migrations/045_unit_entry.sql +0 -236
- package/migrations/046_membership_visible_to_members.sql +0 -85
- package/migrations/047_tasks.sql +0 -266
- package/migrations/048_every_login_is_a_person.sql +0 -190
- package/migrations/049_bookable_people.sql +0 -126
- package/migrations/050_authz_core.sql +0 -230
- package/migrations/051_authz_helpers.sql +0 -330
- package/migrations/052_authz_fanout.sql +0 -253
- package/migrations/053_authz_catalog_templates.sql +0 -402
- package/migrations/060_tenancy_seam.sql +0 -132
- package/migrations/061_create_tenant.sql +0 -125
- package/migrations/062_invitations.sql +0 -329
- package/migrations/063_legacy_tenancy_retired.sql +0 -568
- package/migrations/064_agent_guard_authz.sql +0 -118
- package/migrations/064b_supersede_unit_scope.sql +0 -205
- package/migrations/065_scaffold_sweep.sql +0 -139
- package/migrations/066_shadow_report.sql +0 -122
- package/migrations/070_permission_catalog.sql +0 -205
- package/migrations/071_v1_permission_map.sql +0 -266
- package/migrations/072_role_rpcs.sql +0 -126
- package/migrations/073_legacy_rbac_retire.sql +0 -471
- package/migrations/075_company_legal_entities.sql +0 -169
- package/migrations/076_tenant_settings.sql +0 -251
- package/migrations/080_people_roles.sql +0 -269
- package/migrations/081_people_identity.sql +0 -271
- package/migrations/082_people_membership_link.sql +0 -142
- package/migrations/083_people_data_migration.sql +0 -75
- package/migrations/084_people_registrations_tokens.sql +0 -282
- package/migrations/085_catalog_products.sql +0 -333
- package/migrations/086_catalog_pricing.sql +0 -194
- package/migrations/087_catalog_packages.sql +0 -134
- package/migrations/090_orders_venda.sql +0 -166
- package/migrations/091_orders_scaffold.sql +0 -92
- package/migrations/095_config_field_rules.sql +0 -261
- package/migrations/096_audit_append_only.sql +0 -256
- package/migrations/097_sequences_authorized.sql +0 -111
- package/migrations/098_holidays.sql +0 -80
- package/migrations/099_access_grants.sql +0 -293
- package/migrations/100_migration_schema.sql +0 -217
- package/migrations/101_migration_allowlist.sql +0 -202
- package/migrations/102_migration_rpcs.sql +0 -933
- package/migrations/103_migration_reconcile_ponr.sql +0 -132
- package/migrations/105_policies_to_authenticated.sql +0 -71
- package/migrations/106_function_search_path.sql +0 -55
- package/migrations/107_core_table_prefix.sql +0 -361
- package/migrations/108_authz_write_guards.sql +0 -149
- package/migrations/109_catalog_migration_promoted_columns.sql +0 -126
- package/migrations/110_people_kind_vocabulary.sql +0 -120
- package/migrations/111_migration_excluded_is_a_decision.sql +0 -259
- package/migrations/111_replacing_a_view_keeps_its_dependents.sql +0 -132
- package/migrations/112_money_precision_standard.sql +0 -132
- package/migrations/113_addresses_units_companies.sql +0 -114
- package/migrations/114_migrations_ledger_immutable.sql +0 -65
- package/migrations/115_registration_honours_field_rules.sql +0 -172
- package/migrations/116_field_rules_check_needs_a_tenant.sql +0 -72
- package/migrations/117_authz_reconciliation_schedule.sql +0 -82
- package/migrations/118_migration_catalog_writers.sql +0 -450
- package/migrations/119_migration_config_writers.sql +0 -193
- package/migrations/120_migration_facet_writer.sql +0 -139
- package/migrations/121_migration_financial_writers.sql +0 -294
- package/migrations/122_migration_inventory_writers.sql +0 -245
- package/migrations/123_migration_order_item_writer.sql +0 -144
- package/migrations/124_migration_ledger_writers.sql +0 -170
- package/migrations/125_migration_authz_people_writers.sql +0 -205
- package/migrations/126_owner_scoping_is_configuration.sql +0 -247
- package/migrations/127_scaffold_audit_hook.sql +0 -98
- package/migrations/128_shadow_tables_enforced.sql +0 -93
- package/migrations/129_order_kind_terminal_states.sql +0 -57
- package/migrations/130_every_login_is_a_person.sql +0 -196
- package/migrations/131_migration_invoice_split.sql +0 -127
- package/migrations/132_audit_prune_scheduled.sql +0 -28
- package/migrations/133_unit_is_a_session_property.sql +0 -155
- package/migrations/134_field_rules_on_read.sql +0 -341
- package/migrations/135_distribution_polarity.sql +0 -158
- package/migrations/136_unit_id_means_locality.sql +0 -358
- package/migrations/137_vendor_named_spine_objects.sql +0 -126
- package/migrations/138_grants_inherit_down_the_tree.sql +0 -178
- package/migrations/139_distributable_is_a_scaffold_option.sql +0 -109
- package/migrations/140_masking_is_a_scaffold_option.sql +0 -85
- package/migrations/141_renamed_objects_keep_a_compat_name.sql +0 -192
- package/migrations/142_a_person_has_addresses.sql +0 -212
- package/migrations/143_a_professional_has_a_rate.sql +0 -75
- package/migrations/144_a_product_can_be_sold_in_sizes.sql +0 -57
- package/migrations/145_the_app_says_which_tenant_it_is_in.sql +0 -123
- package/migrations/146_products_stock_has_one_writer.sql +0 -67
- package/migrations/147_every_archetype_has_a_kind_registry.sql +0 -388
- package/migrations/148_the_legacy_archetype_stops_being_writable.sql +0 -154
- package/migrations/149_the_event_log_gets_a_consumer.sql +0 -331
- package/migrations/150_the_app_is_configured_in_the_database.sql +0 -285
- package/migrations/151_stock_balance_is_an_archetype.sql +0 -144
- package/migrations/152_a_file_is_an_archetype.sql +0 -222
- package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +0 -199
- package/migrations/154_the_plan_is_a_ceiling.sql +0 -258
- package/migrations/155_a_tenant_can_leave.sql +0 -271
- package/migrations/156_the_service_door_has_a_server_side.sql +0 -335
- package/migrations/157_nobody_creates_objects_in_public.sql +0 -74
- package/migrations/158_the_registry_owns_the_status_vocabulary.sql +0 -153
- package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +0 -132
- package/migrations/160_the_vertical_decides_what_starts_on.sql +0 -246
- package/migrations/161_a_tenant_leaves_with_its_data.sql +0 -363
- package/migrations/162_every_name_has_one_creator_for_real.sql +0 -126
- package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +0 -184
- package/migrations/164_the_sequence_table_joins_the_register.sql +0 -26
- package/migrations/165_a_view_is_not_a_way_in.sql +0 -90
- package/migrations/166_the_platform_billing_leftovers_go.sql +0 -72
- package/migrations/167_a_view_gets_back_what_its_table_grants.sql +0 -112
- package/migrations/168_the_grant_census_stops_crying_wolf.sql +0 -33
- package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +0 -45
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
-- 122_migration_inventory_writers.sql
|
|
2
|
-
--
|
|
3
|
-
-- Inventory gets its writers, and positions get told the truth. #143 / #148.
|
|
4
|
-
--
|
|
5
|
-
-- Every inventory row in the allowlist had `writer = NULL`. Three of them are
|
|
6
|
-
-- plain reference data (measurement units, their conversions, stock locations)
|
|
7
|
-
-- and one is the ledger itself.
|
|
8
|
-
--
|
|
9
|
-
-- POSITIONS ARE NOT MIGRATED, AND THAT IS THE POINT OF THE SLICE. In V2 a
|
|
10
|
-
-- position is DERIVED: `plg_inventory_stock_movements` is the ledger, and the
|
|
11
|
-
-- after-insert trigger of `007` maintains `plg_inventory_stock_positions` from
|
|
12
|
-
-- it. Writing V1 positions in directly would fight the derivation and produce a
|
|
13
|
-
-- balance that agrees with nothing — the first movement after cutover would
|
|
14
|
-
-- recompute it away. So `stock_positions` becomes `verify_only`, exactly like
|
|
15
|
-
-- `client_credits`: the extractor offers it, nothing is written, and
|
|
16
|
-
-- reconciliation compares the V1 balance against the one V2 derived. That is a
|
|
17
|
-
-- stronger check than copying the number would have been, because it proves the
|
|
18
|
-
-- movements add up.
|
|
19
|
-
--
|
|
20
|
-
-- The movement writer declares itself internal. `005`'s single-writer gate
|
|
21
|
-
-- refuses any write to the ledger or to positions unless
|
|
22
|
-
-- `fayz.inventory_internal` is on, and it is right to: "however privileged" is
|
|
23
|
-
-- the phrase in its own message. The migration is one of the internal writers —
|
|
24
|
-
-- it is definer-only, fenced, and audited — so it sets the flag for its
|
|
25
|
-
-- statement and lets the derivation run, rather than being exempted from it.
|
|
26
|
-
--
|
|
27
|
-
-- THREE MAPPINGS NAMED COLUMNS THAT DO NOT EXIST, because `005` renamed them and
|
|
28
|
-
-- 091 was written against the shape before that: `stock_location_id` is
|
|
29
|
-
-- `source_location_id` on movements (and stays `stock_location_id` on positions),
|
|
30
|
-
-- `destination_stock_location_id` is `destination_location_id`, and V1's
|
|
31
|
-
-- `movement_type` is `kind` with a closed vocabulary (in/out/transfer/adjust/
|
|
32
|
-
-- reverse) that is now validated instead of trusted.
|
|
33
|
-
--
|
|
34
|
-
-- TWO TABLES ARE DEFERRED because their destination was never built:
|
|
35
|
-
-- `plg_inventory_stock_unit_labels` and `plg_inventory_supplier_products` do not
|
|
36
|
-
-- exist anywhere in the repo (#143 found this). Creating them here would mean
|
|
37
|
-
-- inventing columns PRD 06 R5 names but does not specify.
|
|
38
|
-
--
|
|
39
|
-
-- Replay-safe: `_allow` upserts under the documented unlock; writers are
|
|
40
|
-
-- CREATE OR REPLACE.
|
|
41
|
-
|
|
42
|
-
ALTER TABLE migration.allowlist DROP CONSTRAINT IF EXISTS allowlist_writer_check;
|
|
43
|
-
ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check
|
|
44
|
-
CHECK (writer IS NULL OR writer IN ('person', 'unit', 'category', 'product', 'order', 'invoice',
|
|
45
|
-
'price_table', 'price_table_item', 'package', 'package_item',
|
|
46
|
-
'field_rule', 'holiday', 'facet',
|
|
47
|
-
'fin_account', 'fin_payment_method_type', 'fin_payment_method',
|
|
48
|
-
'fin_card_brand', 'fin_chart_account', 'fin_cost_center',
|
|
49
|
-
'inv_measurement_unit', 'inv_unit_conversion', 'inv_location', 'inv_movement'));
|
|
50
|
-
|
|
51
|
-
DO $unlock$
|
|
52
|
-
BEGIN
|
|
53
|
-
PERFORM set_config('migration.allowlist_unlock', 'on', true);
|
|
54
|
-
|
|
55
|
-
UPDATE migration.allowlist SET writer = 'inv_measurement_unit' WHERE source_table = 'measurement_units';
|
|
56
|
-
UPDATE migration.allowlist SET writer = 'inv_unit_conversion' WHERE source_table = 'unit_conversions';
|
|
57
|
-
UPDATE migration.allowlist SET writer = 'inv_location' WHERE source_table = 'stock_locations';
|
|
58
|
-
|
|
59
|
-
UPDATE migration.allowlist SET writer = 'inv_movement',
|
|
60
|
-
columns = '{"uuid":"id","product_id":"product_id@products","stock_location_id":"source_location_id@stock_locations","destination_stock_location_id":"destination_location_id@stock_locations","movement_type":"kind","quantity":"quantity","unit_cost":"unit_cost","total_cost":"total_cost","batch_number":"batch_number","expiration_date":"expiration_date","document_number":"document_number","reason":"reason","notes":"notes","movement_date":"movement_date","supplier_id":"jsonb:v1_supplier_ref"}'::jsonb,
|
|
61
|
-
notes = 'PRD 06 R1: the append-only ledger. 005 renamed stock_location_id -> source_location_id and movement_type -> kind; 091 was written against the shape before that (#112).'
|
|
62
|
-
WHERE source_table = 'stock_movements';
|
|
63
|
-
|
|
64
|
-
-- positions are DERIVED from the ledger in V2; copying V1''s numbers in would
|
|
65
|
-
-- fight the derivation, so reconciliation compares them instead
|
|
66
|
-
UPDATE migration.allowlist SET writer = NULL, writer_args = '{"verify_only":true}'::jsonb,
|
|
67
|
-
notes = 'PRD 06 R1: NOT migrated. A position is derived from plg_inventory_stock_movements by 007''s trigger; writing V1 balances in directly would fight that and the first movement after cutover would recompute them away. Reconciliation compares the V1 balance against the derived one, which proves the movements add up (#112).'
|
|
68
|
-
WHERE source_table = 'stock_positions';
|
|
69
|
-
|
|
70
|
-
DELETE FROM migration.allowlist WHERE source_table IN ('stock_unit_labels', 'supplier_product_mappings',
|
|
71
|
-
'discount_reasons', 'discount_users',
|
|
72
|
-
'cancellation_reasons', 'cancellation_users');
|
|
73
|
-
END $unlock$;
|
|
74
|
-
|
|
75
|
-
INSERT INTO migration.excluded_tables (source_table, reason) VALUES
|
|
76
|
-
('stock_unit_labels', 'Deferred: public.plg_inventory_stock_unit_labels does not exist anywhere in the repo. PRD 06 R5 names the concept without specifying it, so building the table here would mean inventing its columns (#143).'),
|
|
77
|
-
('discount_reasons', 'Deferred: 091 pointed it at `config.orders`, a schema that never shipped. PRD 04 R7 says a discount reason is configuration of the owning plugin (orders/financial) — and no such table was built, so there is nowhere to put it. A tenant re-declares its reasons in V2 (#112).'),
|
|
78
|
-
('discount_users', 'Deferred with discount_reasons: who may grant a discount is a permission in V2 (app.role_permissions), not a row (#112).'),
|
|
79
|
-
('cancellation_reasons', 'Deferred: same as discount_reasons — configuration with no destination contract (#112).'),
|
|
80
|
-
('cancellation_users', 'Deferred with cancellation_reasons: who may cancel is a permission in V2, not a row (#112).'),
|
|
81
|
-
('supplier_product_mappings', 'Deferred: public.plg_inventory_supplier_products does not exist. Same as stock_unit_labels — named by PRD 06 R5, never specified. The supplier itself IS migrated, as a Person with the supplier role (#143).')
|
|
82
|
-
ON CONFLICT (source_table) DO UPDATE SET reason = EXCLUDED.reason;
|
|
83
|
-
|
|
84
|
-
CREATE OR REPLACE FUNCTION migration.upsert_inv_measurement_unit(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
|
|
85
|
-
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
86
|
-
DECLARE v_existing uuid; v_created boolean := false; v_standalone boolean := migration._standalone();
|
|
87
|
-
BEGIN
|
|
88
|
-
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
89
|
-
IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN
|
|
90
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a measurement unit needs an id and a name');
|
|
91
|
-
END IF;
|
|
92
|
-
SELECT tenant_id INTO v_existing FROM public.plg_inventory_measurement_units WHERE id = p_id;
|
|
93
|
-
IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN
|
|
94
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('measurement unit %s already exists in another tenant', p_id));
|
|
95
|
-
END IF;
|
|
96
|
-
IF v_existing IS NULL THEN
|
|
97
|
-
INSERT INTO public.plg_inventory_measurement_units (id, tenant_id, name, abbreviation, is_active)
|
|
98
|
-
VALUES (p_id, p_tenant, p_row ->> 'name', p_row ->> 'abbreviation', coalesce((p_row ->> 'is_active')::boolean, true));
|
|
99
|
-
v_created := true;
|
|
100
|
-
ELSE
|
|
101
|
-
UPDATE public.plg_inventory_measurement_units SET
|
|
102
|
-
name = p_row ->> 'name',
|
|
103
|
-
abbreviation = CASE WHEN p_row ? 'abbreviation' THEN p_row ->> 'abbreviation' ELSE abbreviation END,
|
|
104
|
-
is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END
|
|
105
|
-
WHERE id = p_id;
|
|
106
|
-
END IF;
|
|
107
|
-
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_inventory_measurement_units', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
|
|
108
|
-
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
|
|
109
|
-
END $$;
|
|
110
|
-
|
|
111
|
-
CREATE OR REPLACE FUNCTION migration.upsert_inv_unit_conversion(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
|
|
112
|
-
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
113
|
-
DECLARE v_existing uuid; v_created boolean := false; v_standalone boolean := migration._standalone();
|
|
114
|
-
BEGIN
|
|
115
|
-
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
116
|
-
IF p_id IS NULL OR (p_row ->> 'from_unit_id') IS NULL OR (p_row ->> 'to_unit_id') IS NULL THEN
|
|
117
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk', 'reason', 'a conversion needs both of its units');
|
|
118
|
-
END IF;
|
|
119
|
-
IF coalesce((p_row ->> 'factor')::numeric, 0) <= 0 THEN
|
|
120
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a conversion factor must be > 0 — a zero factor would make every converted quantity vanish');
|
|
121
|
-
END IF;
|
|
122
|
-
SELECT tenant_id INTO v_existing FROM public.plg_inventory_unit_conversions WHERE id = p_id;
|
|
123
|
-
IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN
|
|
124
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('conversion %s already exists in another tenant', p_id));
|
|
125
|
-
END IF;
|
|
126
|
-
IF v_existing IS NULL THEN
|
|
127
|
-
INSERT INTO public.plg_inventory_unit_conversions (id, tenant_id, from_unit_id, to_unit_id, factor, product_id)
|
|
128
|
-
VALUES (p_id, p_tenant, (p_row ->> 'from_unit_id')::uuid, (p_row ->> 'to_unit_id')::uuid,
|
|
129
|
-
(p_row ->> 'factor')::numeric, (p_row ->> 'product_id')::uuid);
|
|
130
|
-
v_created := true;
|
|
131
|
-
ELSE
|
|
132
|
-
UPDATE public.plg_inventory_unit_conversions SET
|
|
133
|
-
from_unit_id = (p_row ->> 'from_unit_id')::uuid, to_unit_id = (p_row ->> 'to_unit_id')::uuid,
|
|
134
|
-
factor = (p_row ->> 'factor')::numeric
|
|
135
|
-
WHERE id = p_id;
|
|
136
|
-
END IF;
|
|
137
|
-
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_inventory_unit_conversions', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
|
|
138
|
-
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
|
|
139
|
-
END $$;
|
|
140
|
-
|
|
141
|
-
CREATE OR REPLACE FUNCTION migration.upsert_inv_location(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
|
|
142
|
-
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
143
|
-
DECLARE v_existing uuid; v_created boolean := false; v_standalone boolean := migration._standalone();
|
|
144
|
-
BEGIN
|
|
145
|
-
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
146
|
-
IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN
|
|
147
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a stock location needs an id and a name');
|
|
148
|
-
END IF;
|
|
149
|
-
-- PRD 06 R1: a location belongs to a unit. V1 rows without one cannot be placed.
|
|
150
|
-
IF (p_row ->> 'unit_id') IS NULL THEN
|
|
151
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk', 'reason', 'a stock location needs its unit (PRD 06 R1: unit_id is mandatory)');
|
|
152
|
-
END IF;
|
|
153
|
-
SELECT tenant_id INTO v_existing FROM public.plg_inventory_stock_locations WHERE id = p_id;
|
|
154
|
-
IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN
|
|
155
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('stock location %s already exists in another tenant', p_id));
|
|
156
|
-
END IF;
|
|
157
|
-
IF v_existing IS NULL THEN
|
|
158
|
-
INSERT INTO public.plg_inventory_stock_locations (id, tenant_id, name, description, unit_id, is_active)
|
|
159
|
-
VALUES (p_id, p_tenant, p_row ->> 'name', p_row ->> 'description', (p_row ->> 'unit_id')::uuid,
|
|
160
|
-
coalesce((p_row ->> 'is_active')::boolean, true));
|
|
161
|
-
v_created := true;
|
|
162
|
-
ELSE
|
|
163
|
-
UPDATE public.plg_inventory_stock_locations SET
|
|
164
|
-
name = p_row ->> 'name',
|
|
165
|
-
description = CASE WHEN p_row ? 'description' THEN p_row ->> 'description' ELSE description END,
|
|
166
|
-
unit_id = (p_row ->> 'unit_id')::uuid,
|
|
167
|
-
is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END
|
|
168
|
-
WHERE id = p_id;
|
|
169
|
-
END IF;
|
|
170
|
-
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_inventory_stock_locations', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
|
|
171
|
-
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
|
|
172
|
-
END $$;
|
|
173
|
-
|
|
174
|
-
CREATE OR REPLACE FUNCTION migration.upsert_inv_movement(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
|
|
175
|
-
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
176
|
-
DECLARE
|
|
177
|
-
v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'kind', ''))), '');
|
|
178
|
-
v_kind text := CASE v_kind_raw
|
|
179
|
-
WHEN 'in' THEN 'in' WHEN 'out' THEN 'out'
|
|
180
|
-
WHEN 'transfer' THEN 'transfer' WHEN 'adjust' THEN 'adjust' WHEN 'reverse' THEN 'reverse'
|
|
181
|
-
-- the SDK's legacy labels, and V1's pt-BR
|
|
182
|
-
WHEN 'entry' THEN 'in' WHEN 'entrada' THEN 'in'
|
|
183
|
-
WHEN 'exit' THEN 'out' WHEN 'saida' THEN 'out' WHEN 'saída' THEN 'out'
|
|
184
|
-
WHEN 'loss' THEN 'out' WHEN 'perda' THEN 'out'
|
|
185
|
-
WHEN 'adjustment' THEN 'adjust' WHEN 'ajuste' THEN 'adjust'
|
|
186
|
-
WHEN 'transferencia' THEN 'transfer' WHEN 'transferência' THEN 'transfer'
|
|
187
|
-
END;
|
|
188
|
-
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
189
|
-
v_exists boolean;
|
|
190
|
-
v_standalone boolean := migration._standalone();
|
|
191
|
-
BEGIN
|
|
192
|
-
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
193
|
-
IF p_id IS NULL OR (p_row ->> 'product_id') IS NULL THEN
|
|
194
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk', 'reason', 'a stock movement needs an id and its product');
|
|
195
|
-
END IF;
|
|
196
|
-
IF v_kind IS NULL THEN
|
|
197
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
|
|
198
|
-
'reason', format('unknown movement type %L (expected in|out|transfer|adjust|reverse, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));
|
|
199
|
-
END IF;
|
|
200
|
-
IF coalesce((p_row ->> 'quantity')::numeric, 0) <= 0 THEN
|
|
201
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a stock movement needs a quantity > 0 — direction is the kind, not the sign');
|
|
202
|
-
END IF;
|
|
203
|
-
IF v_kind = 'transfer' AND (p_row ->> 'destination_location_id') IS NULL THEN
|
|
204
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a transfer needs its destination location');
|
|
205
|
-
END IF;
|
|
206
|
-
|
|
207
|
-
SELECT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements WHERE id = p_id) INTO v_exists;
|
|
208
|
-
IF v_exists THEN
|
|
209
|
-
-- the ledger is append-only: a second offer of the same movement is a replay,
|
|
210
|
-
-- not an update. Nothing to do, and saying "migrated" again is honest —
|
|
211
|
-
-- the destination already holds exactly this row.
|
|
212
|
-
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'already in the ledger (append-only)');
|
|
213
|
-
END IF;
|
|
214
|
-
|
|
215
|
-
-- 005's single-writer gate refuses any write "however privileged" unless this
|
|
216
|
-
-- is set. The migration IS one of the internal writers — definer-only, fenced
|
|
217
|
-
-- and audited — so it declares itself rather than being exempted, and the
|
|
218
|
-
-- after-insert trigger derives the position from what it writes.
|
|
219
|
-
PERFORM set_config('fayz.inventory_internal', 'on', true);
|
|
220
|
-
INSERT INTO public.plg_inventory_stock_movements
|
|
221
|
-
(id, tenant_id, product_id, kind, quantity, unit_cost, total_cost,
|
|
222
|
-
source_location_id, destination_location_id, batch_number, expiration_date,
|
|
223
|
-
document_number, reason, notes, movement_date, metadata)
|
|
224
|
-
VALUES (p_id, p_tenant, (p_row ->> 'product_id')::uuid, v_kind,
|
|
225
|
-
(p_row ->> 'quantity')::numeric,
|
|
226
|
-
(p_row ->> 'unit_cost')::numeric, (p_row ->> 'total_cost')::numeric,
|
|
227
|
-
(p_row ->> 'source_location_id')::uuid, (p_row ->> 'destination_location_id')::uuid,
|
|
228
|
-
p_row ->> 'batch_number', (p_row ->> 'expiration_date')::date,
|
|
229
|
-
p_row ->> 'document_number', p_row ->> 'reason', p_row ->> 'notes',
|
|
230
|
-
coalesce((p_row ->> 'movement_date')::timestamptz, now()), v_meta);
|
|
231
|
-
PERFORM set_config('fayz.inventory_internal', 'off', true);
|
|
232
|
-
|
|
233
|
-
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_inventory_stock_movements', p_id, jsonb_build_object('reason', 'created')); END IF;
|
|
234
|
-
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');
|
|
235
|
-
END $$;
|
|
236
|
-
|
|
237
|
-
DO $$
|
|
238
|
-
DECLARE r record;
|
|
239
|
-
BEGIN
|
|
240
|
-
FOR r IN SELECT p.oid::regprocedure AS sig FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
241
|
-
WHERE n.nspname = 'migration' AND p.proname LIKE 'upsert\_inv\_%' LOOP
|
|
242
|
-
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM public, anon, authenticated', r.sig);
|
|
243
|
-
EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', r.sig);
|
|
244
|
-
END LOOP;
|
|
245
|
-
END $$;
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
-- 123_migration_order_item_writer.sql
|
|
2
|
-
--
|
|
3
|
-
-- The Venda's lines, and four histories that were never mapped. #140 / #148.
|
|
4
|
-
--
|
|
5
|
-
-- `invoice_items` is the last big row-per-row destination in the catalog/orders
|
|
6
|
-
-- half. It becomes `public.order_items`, which `091` made a first-class business
|
|
7
|
-
-- table (tenant_id, unit_id, composite FK to the order, execution status as a
|
|
8
|
-
-- column).
|
|
9
|
-
--
|
|
10
|
-
-- ONE THING THE WRITER REFUSES ON PURPOSE. `091` gave the line an
|
|
11
|
-
-- `execution_status` COLUMN, and 091 maps V1's `is_executed` into
|
|
12
|
-
-- `jsonb:execution.is_executed`. Both cannot be the truth. The column wins — it
|
|
13
|
-
-- is what `financial_cancel_paid_item` reads — so the writer sets it from the
|
|
14
|
-
-- V1 flag and leaves the rest of the execution detail in metadata, which is what
|
|
15
|
-
-- ADR 0009 says a promoted column means. This is the same defect as #198, caught
|
|
16
|
-
-- before it shipped rather than after.
|
|
17
|
-
--
|
|
18
|
-
-- THREE FACETS AND FOUR HISTORIES:
|
|
19
|
-
--
|
|
20
|
-
-- invoice_item_adjustments / _participants / invoice_items_discounted map NO
|
|
21
|
-
-- column (`'{}'` in 091), so they become facets of the line. The facet writer
|
|
22
|
-
-- reports `skipped` with "the facet maps no column into the destination" —
|
|
23
|
-
-- recorded, not silently dropped, and reconcile-safe.
|
|
24
|
-
--
|
|
25
|
-
-- invoice_cancellation_logs, invoice_item_cancellation_logs,
|
|
26
|
-
-- payment_deletion_logs and payment_unlink_logs are DEFERRED. They map no
|
|
27
|
-
-- column either, and unlike a facet there is no row for them to enrich: each
|
|
28
|
-
-- would have to become an `audit_logs` row, and `public.audit_logs` is the
|
|
29
|
-
-- actor-derived log of V2 actions — every row carries the user who did it and
|
|
30
|
-
-- the entity it happened to. A V1 history row has neither in the mapping, so
|
|
31
|
-
-- importing it produces audit entries with no actor and no content. They are
|
|
32
|
-
-- re-admitted the day someone maps their columns.
|
|
33
|
-
--
|
|
34
|
-
-- bill_attachments points at `storage`: files, not rows. Recorded as a
|
|
35
|
-
-- decision rather than left pending forever.
|
|
36
|
-
--
|
|
37
|
-
-- Replay-safe: `_allow` upserts under the documented unlock; writer is
|
|
38
|
-
-- CREATE OR REPLACE.
|
|
39
|
-
|
|
40
|
-
ALTER TABLE migration.allowlist DROP CONSTRAINT IF EXISTS allowlist_writer_check;
|
|
41
|
-
ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check
|
|
42
|
-
CHECK (writer IS NULL OR writer IN ('person', 'unit', 'category', 'product', 'order', 'invoice',
|
|
43
|
-
'price_table', 'price_table_item', 'package', 'package_item',
|
|
44
|
-
'field_rule', 'holiday', 'facet',
|
|
45
|
-
'fin_account', 'fin_payment_method_type', 'fin_payment_method',
|
|
46
|
-
'fin_card_brand', 'fin_chart_account', 'fin_cost_center',
|
|
47
|
-
'inv_measurement_unit', 'inv_unit_conversion', 'inv_location', 'inv_movement',
|
|
48
|
-
'order_item'));
|
|
49
|
-
|
|
50
|
-
DO $unlock$
|
|
51
|
-
BEGIN
|
|
52
|
-
PERFORM set_config('migration.allowlist_unlock', 'on', true);
|
|
53
|
-
|
|
54
|
-
UPDATE migration.allowlist SET writer = 'order_item' WHERE source_table = 'invoice_items';
|
|
55
|
-
|
|
56
|
-
UPDATE migration.allowlist SET writer = 'facet', writer_args = jsonb_build_object('target', 'public.order_items')
|
|
57
|
-
WHERE source_table IN ('invoice_item_adjustments', 'invoice_item_participants', 'invoice_items_discounted');
|
|
58
|
-
|
|
59
|
-
UPDATE migration.allowlist SET writer = 'facet', writer_args = jsonb_build_object('target', 'public.products')
|
|
60
|
-
WHERE source_table = 'product_intermediate_info';
|
|
61
|
-
|
|
62
|
-
DELETE FROM migration.allowlist
|
|
63
|
-
WHERE source_table IN ('invoice_cancellation_logs', 'invoice_item_cancellation_logs',
|
|
64
|
-
'payment_deletion_logs', 'payment_unlink_logs', 'bill_attachments');
|
|
65
|
-
END $unlock$;
|
|
66
|
-
|
|
67
|
-
INSERT INTO migration.excluded_tables (source_table, reason) VALUES
|
|
68
|
-
('invoice_cancellation_logs', 'Deferred: 091 maps no column, and public.audit_logs is the actor-derived log of V2 actions — every row carries who did it and what it happened to. A V1 history row has neither in the mapping, so importing it produces audit entries with no actor and no content. Re-admit when the columns are mapped (#113).'),
|
|
69
|
-
('invoice_item_cancellation_logs', 'Deferred with invoice_cancellation_logs (#113).'),
|
|
70
|
-
('payment_deletion_logs', 'Deferred with invoice_cancellation_logs (#113).'),
|
|
71
|
-
('payment_unlink_logs', 'Deferred with invoice_cancellation_logs (#113).'),
|
|
72
|
-
('bill_attachments', 'Not a row destination: bill attachments are FILES. 091 pointed them at `storage`, which is object storage and not something a row writer reaches. Recorded as a decision rather than left pending forever (#113).')
|
|
73
|
-
ON CONFLICT (source_table) DO UPDATE SET reason = EXCLUDED.reason;
|
|
74
|
-
|
|
75
|
-
CREATE OR REPLACE FUNCTION migration.upsert_order_item(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
|
|
76
|
-
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
77
|
-
DECLARE
|
|
78
|
-
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
79
|
-
v_order uuid;
|
|
80
|
-
v_order_tenant uuid;
|
|
81
|
-
v_exec text;
|
|
82
|
-
v_existing uuid;
|
|
83
|
-
v_created boolean := false;
|
|
84
|
-
v_standalone boolean := migration._standalone();
|
|
85
|
-
BEGIN
|
|
86
|
-
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
87
|
-
BEGIN
|
|
88
|
-
v_order := (p_row ->> 'order_id')::uuid;
|
|
89
|
-
EXCEPTION WHEN OTHERS THEN v_order := NULL; END;
|
|
90
|
-
IF p_id IS NULL OR v_order IS NULL THEN
|
|
91
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk', 'reason', 'an order line needs its order — a line without a Venda is not a line');
|
|
92
|
-
END IF;
|
|
93
|
-
SELECT tenant_id INTO v_order_tenant FROM public.orders WHERE id = v_order;
|
|
94
|
-
IF v_order_tenant IS NULL THEN
|
|
95
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk', 'reason', format('order %s is not in the destination yet', v_order));
|
|
96
|
-
END IF;
|
|
97
|
-
IF v_order_tenant <> p_tenant THEN
|
|
98
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order %s belongs to another tenant', v_order));
|
|
99
|
-
END IF;
|
|
100
|
-
|
|
101
|
-
-- 081 promoted execution state to a COLUMN and 091 still maps V1's is_executed
|
|
102
|
-
-- into JSONB. Both cannot be the truth; the column is what
|
|
103
|
-
-- financial_cancel_paid_item reads, so it is set from the flag and the rest of
|
|
104
|
-
-- the execution detail stays in metadata (ADR 0009).
|
|
105
|
-
v_exec := CASE
|
|
106
|
-
WHEN (v_meta #>> '{execution,is_executed}') IN ('true', 't', '1') THEN 'executed'
|
|
107
|
-
WHEN (v_meta #>> '{cancellation,at}') IS NOT NULL THEN 'cancelled'
|
|
108
|
-
ELSE 'pending'
|
|
109
|
-
END;
|
|
110
|
-
|
|
111
|
-
SELECT tenant_id INTO v_existing FROM public.order_items WHERE id = p_id;
|
|
112
|
-
IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN
|
|
113
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order line %s already exists in another tenant', p_id));
|
|
114
|
-
END IF;
|
|
115
|
-
IF v_existing IS NULL THEN
|
|
116
|
-
INSERT INTO public.order_items (id, tenant_id, order_id, product_id, name, description,
|
|
117
|
-
quantity, unit_price, discount, total, sort_order, execution_status, metadata)
|
|
118
|
-
VALUES (p_id, p_tenant, v_order, (p_row ->> 'product_id')::uuid,
|
|
119
|
-
coalesce(nullif(btrim(coalesce(p_row ->> 'name', '')), ''), 'Item'),
|
|
120
|
-
p_row ->> 'description',
|
|
121
|
-
coalesce((p_row ->> 'quantity')::numeric, 1),
|
|
122
|
-
round(coalesce((p_row ->> 'unit_price')::numeric, 0), 2),
|
|
123
|
-
round(coalesce((p_row ->> 'discount')::numeric, 0), 2),
|
|
124
|
-
round(coalesce((p_row ->> 'total')::numeric, 0), 2),
|
|
125
|
-
coalesce((p_row ->> 'sort_order')::integer, 0), v_exec, v_meta);
|
|
126
|
-
v_created := true;
|
|
127
|
-
ELSE
|
|
128
|
-
UPDATE public.order_items SET
|
|
129
|
-
order_id = v_order,
|
|
130
|
-
product_id = CASE WHEN p_row ? 'product_id' THEN (p_row ->> 'product_id')::uuid ELSE product_id END,
|
|
131
|
-
name = CASE WHEN p_row ? 'name' THEN coalesce(nullif(btrim(p_row ->> 'name'), ''), name) ELSE name END,
|
|
132
|
-
quantity = CASE WHEN p_row ? 'quantity' THEN coalesce((p_row ->> 'quantity')::numeric, 1) ELSE quantity END,
|
|
133
|
-
unit_price = CASE WHEN p_row ? 'unit_price' THEN round(coalesce((p_row ->> 'unit_price')::numeric, 0), 2) ELSE unit_price END,
|
|
134
|
-
discount = CASE WHEN p_row ? 'discount' THEN round(coalesce((p_row ->> 'discount')::numeric, 0), 2) ELSE discount END,
|
|
135
|
-
total = CASE WHEN p_row ? 'total' THEN round(coalesce((p_row ->> 'total')::numeric, 0), 2) ELSE total END,
|
|
136
|
-
execution_status = v_exec,
|
|
137
|
-
metadata = migration._merge_meta(metadata, v_meta)
|
|
138
|
-
WHERE id = p_id;
|
|
139
|
-
END IF;
|
|
140
|
-
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.order_items', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;
|
|
141
|
-
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
|
|
142
|
-
END $$;
|
|
143
|
-
REVOKE ALL ON FUNCTION migration.upsert_order_item(uuid, uuid, jsonb, jsonb) FROM public, anon, authenticated;
|
|
144
|
-
GRANT EXECUTE ON FUNCTION migration.upsert_order_item(uuid, uuid, jsonb, jsonb) TO service_role;
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
-- 124_migration_ledger_writers.sql
|
|
2
|
-
--
|
|
3
|
-
-- V1's money events reach the two-leg ledger. #142 / #148.
|
|
4
|
-
--
|
|
5
|
-
-- `financial_movements` is the largest financial table in a V1 pool and had no
|
|
6
|
-
-- writer, so every one of a tenant's money events landed `pending`.
|
|
7
|
-
--
|
|
8
|
-
-- LEGS. V2's ledger is two-legged and `plg_financial_movements_legs_check`
|
|
9
|
-
-- refuses a money event without both. A V1 movement has neither — it predates
|
|
10
|
-
-- the model — so the writer resolves them the same way the in-place backfill
|
|
11
|
-
-- does, through `plg_financial_legacy_legs`: the money holder from the bank
|
|
12
|
-
-- account or the cash session, the counter-leg from the invoice's direction, and
|
|
13
|
-
-- `legacy` / `legacy_holder` when neither is knowable. Reusing that function
|
|
14
|
-
-- rather than reimplementing it means a migrated pool and an upgraded pool end
|
|
15
|
-
-- up with the same postings, which is the property that makes the two paths
|
|
16
|
-
-- comparable at all.
|
|
17
|
-
--
|
|
18
|
-
-- Bills and commissions are obligation MIRRORS, not money events: the helper
|
|
19
|
-
-- says so and they are inserted without legs, which is what `legs_check`'s own
|
|
20
|
-
-- first clause allows.
|
|
21
|
-
--
|
|
22
|
-
-- TWO TABLES ARE DEFERRED, both because writing them would mean guessing at
|
|
23
|
-
-- money:
|
|
24
|
-
--
|
|
25
|
-
-- payment_allocations. V1 links a payment movement to a BILL MOVEMENT; V2
|
|
26
|
-
-- links a payment movement to an INSTALLMENT (`plg_financial_invoice_
|
|
27
|
-
-- installments`), and those are different objects — the installment is created
|
|
28
|
-
-- by `financial_create_invoice`, not by the movement import. 101's mapping
|
|
29
|
-
-- papers over that by pointing `bill_movement_id` at
|
|
30
|
-
-- `installment_id@financial_movements`, which would write a movement's id into
|
|
31
|
-
-- a column that means an installment. An allocation is what says WHICH debt a
|
|
32
|
-
-- payment settled; getting it wrong silently mis-states every balance built on
|
|
33
|
-
-- it, and there is no way to verify the mapping without the real V1 rows. It
|
|
34
|
-
-- is re-admitted with #141's invoice split, which is what creates the
|
|
35
|
-
-- installments an allocation can point at.
|
|
36
|
-
--
|
|
37
|
-
-- cc_receiving_fee_configs. 091 maps NO column (`'{}'`), and a fee config is
|
|
38
|
-
-- brand x installment range x rate per receiving account — four things, none
|
|
39
|
-
-- of them mapped. A wrong fee rate silently mis-states every card receivable.
|
|
40
|
-
--
|
|
41
|
-
-- Replay-safe: `_allow` upserts under the documented unlock; writers are
|
|
42
|
-
-- CREATE OR REPLACE; the movement writer is idempotent by id and the ledger is
|
|
43
|
-
-- append-only, so a second offer is a replay.
|
|
44
|
-
|
|
45
|
-
ALTER TABLE migration.allowlist DROP CONSTRAINT IF EXISTS allowlist_writer_check;
|
|
46
|
-
ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check
|
|
47
|
-
CHECK (writer IS NULL OR writer IN ('person', 'unit', 'category', 'product', 'order', 'invoice',
|
|
48
|
-
'price_table', 'price_table_item', 'package', 'package_item',
|
|
49
|
-
'field_rule', 'holiday', 'facet',
|
|
50
|
-
'fin_account', 'fin_payment_method_type', 'fin_payment_method',
|
|
51
|
-
'fin_card_brand', 'fin_chart_account', 'fin_cost_center',
|
|
52
|
-
'inv_measurement_unit', 'inv_unit_conversion', 'inv_location', 'inv_movement',
|
|
53
|
-
'order_item', 'fin_movement'));
|
|
54
|
-
|
|
55
|
-
DO $unlock$
|
|
56
|
-
BEGIN
|
|
57
|
-
PERFORM set_config('migration.allowlist_unlock', 'on', true);
|
|
58
|
-
-- `direction` was NOT MAPPED by 091, and it is the column that says whether
|
|
59
|
-
-- money came in or went out. Without it every movement would have to guess.
|
|
60
|
-
UPDATE migration.allowlist SET writer = 'fin_movement',
|
|
61
|
-
columns = columns || '{"direction":"direction"}'::jsonb
|
|
62
|
-
WHERE source_table = 'financial_movements';
|
|
63
|
-
DELETE FROM migration.allowlist WHERE source_table IN ('payment_allocations', 'cc_receiving_fee_configs',
|
|
64
|
-
'cash_register_sessions', 'cash_session_reviewers');
|
|
65
|
-
END $unlock$;
|
|
66
|
-
|
|
67
|
-
INSERT INTO migration.excluded_tables (source_table, reason) VALUES
|
|
68
|
-
('payment_allocations', 'Deferred: V1 links a payment movement to a BILL MOVEMENT; V2 links it to an INSTALLMENT, and those are different objects — the installment is created by financial_create_invoice, not by the movement import. 091 papered over that by pointing bill_movement_id at installment_id@financial_movements, which writes a movement id into a column meaning an installment. An allocation says WHICH debt a payment settled; getting it wrong silently mis-states every balance built on it. Re-admit with #141''s invoice split, which creates the installments an allocation can point at (#114).'),
|
|
69
|
-
('cash_register_sessions', 'Deferred: a V2 cash session is ATTRIBUTED — user_id is NOT NULL and only one may be open per user and unit, because the session is the accountability record for a drawer. 091 carries V1''s opener only as an opaque `v1_opened_by` reference with no mapping to a V2 auth user, so every migrated session would have to be attributed to somebody who did not open it. Re-admit when V1 users are mapped to V2 logins (#114).'),
|
|
70
|
-
('cash_session_reviewers', 'Deferred with cash_register_sessions: a reviewer of a session that is not migrated has nothing to review (#114).'),
|
|
71
|
-
('cc_receiving_fee_configs', 'Deferred: 091 maps no column at all, and a fee config is brand x installment range x rate per receiving account — four things, none of them mapped. A wrong fee rate silently mis-states every card receivable (#114).')
|
|
72
|
-
ON CONFLICT (source_table) DO UPDATE SET reason = EXCLUDED.reason;
|
|
73
|
-
|
|
74
|
-
CREATE OR REPLACE FUNCTION migration.upsert_fin_movement(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
|
|
75
|
-
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
76
|
-
DECLARE
|
|
77
|
-
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
78
|
-
v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'movement_kind', ''))), '');
|
|
79
|
-
v_kind text := CASE v_kind_raw
|
|
80
|
-
WHEN 'bill' THEN 'bill' WHEN 'payment' THEN 'payment'
|
|
81
|
-
WHEN 'transfer' THEN 'transfer' WHEN 'commission' THEN 'commission'
|
|
82
|
-
-- V1's vocabulary
|
|
83
|
-
WHEN 'conta' THEN 'bill' WHEN 'titulo' THEN 'bill' WHEN 'título' THEN 'bill'
|
|
84
|
-
WHEN 'pagamento' THEN 'payment' WHEN 'recebimento' THEN 'payment'
|
|
85
|
-
WHEN 'transferencia' THEN 'transfer' WHEN 'transferência' THEN 'transfer'
|
|
86
|
-
WHEN 'comissao' THEN 'commission' WHEN 'comissão' THEN 'commission'
|
|
87
|
-
END;
|
|
88
|
-
v_dir text := nullif(btrim(lower(coalesce(p_row ->> 'direction', ''))), '');
|
|
89
|
-
v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);
|
|
90
|
-
v_legs record;
|
|
91
|
-
v_debit uuid := (p_row ->> 'debit_account_id')::uuid;
|
|
92
|
-
v_credit uuid := (p_row ->> 'credit_account_id')::uuid;
|
|
93
|
-
v_exists boolean;
|
|
94
|
-
v_standalone boolean := migration._standalone();
|
|
95
|
-
BEGIN
|
|
96
|
-
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
97
|
-
IF p_id IS NULL THEN
|
|
98
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a movement needs an id');
|
|
99
|
-
END IF;
|
|
100
|
-
IF v_kind IS NULL THEN
|
|
101
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
|
|
102
|
-
'reason', format('unknown movement kind %L (expected bill|payment|transfer|commission, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));
|
|
103
|
-
END IF;
|
|
104
|
-
IF v_dir IS NOT NULL AND v_dir NOT IN ('debit', 'credit') THEN
|
|
105
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown direction %L (expected debit|credit)', v_dir));
|
|
106
|
-
END IF;
|
|
107
|
-
-- A V1 pool that does not carry the column still knows the direction, through
|
|
108
|
-
-- the invoice the movement settles: a receivable is money in, a payable money
|
|
109
|
-
-- out. That is a derivation, not a guess. When there is no invoice either,
|
|
110
|
-
-- refuse — "which way did this money go" is not something to default.
|
|
111
|
-
IF v_dir IS NULL AND (p_row ->> 'financial_invoice_id') IS NOT NULL THEN
|
|
112
|
-
SELECT CASE t.direction WHEN 'receivable' THEN 'credit' WHEN 'payable' THEN 'debit' END INTO v_dir
|
|
113
|
-
FROM public.plg_financial_invoices t WHERE t.id = (p_row ->> 'financial_invoice_id')::uuid;
|
|
114
|
-
END IF;
|
|
115
|
-
IF v_dir IS NULL THEN
|
|
116
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
|
|
117
|
-
'reason', 'the movement carries no direction and settles no invoice — which way the money went cannot be derived, and must not be defaulted');
|
|
118
|
-
END IF;
|
|
119
|
-
IF v_amount < 0 THEN
|
|
120
|
-
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a money event cannot carry a negative amount — direction is the direction, not the sign');
|
|
121
|
-
END IF;
|
|
122
|
-
|
|
123
|
-
SELECT EXISTS (SELECT 1 FROM public.plg_financial_movements WHERE id = p_id) INTO v_exists;
|
|
124
|
-
IF v_exists THEN
|
|
125
|
-
-- append-only: a second offer of the same money event is a replay
|
|
126
|
-
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'already in the ledger (append-only)');
|
|
127
|
-
END IF;
|
|
128
|
-
|
|
129
|
-
-- Legs, resolved exactly as the in-place backfill resolves them, so a migrated
|
|
130
|
-
-- pool and an upgraded pool end up with the same postings.
|
|
131
|
-
IF v_debit IS NULL OR v_credit IS NULL THEN
|
|
132
|
-
SELECT * INTO v_legs FROM public.plg_financial_legacy_legs(
|
|
133
|
-
p_tenant, v_kind, v_dir, (p_row ->> 'bank_account_id')::uuid, (p_row ->> 'cash_session_id')::uuid,
|
|
134
|
-
(p_row ->> 'financial_invoice_id')::uuid, v_meta);
|
|
135
|
-
IF v_legs.o_mirror THEN
|
|
136
|
-
-- an obligation mirror, not a money event: legs_check allows it legless
|
|
137
|
-
v_meta := v_meta || jsonb_build_object('legacy_mirror', true);
|
|
138
|
-
v_debit := NULL; v_credit := NULL;
|
|
139
|
-
ELSE
|
|
140
|
-
v_debit := coalesce(v_debit, v_legs.o_debit);
|
|
141
|
-
v_credit := coalesce(v_credit, v_legs.o_credit);
|
|
142
|
-
END IF;
|
|
143
|
-
END IF;
|
|
144
|
-
|
|
145
|
-
INSERT INTO public.plg_financial_movements
|
|
146
|
-
(id, tenant_id, financial_invoice_id, direction, movement_kind, amount, paid_amount, status,
|
|
147
|
-
due_date, payment_date, installment_number, notes, metadata, debit_account_id, credit_account_id)
|
|
148
|
-
VALUES (p_id, p_tenant, (p_row ->> 'financial_invoice_id')::uuid, v_dir, v_kind, v_amount,
|
|
149
|
-
round(coalesce((p_row ->> 'paid_amount')::numeric, 0), 2),
|
|
150
|
-
coalesce(p_row ->> 'status', 'pending'),
|
|
151
|
-
-- due_date is NOT NULL on the ledger. A V1 payment has no due date —
|
|
152
|
-
-- it is not an obligation — so it takes the date the money moved, which
|
|
153
|
-
-- is the only date that means anything for it. A bill keeps its own.
|
|
154
|
-
coalesce((p_row ->> 'due_date')::date, (p_row ->> 'paid_at')::date, current_date),
|
|
155
|
-
(p_row ->> 'paid_at')::date,
|
|
156
|
-
(p_row ->> 'installment_number')::integer, p_row ->> 'notes', v_meta, v_debit, v_credit);
|
|
157
|
-
|
|
158
|
-
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_movements', p_id, jsonb_build_object('reason', 'created')); END IF;
|
|
159
|
-
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');
|
|
160
|
-
END $$;
|
|
161
|
-
|
|
162
|
-
DO $$
|
|
163
|
-
DECLARE r record;
|
|
164
|
-
BEGIN
|
|
165
|
-
FOR r IN SELECT p.oid::regprocedure AS sig FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
166
|
-
WHERE n.nspname = 'migration' AND p.proname = 'upsert_fin_movement' LOOP
|
|
167
|
-
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM public, anon, authenticated', r.sig);
|
|
168
|
-
EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', r.sig);
|
|
169
|
-
END LOOP;
|
|
170
|
-
END $$;
|