ekwo-os 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +873 -0
- package/dist/args.d.ts +57 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +134 -0
- package/dist/args.js.map +1 -0
- package/dist/assets/expected-objects.json +9432 -0
- package/dist/assets/migrations/20260911120000_core_companies.sql +260 -0
- package/dist/assets/migrations/20260911120100_accounts_journals.sql +211 -0
- package/dist/assets/migrations/20260911120200_contacts.sql +90 -0
- package/dist/assets/migrations/20260911120300_taxes.sql +146 -0
- package/dist/assets/migrations/20260911120400_entries.sql +338 -0
- package/dist/assets/migrations/20260911120500_documents.sql +279 -0
- package/dist/assets/migrations/20260911120600_payments_reconciliation.sql +251 -0
- package/dist/assets/migrations/20260911120700_bank.sql +178 -0
- package/dist/assets/migrations/20260911120800_analytics_attachments.sql +128 -0
- package/dist/assets/migrations/20260911120900_post_document.sql +279 -0
- package/dist/assets/migrations/20260911121000_reporting.sql +235 -0
- package/dist/assets/migrations/20260911121100_country_templates.sql +226 -0
- package/dist/assets/migrations/20260911121200_fec.sql +71 -0
- package/dist/assets/migrations/20260911130000_instance.sql +77 -0
- package/dist/assets/migrations/20260911130100_instance_members.sql +224 -0
- package/dist/assets/migrations/20260911140000_instance_admins.sql +112 -0
- package/dist/assets/migrations/20260911160000_tax_posting_templates_unique.sql +17 -0
- package/dist/assets/migrations/20260911170000_document_amount_paid.sql +82 -0
- package/dist/assets/migrations/20260911173000_post_payment.sql +122 -0
- package/dist/assets/migrations/20260911173100_sequence_counters_under_rls.sql +84 -0
- package/dist/assets/migrations/20260911183000_country_journal_defaults.sql +143 -0
- package/dist/assets/migrations/20260911193853_line_account_defaults.sql +274 -0
- package/dist/assets/migrations/20260911195054_products.sql +221 -0
- package/dist/assets/migrations/20260911210131_anon_surface.sql +85 -0
- package/dist/assets/migrations/20260912074712_country_packs.sql +318 -0
- package/dist/assets/migrations/20260912080311_report_code_and_region.sql +198 -0
- package/dist/assets/migrations/20260912081014_pack_certification_maintained.sql +31 -0
- package/dist/assets/migrations/20260912081015_pack_certification_backfill.sql +16 -0
- package/dist/assets/migrations/20260912090407_tax_report_boxes.sql +329 -0
- package/dist/assets/migrations/20260912091917_tax_on_base_value.sql +25 -0
- package/dist/assets/migrations/20260912091918_tax_engine_columns.sql +776 -0
- package/dist/assets/migrations/20260912094412_opening_and_closing.sql +759 -0
- package/dist/assets/migrations/20260912095825_charts_of_accounts.sql +326 -0
- package/dist/assets/migrations/20260912100412_financial_statements.sql +632 -0
- package/dist/assets/migrations/20260912104719_one_formula_evaluator.sql +209 -0
- package/dist/assets/migrations/20260912105720_entry_kind_appropriation.sql +26 -0
- package/dist/assets/migrations/20260912105721_appropriation_entry_kind.sql +444 -0
- package/dist/assets/migrations/20260912111751_document_rules.sql +274 -0
- package/dist/assets/migrations/20260912112132_cash_basis_vat_and_fx.sql +1189 -0
- package/dist/assets/migrations/20260913074512_modules.sql +493 -0
- package/dist/assets/migrations/20260913075903_asset_disposal_roles.sql +47 -0
- package/dist/assets/migrations/20260913083216_capabilities.sql +637 -0
- package/dist/assets/migrations/20260913083901_company_invitations.sql +262 -0
- package/dist/assets/migrations/20260913084402_user_preferences.sql +376 -0
- package/dist/assets/migrations/20260913084847_company_profile.sql +261 -0
- package/dist/assets/migrations/20260913085436_numbering_reads_the_pack.sql +235 -0
- package/dist/assets/migrations/20260913085932_api_keys.sql +303 -0
- package/dist/assets/migrations/20260913090216_fiscal_year_bounds.sql +159 -0
- package/dist/assets/migrations/20260913092527_entries_import.sql +219 -0
- package/dist/assets/migrations/20260913101536_null_safe_guards.sql +188 -0
- package/dist/assets/migrations/20260913102115_installer_is_named.sql +462 -0
- package/dist/assets/migrations/20260913102758_no_currency_default.sql +138 -0
- package/dist/assets/migrations/20260913103355_cash_basis_needs_a_box.sql +453 -0
- package/dist/assets/migrations/20260913104014_aged_balance_named_group.sql +79 -0
- package/dist/assets/migrations/20260913104232_foreign_key_indexes.sql +144 -0
- package/dist/assets/migrations/20260913105120_declared_no_reader.sql +41 -0
- package/dist/assets/migrations/20260913111407_pack_languages.sql +223 -0
- package/dist/assets/migrations/20260913112233_schema_comments_for_a_reader.sql +27 -0
- package/dist/assets/migrations/20260913114535_fec_opening_balances.sql +307 -0
- package/dist/assets/migrations/20260914103412_audit_log.sql +509 -0
- package/dist/assets/migrations/20260914111907_pack_upgrade.sql +428 -0
- package/dist/assets/migrations/20260914120500_rounding_reads_the_currency.sql +206 -0
- package/dist/assets/migrations/20260914121200_amounts_round_at_the_currency.sql +2403 -0
- package/dist/assets/migrations/20260914134325_schema_version_0_2_0.sql +36 -0
- package/dist/assets/migrations/20260914143915_accounts_in_use.sql +428 -0
- package/dist/assets/migrations/20260914144731_account_code_frozen.sql +99 -0
- package/dist/assets/migrations/20260914151207_schema_grants_its_own_rights.sql +284 -0
- package/dist/assets/migrations/20260914152840_pack_upgrade_records_its_own_line.sql +237 -0
- package/dist/assets/migrations/20260914163943_declaration_periodicity.sql +349 -0
- package/dist/assets/migrations/20260915094000_foreign_services_received.sql +37 -0
- package/dist/assets/migrations/20260915094500_mentions_read_a_foreign_service.sql +72 -0
- package/dist/assets/migrations/20260915153000_document_shares.sql +568 -0
- package/dist/assets/migrations/20260915160000_ec_sales_list.sql +181 -0
- package/dist/assets/migrations/20260915161842_pack_sources.sql +56 -0
- package/dist/assets/migrations/20260915170500_schema_version_0_3_0.sql +45 -0
- package/dist/assets/migrations/20260915174500_a_vat_category_is_a_code.sql +177 -0
- package/dist/assets/migrations/20260915180000_document_rule_references.sql +85 -0
- package/dist/assets/migrations/20260915181000_territories.sql +342 -0
- package/dist/assets/migrations/20260915181500_ec_sales_list_reads_the_territories.sql +222 -0
- package/dist/assets/migrations/20260915182000_intracom_triangular.sql +53 -0
- package/dist/assets/migrations/20260915182500_a_triangular_supply_says_reverse_charge.sql +99 -0
- package/dist/assets/migrations/20260915191200_a_document_knows_its_language.sql +633 -0
- package/dist/assets/migrations/20260915195000_a_price_that_holds_its_tax.sql +649 -0
- package/dist/assets/migrations/20260915200000_a_posting_names_its_boxes.sql +1004 -0
- package/dist/assets/migrations/20260916094500_a_company_files_more_than_one_declaration.sql +795 -0
- package/dist/assets/migrations/20260916103000_a_ledger_line_names_its_posting.sql +902 -0
- package/dist/assets/migrations/20260916123000_a_vocabulary_for_a_tax_that_is_not_a_vat.sql +144 -0
- package/dist/assets/migrations/20260916124000_a_box_can_be_a_rate_of_a_box.sql +525 -0
- package/dist/assets/migrations/20260916125000_a_tax_follows_the_territory.sql +1318 -0
- package/dist/assets/migrations/20260916126000_a_tax_point_and_its_exception.sql +1211 -0
- package/dist/assets/migrations/20260917090000_a_counterparty_that_learns.sql +561 -0
- package/dist/assets/migrations/20260917120000_what_the_money_pays.sql +545 -0
- package/dist/assets/migrations/20260917150000_a_filing_is_a_row.sql +488 -0
- package/dist/assets/migrations/20260917170000_when_it_is_due.sql +194 -0
- package/dist/assets/migrations/20260917180000_a_box_is_a_number_and_a_kind.sql +213 -0
- package/dist/assets/migrations/20260917190000_what_a_declaration_owes.sql +486 -0
- package/dist/assets/migrations/20260917200000_what_moved_after_it_went.sql +227 -0
- package/dist/assets/migrations/20260918090000_a_form_names_its_file.sql +27 -0
- package/dist/assets/migrations/20260918110000_a_deposit_and_what_came_back.sql +286 -0
- package/dist/assets/migrations/20260918113741_a_fourth_preset_is_named.sql +9 -0
- package/dist/assets/migrations/20260918113807_a_client_reads_and_hands_over.sql +96 -0
- package/dist/assets/migrations/20260918114322_two_definer_functions_check_their_caller.sql +75 -0
- package/dist/assets/migrations/20260918130000_every_company_somebody_keeps.sql +177 -0
- package/dist/assets/migrations/20260918140000_a_session_nobody_prepared.sql +40 -0
- package/dist/assets/migrations/20260918141107_a_line_keeps_the_tax_it_was_posted_with.sql +256 -0
- package/dist/assets/migrations/20260918141342_a_company_has_an_electronic_address.sql +66 -0
- package/dist/assets/migrations/20260918141605_an_invoice_reads_whole_from_the_views.sql +313 -0
- package/dist/assets/migrations/20260918141627_a_policy_asks_once.sql +132 -0
- package/dist/assets/migrations/20260918143352_a_suggestion_reads_the_words_once.sql +204 -0
- package/dist/assets/migrations/20260918143417_a_reference_of_the_caller_and_a_rehearsal.sql +115 -0
- package/dist/assets/migrations/20260918150712_a_company_leaves_with_its_books.sql +1119 -0
- package/dist/assets/migrations/20260918150931_a_statement_is_imported_once.sql +654 -0
- package/dist/assets/migrations/20260918161204_a_posted_document_does_not_move.sql +437 -0
- package/dist/assets/migrations/20260918161538_a_posted_entry_does_not_move.sql +252 -0
- package/dist/assets/migrations/20260918171946_an_entry_is_posted_by_post_entry.sql +289 -0
- package/dist/assets/migrations/20260918174312_schema_version_0_4_0.sql +53 -0
- package/dist/assets/modules/assets/module.json +13 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913081447_assets.sql +1190 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104234_foreign_key_indexes.sql +47 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104500_assets_capabilities.sql +145 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914122100_assets_round_at_the_currency.sql +564 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914145012_assets_accounts_in_use.sql +37 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914151530_assets_grants_its_own_rights.sql +63 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918113912_assets_client_reads.sql +20 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918150801_assets_leave_with_the_company.sql +41 -0
- package/dist/assets/modules/budgets/module.json +12 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913083012_budgets.sql +210 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104233_foreign_key_indexes.sql +37 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104501_budgets_capabilities.sql +72 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914122400_budgets_round_at_the_currency.sql +68 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914145048_budgets_accounts_in_use.sql +30 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914151812_budgets_grants_its_own_rights.sql +31 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918113954_budgets_client_reads.sql +20 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918150823_budgets_leave_with_the_company.sql +37 -0
- package/dist/assets/modules/schema/module.1.json +69 -0
- package/dist/assets/seed/00_currencies.sql +15 -0
- package/dist/assets/seed/00_territories.sql +280 -0
- package/dist/assets/seed/05_framework_generic.sql +110 -0
- package/dist/assets/seed/10_pack_be.sql +1284 -0
- package/dist/assets/seed/11_pack_fr.sql +1112 -0
- package/dist/assets/seed/12_pack_lu.sql +2484 -0
- package/dist/assets/seed/13_pack_ee.sql +733 -0
- package/dist/assets/seed/14_pack_gb.sql +796 -0
- package/dist/assets/seed/15_pack_us.sql +751 -0
- package/dist/assets/seed/90_demo_company.sql +368 -0
- package/dist/assets/seed/modules/assets/10_pack_be.sql +58 -0
- package/dist/assets/seed/modules/assets/11_pack_fr.sql +57 -0
- package/dist/assets/seed/modules/assets/14_pack_gb.sql +58 -0
- package/dist/assets/seed/modules/assets/15_pack_us.sql +60 -0
- package/dist/auth.d.ts +59 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +134 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +8 -0
- package/dist/bin.js.map +1 -0
- package/dist/books.d.ts +60 -0
- package/dist/books.d.ts.map +1 -0
- package/dist/books.js +168 -0
- package/dist/books.js.map +1 -0
- package/dist/bootstrap.d.ts +236 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +459 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bundle.d.ts +27 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +47 -0
- package/dist/bundle.js.map +1 -0
- package/dist/checklist.d.ts +35 -0
- package/dist/checklist.d.ts.map +1 -0
- package/dist/checklist.js +64 -0
- package/dist/checklist.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +318 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/company.d.ts +59 -0
- package/dist/commands/company.d.ts.map +1 -0
- package/dist/commands/company.js +287 -0
- package/dist/commands/company.js.map +1 -0
- package/dist/commands/contact.d.ts +9 -0
- package/dist/commands/contact.d.ts.map +1 -0
- package/dist/commands/contact.js +82 -0
- package/dist/commands/contact.js.map +1 -0
- package/dist/commands/demo.d.ts +19 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +72 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +68 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/document.d.ts +17 -0
- package/dist/commands/document.d.ts.map +1 -0
- package/dist/commands/document.js +217 -0
- package/dist/commands/document.js.map +1 -0
- package/dist/commands/init.d.ts +31 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +535 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +24 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +128 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +115 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/module.d.ts +32 -0
- package/dist/commands/module.d.ts.map +1 -0
- package/dist/commands/module.js +285 -0
- package/dist/commands/module.js.map +1 -0
- package/dist/commands/pack.d.ts +22 -0
- package/dist/commands/pack.d.ts.map +1 -0
- package/dist/commands/pack.js +481 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/payment.d.ts +15 -0
- package/dist/commands/payment.d.ts.map +1 -0
- package/dist/commands/payment.js +97 -0
- package/dist/commands/payment.js.map +1 -0
- package/dist/commands/register.d.ts +20 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +134 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +134 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/whoami.d.ts +21 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +133 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/company.d.ts +16 -0
- package/dist/company.d.ts.map +1 -0
- package/dist/company.js +26 -0
- package/dist/company.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/connection.d.ts +79 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +116 -0
- package/dist/connection.js.map +1 -0
- package/dist/context.d.ts +47 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +93 -0
- package/dist/context.js.map +1 -0
- package/dist/doctor.d.ts +45 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +448 -0
- package/dist/doctor.js.map +1 -0
- package/dist/grants.d.ts +88 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +174 -0
- package/dist/grants.js.map +1 -0
- package/dist/identity.d.ts +54 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +132 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +155 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +303 -0
- package/dist/inventory.js.map +1 -0
- package/dist/migrations.d.ts +76 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +226 -0
- package/dist/migrations.js.map +1 -0
- package/dist/module/read.d.ts +91 -0
- package/dist/module/read.d.ts.map +1 -0
- package/dist/module/read.js +172 -0
- package/dist/module/read.js.map +1 -0
- package/dist/output.d.ts +110 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +173 -0
- package/dist/output.js.map +1 -0
- package/dist/pack/certification.d.ts +27 -0
- package/dist/pack/certification.d.ts.map +1 -0
- package/dist/pack/certification.js +38 -0
- package/dist/pack/certification.js.map +1 -0
- package/dist/pack/compile.d.ts +75 -0
- package/dist/pack/compile.d.ts.map +1 -0
- package/dist/pack/compile.js +801 -0
- package/dist/pack/compile.js.map +1 -0
- package/dist/pack/filing.d.ts +48 -0
- package/dist/pack/filing.d.ts.map +1 -0
- package/dist/pack/filing.js +56 -0
- package/dist/pack/filing.js.map +1 -0
- package/dist/pack/read.d.ts +766 -0
- package/dist/pack/read.d.ts.map +1 -0
- package/dist/pack/read.js +2336 -0
- package/dist/pack/read.js.map +1 -0
- package/dist/pack/schema.d.ts +26 -0
- package/dist/pack/schema.d.ts.map +1 -0
- package/dist/pack/schema.js +181 -0
- package/dist/pack/schema.js.map +1 -0
- package/dist/pack/territories.d.ts +98 -0
- package/dist/pack/territories.d.ts.map +1 -0
- package/dist/pack/territories.js +0 -0
- package/dist/pack/territories.js.map +1 -0
- package/dist/pack/upgrade.d.ts +86 -0
- package/dist/pack/upgrade.d.ts.map +1 -0
- package/dist/pack/upgrade.js +76 -0
- package/dist/pack/upgrade.js.map +1 -0
- package/dist/pack/vat-codes.d.ts +273 -0
- package/dist/pack/vat-codes.d.ts.map +1 -0
- package/dist/pack/vat-codes.js +513 -0
- package/dist/pack/vat-codes.js.map +1 -0
- package/dist/profiles.d.ts +67 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +187 -0
- package/dist/profiles.js.map +1 -0
- package/dist/prompt.d.ts +40 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +120 -0
- package/dist/prompt.js.map +1 -0
- package/dist/registry.d.ts +84 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +104 -0
- package/dist/registry.js.map +1 -0
- package/dist/rest.d.ts +81 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +245 -0
- package/dist/rest.js.map +1 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +15 -0
- package/dist/schema.js.map +1 -0
- package/dist/seeds.d.ts +27 -0
- package/dist/seeds.d.ts.map +1 -0
- package/dist/seeds.js +47 -0
- package/dist/seeds.js.map +1 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +92 -0
- package/dist/session.js.map +1 -0
- package/dist/sql.d.ts +48 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +112 -0
- package/dist/sql.js.map +1 -0
- package/dist/status.d.ts +94 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +120 -0
- package/dist/status.js.map +1 -0
- package/dist/ui.d.ts +55 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +114 -0
- package/dist/ui.js.map +1 -0
- package/package.json +29 -0
- package/schema/output.1.json +613 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
-- Ekwo OS — the instance-level role.
|
|
2
|
+
--
|
|
3
|
+
-- `member_role` is now one vocabulary for the whole installation:
|
|
4
|
+
-- `instance_admin` at instance level, `owner` / `accountant` / `viewer` per
|
|
5
|
+
-- company. The storage is split because the keys differ — a company role is
|
|
6
|
+
-- keyed by (company, user), an instance role by user alone — and a nullable
|
|
7
|
+
-- company in `company_members` would break its primary key and its meaning.
|
|
8
|
+
-- Two check constraints keep each table to the roles it may carry.
|
|
9
|
+
--
|
|
10
|
+
-- Users live in the customer's own Supabase Auth. `user_id` holds an
|
|
11
|
+
-- `auth.users.id` from their project; Ekwo never holds an account.
|
|
12
|
+
|
|
13
|
+
create table instance_members (
|
|
14
|
+
user_id uuid primary key,
|
|
15
|
+
role member_role not null default 'instance_admin',
|
|
16
|
+
created_at timestamptz not null default now(),
|
|
17
|
+
updated_at timestamptz not null default now(),
|
|
18
|
+
-- The only instance-level role today. Widening this is a deliberate act.
|
|
19
|
+
constraint instance_members_role_is_instance_level check (role = 'instance_admin')
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
comment on table instance_members is
|
|
23
|
+
'Instance administrators: they create companies and invite members. user_id is an auth.users id from the customer''s own Supabase project.';
|
|
24
|
+
comment on column instance_members.user_id is
|
|
25
|
+
'An auth.users.id in the customer''s Supabase Auth. No foreign key, so the schema installs on a plain Postgres and seeds never write into auth.';
|
|
26
|
+
|
|
27
|
+
create trigger instance_members_set_updated_at
|
|
28
|
+
before update on instance_members
|
|
29
|
+
for each row execute function set_updated_at();
|
|
30
|
+
|
|
31
|
+
-- A company role is a company role.
|
|
32
|
+
alter table company_members
|
|
33
|
+
add constraint company_members_role_is_company_level check (role <> 'instance_admin');
|
|
34
|
+
|
|
35
|
+
-- ---------------------------------------------------------------------------
|
|
36
|
+
-- Helpers
|
|
37
|
+
-- ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
-- SECURITY DEFINER so the policy on instance_members does not recurse into
|
|
40
|
+
-- the table it protects.
|
|
41
|
+
create or replace function is_instance_admin()
|
|
42
|
+
returns boolean
|
|
43
|
+
language sql
|
|
44
|
+
stable
|
|
45
|
+
security definer
|
|
46
|
+
set search_path = public, pg_temp
|
|
47
|
+
as $$
|
|
48
|
+
select exists (
|
|
49
|
+
select 1 from instance_members m
|
|
50
|
+
where m.user_id = auth.uid() and m.role = 'instance_admin'
|
|
51
|
+
);
|
|
52
|
+
$$;
|
|
53
|
+
|
|
54
|
+
comment on function is_instance_admin() is
|
|
55
|
+
'Whether the current user administers this installation.';
|
|
56
|
+
|
|
57
|
+
create or replace function instance_has_no_admin()
|
|
58
|
+
returns boolean
|
|
59
|
+
language sql
|
|
60
|
+
stable
|
|
61
|
+
security definer
|
|
62
|
+
set search_path = public, pg_temp
|
|
63
|
+
as $$
|
|
64
|
+
select not exists (select 1 from instance_members);
|
|
65
|
+
$$;
|
|
66
|
+
|
|
67
|
+
-- ---------------------------------------------------------------------------
|
|
68
|
+
-- Installation
|
|
69
|
+
-- ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
create or replace function init_instance(
|
|
72
|
+
p_organization_name text,
|
|
73
|
+
p_country char(2),
|
|
74
|
+
p_edition instance_edition default 'community'
|
|
75
|
+
)
|
|
76
|
+
returns instance
|
|
77
|
+
language plpgsql
|
|
78
|
+
as $$
|
|
79
|
+
declare
|
|
80
|
+
v_row instance%rowtype;
|
|
81
|
+
begin
|
|
82
|
+
if exists (select 1 from instance) then
|
|
83
|
+
raise exception 'instance_already_initialised: this installation is already set up';
|
|
84
|
+
end if;
|
|
85
|
+
|
|
86
|
+
insert into instance (id, organization_name, country, edition, schema_version)
|
|
87
|
+
values (1, p_organization_name, upper(p_country), p_edition, ekwo_schema_version())
|
|
88
|
+
returning * into v_row;
|
|
89
|
+
|
|
90
|
+
return v_row;
|
|
91
|
+
end;
|
|
92
|
+
$$;
|
|
93
|
+
|
|
94
|
+
comment on function init_instance(text, char, instance_edition) is
|
|
95
|
+
'Records the installation. Called once, by the installer. Leaves the registration fields empty.';
|
|
96
|
+
|
|
97
|
+
-- The first user to ask takes the instance. After that, only an existing
|
|
98
|
+
-- administrator can appoint another.
|
|
99
|
+
create or replace function claim_instance_admin(p_user_id uuid default auth.uid())
|
|
100
|
+
returns instance_members
|
|
101
|
+
language plpgsql
|
|
102
|
+
as $$
|
|
103
|
+
declare
|
|
104
|
+
v_row instance_members%rowtype;
|
|
105
|
+
begin
|
|
106
|
+
if p_user_id is null then
|
|
107
|
+
raise exception 'no_user: claim_instance_admin needs a signed-in user or an explicit id';
|
|
108
|
+
end if;
|
|
109
|
+
|
|
110
|
+
if exists (select 1 from instance_members) and not is_instance_admin() then
|
|
111
|
+
raise exception 'instance_already_claimed: only an instance administrator can appoint another';
|
|
112
|
+
end if;
|
|
113
|
+
|
|
114
|
+
insert into instance_members (user_id, role)
|
|
115
|
+
values (p_user_id, 'instance_admin')
|
|
116
|
+
on conflict (user_id) do update set role = 'instance_admin'
|
|
117
|
+
returning * into v_row;
|
|
118
|
+
|
|
119
|
+
return v_row;
|
|
120
|
+
end;
|
|
121
|
+
$$;
|
|
122
|
+
|
|
123
|
+
comment on function claim_instance_admin(uuid) is
|
|
124
|
+
'Makes a user an instance administrator. The first claim is open; afterwards only an administrator may appoint one.';
|
|
125
|
+
|
|
126
|
+
-- Opt-in registration with Ekwo. Nothing calls this on its own.
|
|
127
|
+
create or replace function register_instance(p_contact_email text)
|
|
128
|
+
returns instance
|
|
129
|
+
language plpgsql
|
|
130
|
+
as $$
|
|
131
|
+
declare
|
|
132
|
+
v_row instance%rowtype;
|
|
133
|
+
begin
|
|
134
|
+
if not is_instance_admin() then
|
|
135
|
+
raise exception 'not_instance_admin: only an instance administrator can register this installation';
|
|
136
|
+
end if;
|
|
137
|
+
|
|
138
|
+
update instance
|
|
139
|
+
set contact_email = p_contact_email,
|
|
140
|
+
registered_at = now()
|
|
141
|
+
where id = 1
|
|
142
|
+
returning * into v_row;
|
|
143
|
+
|
|
144
|
+
if not found then
|
|
145
|
+
raise exception 'instance_not_initialised: call init_instance() first';
|
|
146
|
+
end if;
|
|
147
|
+
|
|
148
|
+
return v_row;
|
|
149
|
+
end;
|
|
150
|
+
$$;
|
|
151
|
+
|
|
152
|
+
comment on function register_instance(text) is
|
|
153
|
+
'Opt-in: records an address and a date so Ekwo can reach the operator. Never required, and reversible with unregister_instance().';
|
|
154
|
+
|
|
155
|
+
create or replace function unregister_instance()
|
|
156
|
+
returns instance
|
|
157
|
+
language plpgsql
|
|
158
|
+
as $$
|
|
159
|
+
declare
|
|
160
|
+
v_row instance%rowtype;
|
|
161
|
+
begin
|
|
162
|
+
if not is_instance_admin() then
|
|
163
|
+
raise exception 'not_instance_admin: only an instance administrator can unregister this installation';
|
|
164
|
+
end if;
|
|
165
|
+
|
|
166
|
+
update instance set contact_email = null, registered_at = null where id = 1
|
|
167
|
+
returning * into v_row;
|
|
168
|
+
|
|
169
|
+
return v_row;
|
|
170
|
+
end;
|
|
171
|
+
$$;
|
|
172
|
+
|
|
173
|
+
comment on function unregister_instance() is
|
|
174
|
+
'Undoes register_instance(). Opting in is reversible, or it is not a choice.';
|
|
175
|
+
|
|
176
|
+
-- ---------------------------------------------------------------------------
|
|
177
|
+
-- Row level security
|
|
178
|
+
-- ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
alter table instance_members enable row level security;
|
|
181
|
+
|
|
182
|
+
create policy instance_members_select on instance_members
|
|
183
|
+
for select using (auth.uid() is not null);
|
|
184
|
+
create policy instance_members_insert on instance_members
|
|
185
|
+
for insert with check (is_instance_admin() or instance_has_no_admin());
|
|
186
|
+
create policy instance_members_update on instance_members
|
|
187
|
+
for update using (is_instance_admin()) with check (is_instance_admin());
|
|
188
|
+
create policy instance_members_delete on instance_members
|
|
189
|
+
for delete using (is_instance_admin());
|
|
190
|
+
|
|
191
|
+
create policy instance_write on instance
|
|
192
|
+
for all using (is_instance_admin()) with check (is_instance_admin());
|
|
193
|
+
|
|
194
|
+
-- Creating a company is an instance-level act. The select policy has to
|
|
195
|
+
-- follow: `insert ... returning` is checked against it too, so without this
|
|
196
|
+
-- an administrator would create a company and still get an error back.
|
|
197
|
+
drop policy companies_insert on companies;
|
|
198
|
+
create policy companies_insert on companies
|
|
199
|
+
for insert with check (is_instance_admin());
|
|
200
|
+
|
|
201
|
+
drop policy companies_select on companies;
|
|
202
|
+
create policy companies_select on companies
|
|
203
|
+
for select using (is_company_member(id) or is_instance_admin());
|
|
204
|
+
|
|
205
|
+
-- An instance administrator may invite members into any company.
|
|
206
|
+
drop policy company_members_insert on company_members;
|
|
207
|
+
create policy company_members_insert on company_members
|
|
208
|
+
for insert with check (
|
|
209
|
+
is_company_owner(company_id)
|
|
210
|
+
or is_instance_admin()
|
|
211
|
+
or company_has_no_member(company_id)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
drop policy company_members_select on company_members;
|
|
215
|
+
create policy company_members_select on company_members
|
|
216
|
+
for select using (
|
|
217
|
+
user_id = auth.uid() or is_company_member(company_id) or is_instance_admin()
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
-- ... and an instance administrator may administer a company they are not in.
|
|
221
|
+
drop policy companies_update on companies;
|
|
222
|
+
create policy companies_update on companies
|
|
223
|
+
for update using (is_company_owner(id) or is_instance_admin())
|
|
224
|
+
with check (is_company_owner(id) or is_instance_admin());
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
-- Ekwo OS — instance administrators, final shape.
|
|
2
|
+
--
|
|
3
|
+
-- Three changes on top of the previous migration, additive as always:
|
|
4
|
+
--
|
|
5
|
+
-- 1. `instance_members` becomes `instance_admins`. The table name carries
|
|
6
|
+
-- the role, so the `role` column carried nothing.
|
|
7
|
+
-- 2. `user_id` gets a real foreign key to `auth.users`. An administrator is
|
|
8
|
+
-- necessarily a signed-in user, so there is no invite-before-signup case
|
|
9
|
+
-- to accommodate here. `company_members` deliberately keeps no such key:
|
|
10
|
+
-- inviting someone into a company before they have an account is a
|
|
11
|
+
-- normal thing to want.
|
|
12
|
+
-- 3. Reading the instance row is for people who are actually on this
|
|
13
|
+
-- installation — a member of at least one company, or an administrator —
|
|
14
|
+
-- rather than anyone holding a token.
|
|
15
|
+
|
|
16
|
+
alter table instance_members rename to instance_admins;
|
|
17
|
+
|
|
18
|
+
alter policy instance_members_select on instance_admins rename to instance_admins_select;
|
|
19
|
+
alter policy instance_members_insert on instance_admins rename to instance_admins_insert;
|
|
20
|
+
alter policy instance_members_update on instance_admins rename to instance_admins_update;
|
|
21
|
+
alter policy instance_members_delete on instance_admins rename to instance_admins_delete;
|
|
22
|
+
|
|
23
|
+
alter table instance_admins drop constraint instance_members_role_is_instance_level;
|
|
24
|
+
alter table instance_admins drop column role;
|
|
25
|
+
|
|
26
|
+
alter table instance_admins
|
|
27
|
+
add constraint instance_admins_user_fkey
|
|
28
|
+
foreign key (user_id) references auth.users(id) on delete cascade;
|
|
29
|
+
|
|
30
|
+
comment on table instance_admins is
|
|
31
|
+
'Instance administrators: they create companies and invite members. One row per user, keyed on auth.users of the customer''s own Supabase project.';
|
|
32
|
+
comment on column instance_admins.user_id is
|
|
33
|
+
'An auth.users.id in the customer''s Supabase Auth. Ekwo holds no account and no directory.';
|
|
34
|
+
|
|
35
|
+
-- ---------------------------------------------------------------------------
|
|
36
|
+
-- Helpers, against the renamed table
|
|
37
|
+
-- ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
create or replace function is_instance_admin()
|
|
40
|
+
returns boolean
|
|
41
|
+
language sql
|
|
42
|
+
stable
|
|
43
|
+
security definer
|
|
44
|
+
set search_path = public, pg_temp
|
|
45
|
+
as $$
|
|
46
|
+
select exists (select 1 from instance_admins a where a.user_id = auth.uid());
|
|
47
|
+
$$;
|
|
48
|
+
|
|
49
|
+
create or replace function instance_has_no_admin()
|
|
50
|
+
returns boolean
|
|
51
|
+
language sql
|
|
52
|
+
stable
|
|
53
|
+
security definer
|
|
54
|
+
set search_path = public, pg_temp
|
|
55
|
+
as $$
|
|
56
|
+
select not exists (select 1 from instance_admins);
|
|
57
|
+
$$;
|
|
58
|
+
|
|
59
|
+
-- Whether the current user is on the books of at least one company here.
|
|
60
|
+
create or replace function is_any_company_member()
|
|
61
|
+
returns boolean
|
|
62
|
+
language sql
|
|
63
|
+
stable
|
|
64
|
+
security definer
|
|
65
|
+
set search_path = public, pg_temp
|
|
66
|
+
as $$
|
|
67
|
+
select exists (select 1 from company_members m where m.user_id = auth.uid());
|
|
68
|
+
$$;
|
|
69
|
+
|
|
70
|
+
comment on function is_any_company_member() is
|
|
71
|
+
'Whether the current user belongs to at least one company of this installation.';
|
|
72
|
+
|
|
73
|
+
drop function if exists claim_instance_admin(uuid);
|
|
74
|
+
|
|
75
|
+
create or replace function claim_instance_admin(p_user_id uuid default auth.uid())
|
|
76
|
+
returns instance_admins
|
|
77
|
+
language plpgsql
|
|
78
|
+
as $$
|
|
79
|
+
declare
|
|
80
|
+
v_row instance_admins%rowtype;
|
|
81
|
+
begin
|
|
82
|
+
if p_user_id is null then
|
|
83
|
+
raise exception 'no_user: claim_instance_admin needs a signed-in user or an explicit id';
|
|
84
|
+
end if;
|
|
85
|
+
|
|
86
|
+
if exists (select 1 from instance_admins) and not is_instance_admin() then
|
|
87
|
+
raise exception 'instance_already_claimed: only an instance administrator can appoint another';
|
|
88
|
+
end if;
|
|
89
|
+
|
|
90
|
+
insert into instance_admins (user_id)
|
|
91
|
+
values (p_user_id)
|
|
92
|
+
on conflict (user_id) do nothing
|
|
93
|
+
returning * into v_row;
|
|
94
|
+
|
|
95
|
+
if v_row.user_id is null then
|
|
96
|
+
select * into v_row from instance_admins where user_id = p_user_id;
|
|
97
|
+
end if;
|
|
98
|
+
|
|
99
|
+
return v_row;
|
|
100
|
+
end;
|
|
101
|
+
$$;
|
|
102
|
+
|
|
103
|
+
comment on function claim_instance_admin(uuid) is
|
|
104
|
+
'Makes a user an instance administrator. The first claim is open; afterwards only an administrator may appoint one.';
|
|
105
|
+
|
|
106
|
+
-- ---------------------------------------------------------------------------
|
|
107
|
+
-- Reading the instance row
|
|
108
|
+
-- ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
drop policy instance_select on instance;
|
|
111
|
+
create policy instance_select on instance
|
|
112
|
+
for select using (is_any_company_member() or is_instance_admin());
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- Ekwo OS — one natural key for tax posting templates.
|
|
2
|
+
--
|
|
3
|
+
-- `tax_posting_templates` had a surrogate primary key and a partial unique
|
|
4
|
+
-- index on the base line only, so re-applying a seed inserted every tax line
|
|
5
|
+
-- a second time and then failed on the base. Found on the first real
|
|
6
|
+
-- installation, when `ekwo init` was run twice on the same project.
|
|
7
|
+
--
|
|
8
|
+
-- A posting template is identified by the tax, the kind of document, the
|
|
9
|
+
-- posting type and its sequence. Making that a unique index gives the seeds a
|
|
10
|
+
-- conflict target and makes them idempotent, which `supabase/seed/README.md`
|
|
11
|
+
-- had promised all along.
|
|
12
|
+
|
|
13
|
+
create unique index tax_posting_templates_natural_key_idx
|
|
14
|
+
on tax_posting_templates (tax_template_id, document_kind, posting_type, sequence);
|
|
15
|
+
|
|
16
|
+
comment on index tax_posting_templates_natural_key_idx is
|
|
17
|
+
'The natural key of a posting template; the conflict target of the seeds.';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
-- Ekwo OS — a document's settlement is derived from its matching.
|
|
2
|
+
--
|
|
3
|
+
-- `documents.amount_paid` was a column somebody had to remember to write, while
|
|
4
|
+
-- the truth of what has been settled lived in `reconciliations`. That is the
|
|
5
|
+
-- rule of this schema broken on its own terms: totals are derived, never keyed
|
|
6
|
+
-- in. Matching a third-party line now moves the document, and `payment_state`
|
|
7
|
+
-- follows from there as it already did.
|
|
8
|
+
--
|
|
9
|
+
-- The recomputation is folded into the existing reconciliation trigger rather
|
|
10
|
+
-- than added as a second one, because two AFTER ROW triggers on the same table
|
|
11
|
+
-- fire in name order, and this one has to run after the line residuals have
|
|
12
|
+
-- been updated, not before.
|
|
13
|
+
|
|
14
|
+
create or replace function documents_refresh_amount_paid(p_document_id uuid)
|
|
15
|
+
returns void
|
|
16
|
+
language plpgsql
|
|
17
|
+
as $$
|
|
18
|
+
begin
|
|
19
|
+
-- The statement names amount_paid so that documents_refresh_payment_state,
|
|
20
|
+
-- which watches that column, fires in turn.
|
|
21
|
+
update documents d
|
|
22
|
+
set amount_paid = coalesce((
|
|
23
|
+
select sum(l.matched_amount)
|
|
24
|
+
from entry_lines l
|
|
25
|
+
join accounts a on a.id = l.account_id
|
|
26
|
+
where l.entry_id = d.entry_id
|
|
27
|
+
and a.reconcilable
|
|
28
|
+
and a.account_type in ('asset_receivable', 'liability_payable')
|
|
29
|
+
), 0)
|
|
30
|
+
where d.id = p_document_id
|
|
31
|
+
and d.entry_id is not null;
|
|
32
|
+
end;
|
|
33
|
+
$$;
|
|
34
|
+
|
|
35
|
+
comment on function documents_refresh_amount_paid(uuid) is
|
|
36
|
+
'Recomputes what a document has been settled by, from the matched amounts on its third-party lines.';
|
|
37
|
+
|
|
38
|
+
comment on column documents.amount_paid is
|
|
39
|
+
'Derived from reconciliations on the third-party lines of the document''s entry. A value written by hand is replaced at the next matching.';
|
|
40
|
+
|
|
41
|
+
create or replace function reconciliations_refresh_lines()
|
|
42
|
+
returns trigger
|
|
43
|
+
language plpgsql
|
|
44
|
+
as $$
|
|
45
|
+
declare
|
|
46
|
+
v_ids uuid[];
|
|
47
|
+
v_id uuid;
|
|
48
|
+
v_doc uuid;
|
|
49
|
+
begin
|
|
50
|
+
v_ids := array_remove(array[
|
|
51
|
+
coalesce(new.debit_line_id, old.debit_line_id),
|
|
52
|
+
coalesce(new.credit_line_id, old.credit_line_id)
|
|
53
|
+
], null);
|
|
54
|
+
|
|
55
|
+
foreach v_id in array v_ids loop
|
|
56
|
+
update entry_lines l
|
|
57
|
+
set matched_amount = coalesce((
|
|
58
|
+
select sum(r.amount) from reconciliations r
|
|
59
|
+
where r.debit_line_id = l.id or r.credit_line_id = l.id
|
|
60
|
+
), 0),
|
|
61
|
+
matching_number = (
|
|
62
|
+
select r.matching_number from reconciliations r
|
|
63
|
+
where r.debit_line_id = l.id or r.credit_line_id = l.id
|
|
64
|
+
order by r.created_at
|
|
65
|
+
limit 1
|
|
66
|
+
)
|
|
67
|
+
where l.id = v_id;
|
|
68
|
+
end loop;
|
|
69
|
+
|
|
70
|
+
-- Then the documents those lines belong to, now that the residuals are right.
|
|
71
|
+
for v_doc in
|
|
72
|
+
select distinct d.id
|
|
73
|
+
from entry_lines l
|
|
74
|
+
join documents d on d.entry_id = l.entry_id
|
|
75
|
+
where l.id = any (v_ids)
|
|
76
|
+
loop
|
|
77
|
+
perform documents_refresh_amount_paid(v_doc);
|
|
78
|
+
end loop;
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
end;
|
|
82
|
+
$$;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
-- Ekwo OS — booking a payment.
|
|
2
|
+
--
|
|
3
|
+
-- `post_document` turned an invoice into an entry from the first release;
|
|
4
|
+
-- money moving had no equivalent, so every client had to assemble the two
|
|
5
|
+
-- ledger lines of a payment itself. That is the one thing a client must never
|
|
6
|
+
-- do: the moment a caller writes `entry_lines`, the rules about sides,
|
|
7
|
+
-- accounts and locks live in whichever client happens to be writing them, and
|
|
8
|
+
-- there are as many answers as there are clients. So the rule moves here,
|
|
9
|
+
-- next to the other one.
|
|
10
|
+
--
|
|
11
|
+
-- What it books, and nothing else:
|
|
12
|
+
--
|
|
13
|
+
-- inbound — money in. debit the bank, credit the receivable.
|
|
14
|
+
-- outbound — money out. credit the bank, debit the payable.
|
|
15
|
+
--
|
|
16
|
+
-- The third-party account is resolved by role, the same way `post_document`
|
|
17
|
+
-- resolves it: the contact's override first, the company default second,
|
|
18
|
+
-- never by matching a code prefix. The bank side comes from the payment's
|
|
19
|
+
-- bank account, or from the default account of its journal.
|
|
20
|
+
--
|
|
21
|
+
-- What it deliberately does not do: match anything. A payment is money
|
|
22
|
+
-- arriving; which invoices it settles is a separate decision, taken by
|
|
23
|
+
-- `reconcile`, and conflating the two is how a payment ends up silently
|
|
24
|
+
-- allocated to the wrong invoice.
|
|
25
|
+
|
|
26
|
+
create or replace function post_payment(p_payment_id uuid)
|
|
27
|
+
returns entries
|
|
28
|
+
language plpgsql
|
|
29
|
+
as $$
|
|
30
|
+
declare
|
|
31
|
+
v_pay payments%rowtype;
|
|
32
|
+
v_entry entries%rowtype;
|
|
33
|
+
v_money uuid;
|
|
34
|
+
v_third uuid;
|
|
35
|
+
v_contact uuid;
|
|
36
|
+
v_inbound boolean;
|
|
37
|
+
v_label text;
|
|
38
|
+
begin
|
|
39
|
+
select * into v_pay from payments where id = p_payment_id for update;
|
|
40
|
+
if not found then
|
|
41
|
+
raise exception 'unknown_payment: payment % does not exist', p_payment_id;
|
|
42
|
+
end if;
|
|
43
|
+
if v_pay.entry_id is not null then
|
|
44
|
+
raise exception 'payment_already_booked: payment % already points at entry %',
|
|
45
|
+
p_payment_id, v_pay.entry_id;
|
|
46
|
+
end if;
|
|
47
|
+
if v_pay.state = 'cancelled' then
|
|
48
|
+
raise exception 'payment_cancelled: payment % cannot be booked', p_payment_id;
|
|
49
|
+
end if;
|
|
50
|
+
|
|
51
|
+
v_inbound := v_pay.direction = 'inbound';
|
|
52
|
+
|
|
53
|
+
-- The bank side: the account behind the payment's bank account, or the
|
|
54
|
+
-- default account of the journal it goes through.
|
|
55
|
+
select b.account_id into v_money
|
|
56
|
+
from bank_accounts b
|
|
57
|
+
where b.id = v_pay.bank_account_id;
|
|
58
|
+
|
|
59
|
+
if v_money is null then
|
|
60
|
+
select coalesce(j.default_account_id, b.account_id) into v_money
|
|
61
|
+
from journals j
|
|
62
|
+
left join bank_accounts b on b.id = j.bank_account_id
|
|
63
|
+
where j.id = v_pay.journal_id;
|
|
64
|
+
end if;
|
|
65
|
+
|
|
66
|
+
-- A bank account wired to the journal from its own side counts too: that is
|
|
67
|
+
-- the direction an operator fills in first.
|
|
68
|
+
if v_money is null then
|
|
69
|
+
select b.account_id into v_money
|
|
70
|
+
from bank_accounts b
|
|
71
|
+
where b.journal_id = v_pay.journal_id
|
|
72
|
+
and b.active
|
|
73
|
+
and b.account_id is not null
|
|
74
|
+
order by b.created_at
|
|
75
|
+
limit 1;
|
|
76
|
+
end if;
|
|
77
|
+
|
|
78
|
+
if v_money is null then
|
|
79
|
+
raise exception 'no_bank_account: set bank_account_id on the payment, or default_account_id on journal %',
|
|
80
|
+
v_pay.journal_id;
|
|
81
|
+
end if;
|
|
82
|
+
|
|
83
|
+
-- The third-party side, by role.
|
|
84
|
+
v_contact := commercial_entity(v_pay.contact_id);
|
|
85
|
+
v_third := resolve_counterpart_account(v_pay.company_id, v_contact, v_inbound);
|
|
86
|
+
|
|
87
|
+
v_label := coalesce(v_pay.reference, v_pay.memo, 'payment');
|
|
88
|
+
|
|
89
|
+
insert into entries (company_id, journal_id, fiscal_year_id, entry_date, reference,
|
|
90
|
+
description, state, currency_code)
|
|
91
|
+
values (v_pay.company_id, v_pay.journal_id,
|
|
92
|
+
fiscal_year_at(v_pay.company_id, v_pay.payment_date), v_pay.payment_date,
|
|
93
|
+
v_pay.reference, v_label, 'draft', v_pay.currency_code)
|
|
94
|
+
returning * into v_entry;
|
|
95
|
+
|
|
96
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
97
|
+
debit, credit, currency_code)
|
|
98
|
+
values (v_entry.id, v_pay.company_id, v_money, 10, v_label,
|
|
99
|
+
case when v_inbound then v_pay.amount else 0 end,
|
|
100
|
+
case when v_inbound then 0 else v_pay.amount end,
|
|
101
|
+
v_pay.currency_code);
|
|
102
|
+
|
|
103
|
+
insert into entry_lines (entry_id, company_id, account_id, sequence, name,
|
|
104
|
+
debit, credit, contact_id, currency_code)
|
|
105
|
+
values (v_entry.id, v_pay.company_id, v_third, 20, v_label,
|
|
106
|
+
case when v_inbound then 0 else v_pay.amount end,
|
|
107
|
+
case when v_inbound then v_pay.amount else 0 end,
|
|
108
|
+
v_contact, v_pay.currency_code);
|
|
109
|
+
|
|
110
|
+
v_entry := post_entry(v_entry.id);
|
|
111
|
+
|
|
112
|
+
update payments
|
|
113
|
+
set entry_id = v_entry.id,
|
|
114
|
+
state = 'posted'
|
|
115
|
+
where id = p_payment_id;
|
|
116
|
+
|
|
117
|
+
return v_entry;
|
|
118
|
+
end;
|
|
119
|
+
$$;
|
|
120
|
+
|
|
121
|
+
comment on function post_payment(uuid) is
|
|
122
|
+
'Books a payment: the bank side from the payment''s bank account or its journal, the third-party side by role. Matches nothing.';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
-- Ekwo OS — the counters have to work for the person doing the booking.
|
|
2
|
+
--
|
|
3
|
+
-- `journal_sequences` and `matching_sequences` carry a select policy and no
|
|
4
|
+
-- other, which is right: nobody should be able to write a counter by hand.
|
|
5
|
+
-- But `next_entry_number()` and `next_matching_number()` write them, and they
|
|
6
|
+
-- ran as the caller — so posting an entry or drawing a matching letter
|
|
7
|
+
-- succeeded for the table owner and failed for every signed-in user with
|
|
8
|
+
-- "new row violates row-level security policy for table journal_sequences".
|
|
9
|
+
--
|
|
10
|
+
-- It went unnoticed because the tests exercised the schema as the owner, the
|
|
11
|
+
-- installer holds an owner connection, and until now nothing else wrote to
|
|
12
|
+
-- this database. The first client that signs in as a user — the MCP server —
|
|
13
|
+
-- hits it on the first invoice.
|
|
14
|
+
--
|
|
15
|
+
-- The counters are infrastructure, not user data: they exist so two
|
|
16
|
+
-- concurrent bookings do not draw the same number. So both functions become
|
|
17
|
+
-- SECURITY DEFINER, and each one checks first that the caller may write the
|
|
18
|
+
-- company it is counting for. A definer function that skipped that check
|
|
19
|
+
-- would be a way to burn numbers in somebody else's journal.
|
|
20
|
+
|
|
21
|
+
create or replace function next_entry_number(p_journal_id uuid, p_date date)
|
|
22
|
+
returns text
|
|
23
|
+
language plpgsql
|
|
24
|
+
security definer
|
|
25
|
+
set search_path = public, pg_temp
|
|
26
|
+
as $$
|
|
27
|
+
declare
|
|
28
|
+
v_code text;
|
|
29
|
+
v_company uuid;
|
|
30
|
+
v_year smallint := extract(year from p_date)::smallint;
|
|
31
|
+
v_number integer;
|
|
32
|
+
begin
|
|
33
|
+
select j.code, j.company_id into v_code, v_company from journals j where j.id = p_journal_id;
|
|
34
|
+
if v_code is null then
|
|
35
|
+
raise exception 'unknown_journal: journal % does not exist', p_journal_id;
|
|
36
|
+
end if;
|
|
37
|
+
|
|
38
|
+
-- The caller is about to write an entry in this company; they have to be
|
|
39
|
+
-- allowed to. auth.uid() is null on an owner connection, where row level
|
|
40
|
+
-- security is bypassed anyway and the installer is the caller.
|
|
41
|
+
if auth.uid() is not null and not can_write_company(v_company) then
|
|
42
|
+
raise exception 'not_allowed: you may not book in this company'
|
|
43
|
+
using errcode = '42501';
|
|
44
|
+
end if;
|
|
45
|
+
|
|
46
|
+
insert into journal_sequences (journal_id, year, last_number)
|
|
47
|
+
values (p_journal_id, v_year, 1)
|
|
48
|
+
on conflict (journal_id, year)
|
|
49
|
+
do update set last_number = journal_sequences.last_number + 1
|
|
50
|
+
returning last_number into v_number;
|
|
51
|
+
|
|
52
|
+
return v_code || '/' || v_year::text || '/' || lpad(v_number::text, 4, '0');
|
|
53
|
+
end;
|
|
54
|
+
$$;
|
|
55
|
+
|
|
56
|
+
comment on function next_entry_number(uuid, date) is
|
|
57
|
+
'Next number for a journal and year, as CODE/YYYY/NNNN. Atomic: the counter row is locked, not the journal. Definer, because the counter is infrastructure and nobody writes it by hand.';
|
|
58
|
+
|
|
59
|
+
create or replace function next_matching_number(p_company_id uuid)
|
|
60
|
+
returns text
|
|
61
|
+
language plpgsql
|
|
62
|
+
security definer
|
|
63
|
+
set search_path = public, pg_temp
|
|
64
|
+
as $$
|
|
65
|
+
declare
|
|
66
|
+
v_number integer;
|
|
67
|
+
begin
|
|
68
|
+
if auth.uid() is not null and not can_write_company(p_company_id) then
|
|
69
|
+
raise exception 'not_allowed: you may not match in this company'
|
|
70
|
+
using errcode = '42501';
|
|
71
|
+
end if;
|
|
72
|
+
|
|
73
|
+
insert into matching_sequences (company_id, last_number)
|
|
74
|
+
values (p_company_id, 1)
|
|
75
|
+
on conflict (company_id)
|
|
76
|
+
do update set last_number = matching_sequences.last_number + 1
|
|
77
|
+
returning last_number into v_number;
|
|
78
|
+
|
|
79
|
+
return 'A' || lpad(v_number::text, 4, '0');
|
|
80
|
+
end;
|
|
81
|
+
$$;
|
|
82
|
+
|
|
83
|
+
comment on function next_matching_number(uuid) is
|
|
84
|
+
'Next reconciliation letter for a company, as A0001. Definer, for the same reason as next_entry_number.';
|