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.
Files changed (361) hide show
  1. package/README.md +873 -0
  2. package/dist/args.d.ts +57 -0
  3. package/dist/args.d.ts.map +1 -0
  4. package/dist/args.js +134 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/assets/expected-objects.json +9432 -0
  7. package/dist/assets/migrations/20260911120000_core_companies.sql +260 -0
  8. package/dist/assets/migrations/20260911120100_accounts_journals.sql +211 -0
  9. package/dist/assets/migrations/20260911120200_contacts.sql +90 -0
  10. package/dist/assets/migrations/20260911120300_taxes.sql +146 -0
  11. package/dist/assets/migrations/20260911120400_entries.sql +338 -0
  12. package/dist/assets/migrations/20260911120500_documents.sql +279 -0
  13. package/dist/assets/migrations/20260911120600_payments_reconciliation.sql +251 -0
  14. package/dist/assets/migrations/20260911120700_bank.sql +178 -0
  15. package/dist/assets/migrations/20260911120800_analytics_attachments.sql +128 -0
  16. package/dist/assets/migrations/20260911120900_post_document.sql +279 -0
  17. package/dist/assets/migrations/20260911121000_reporting.sql +235 -0
  18. package/dist/assets/migrations/20260911121100_country_templates.sql +226 -0
  19. package/dist/assets/migrations/20260911121200_fec.sql +71 -0
  20. package/dist/assets/migrations/20260911130000_instance.sql +77 -0
  21. package/dist/assets/migrations/20260911130100_instance_members.sql +224 -0
  22. package/dist/assets/migrations/20260911140000_instance_admins.sql +112 -0
  23. package/dist/assets/migrations/20260911160000_tax_posting_templates_unique.sql +17 -0
  24. package/dist/assets/migrations/20260911170000_document_amount_paid.sql +82 -0
  25. package/dist/assets/migrations/20260911173000_post_payment.sql +122 -0
  26. package/dist/assets/migrations/20260911173100_sequence_counters_under_rls.sql +84 -0
  27. package/dist/assets/migrations/20260911183000_country_journal_defaults.sql +143 -0
  28. package/dist/assets/migrations/20260911193853_line_account_defaults.sql +274 -0
  29. package/dist/assets/migrations/20260911195054_products.sql +221 -0
  30. package/dist/assets/migrations/20260911210131_anon_surface.sql +85 -0
  31. package/dist/assets/migrations/20260912074712_country_packs.sql +318 -0
  32. package/dist/assets/migrations/20260912080311_report_code_and_region.sql +198 -0
  33. package/dist/assets/migrations/20260912081014_pack_certification_maintained.sql +31 -0
  34. package/dist/assets/migrations/20260912081015_pack_certification_backfill.sql +16 -0
  35. package/dist/assets/migrations/20260912090407_tax_report_boxes.sql +329 -0
  36. package/dist/assets/migrations/20260912091917_tax_on_base_value.sql +25 -0
  37. package/dist/assets/migrations/20260912091918_tax_engine_columns.sql +776 -0
  38. package/dist/assets/migrations/20260912094412_opening_and_closing.sql +759 -0
  39. package/dist/assets/migrations/20260912095825_charts_of_accounts.sql +326 -0
  40. package/dist/assets/migrations/20260912100412_financial_statements.sql +632 -0
  41. package/dist/assets/migrations/20260912104719_one_formula_evaluator.sql +209 -0
  42. package/dist/assets/migrations/20260912105720_entry_kind_appropriation.sql +26 -0
  43. package/dist/assets/migrations/20260912105721_appropriation_entry_kind.sql +444 -0
  44. package/dist/assets/migrations/20260912111751_document_rules.sql +274 -0
  45. package/dist/assets/migrations/20260912112132_cash_basis_vat_and_fx.sql +1189 -0
  46. package/dist/assets/migrations/20260913074512_modules.sql +493 -0
  47. package/dist/assets/migrations/20260913075903_asset_disposal_roles.sql +47 -0
  48. package/dist/assets/migrations/20260913083216_capabilities.sql +637 -0
  49. package/dist/assets/migrations/20260913083901_company_invitations.sql +262 -0
  50. package/dist/assets/migrations/20260913084402_user_preferences.sql +376 -0
  51. package/dist/assets/migrations/20260913084847_company_profile.sql +261 -0
  52. package/dist/assets/migrations/20260913085436_numbering_reads_the_pack.sql +235 -0
  53. package/dist/assets/migrations/20260913085932_api_keys.sql +303 -0
  54. package/dist/assets/migrations/20260913090216_fiscal_year_bounds.sql +159 -0
  55. package/dist/assets/migrations/20260913092527_entries_import.sql +219 -0
  56. package/dist/assets/migrations/20260913101536_null_safe_guards.sql +188 -0
  57. package/dist/assets/migrations/20260913102115_installer_is_named.sql +462 -0
  58. package/dist/assets/migrations/20260913102758_no_currency_default.sql +138 -0
  59. package/dist/assets/migrations/20260913103355_cash_basis_needs_a_box.sql +453 -0
  60. package/dist/assets/migrations/20260913104014_aged_balance_named_group.sql +79 -0
  61. package/dist/assets/migrations/20260913104232_foreign_key_indexes.sql +144 -0
  62. package/dist/assets/migrations/20260913105120_declared_no_reader.sql +41 -0
  63. package/dist/assets/migrations/20260913111407_pack_languages.sql +223 -0
  64. package/dist/assets/migrations/20260913112233_schema_comments_for_a_reader.sql +27 -0
  65. package/dist/assets/migrations/20260913114535_fec_opening_balances.sql +307 -0
  66. package/dist/assets/migrations/20260914103412_audit_log.sql +509 -0
  67. package/dist/assets/migrations/20260914111907_pack_upgrade.sql +428 -0
  68. package/dist/assets/migrations/20260914120500_rounding_reads_the_currency.sql +206 -0
  69. package/dist/assets/migrations/20260914121200_amounts_round_at_the_currency.sql +2403 -0
  70. package/dist/assets/migrations/20260914134325_schema_version_0_2_0.sql +36 -0
  71. package/dist/assets/migrations/20260914143915_accounts_in_use.sql +428 -0
  72. package/dist/assets/migrations/20260914144731_account_code_frozen.sql +99 -0
  73. package/dist/assets/migrations/20260914151207_schema_grants_its_own_rights.sql +284 -0
  74. package/dist/assets/migrations/20260914152840_pack_upgrade_records_its_own_line.sql +237 -0
  75. package/dist/assets/migrations/20260914163943_declaration_periodicity.sql +349 -0
  76. package/dist/assets/migrations/20260915094000_foreign_services_received.sql +37 -0
  77. package/dist/assets/migrations/20260915094500_mentions_read_a_foreign_service.sql +72 -0
  78. package/dist/assets/migrations/20260915153000_document_shares.sql +568 -0
  79. package/dist/assets/migrations/20260915160000_ec_sales_list.sql +181 -0
  80. package/dist/assets/migrations/20260915161842_pack_sources.sql +56 -0
  81. package/dist/assets/migrations/20260915170500_schema_version_0_3_0.sql +45 -0
  82. package/dist/assets/migrations/20260915174500_a_vat_category_is_a_code.sql +177 -0
  83. package/dist/assets/migrations/20260915180000_document_rule_references.sql +85 -0
  84. package/dist/assets/migrations/20260915181000_territories.sql +342 -0
  85. package/dist/assets/migrations/20260915181500_ec_sales_list_reads_the_territories.sql +222 -0
  86. package/dist/assets/migrations/20260915182000_intracom_triangular.sql +53 -0
  87. package/dist/assets/migrations/20260915182500_a_triangular_supply_says_reverse_charge.sql +99 -0
  88. package/dist/assets/migrations/20260915191200_a_document_knows_its_language.sql +633 -0
  89. package/dist/assets/migrations/20260915195000_a_price_that_holds_its_tax.sql +649 -0
  90. package/dist/assets/migrations/20260915200000_a_posting_names_its_boxes.sql +1004 -0
  91. package/dist/assets/migrations/20260916094500_a_company_files_more_than_one_declaration.sql +795 -0
  92. package/dist/assets/migrations/20260916103000_a_ledger_line_names_its_posting.sql +902 -0
  93. package/dist/assets/migrations/20260916123000_a_vocabulary_for_a_tax_that_is_not_a_vat.sql +144 -0
  94. package/dist/assets/migrations/20260916124000_a_box_can_be_a_rate_of_a_box.sql +525 -0
  95. package/dist/assets/migrations/20260916125000_a_tax_follows_the_territory.sql +1318 -0
  96. package/dist/assets/migrations/20260916126000_a_tax_point_and_its_exception.sql +1211 -0
  97. package/dist/assets/migrations/20260917090000_a_counterparty_that_learns.sql +561 -0
  98. package/dist/assets/migrations/20260917120000_what_the_money_pays.sql +545 -0
  99. package/dist/assets/migrations/20260917150000_a_filing_is_a_row.sql +488 -0
  100. package/dist/assets/migrations/20260917170000_when_it_is_due.sql +194 -0
  101. package/dist/assets/migrations/20260917180000_a_box_is_a_number_and_a_kind.sql +213 -0
  102. package/dist/assets/migrations/20260917190000_what_a_declaration_owes.sql +486 -0
  103. package/dist/assets/migrations/20260917200000_what_moved_after_it_went.sql +227 -0
  104. package/dist/assets/migrations/20260918090000_a_form_names_its_file.sql +27 -0
  105. package/dist/assets/migrations/20260918110000_a_deposit_and_what_came_back.sql +286 -0
  106. package/dist/assets/migrations/20260918113741_a_fourth_preset_is_named.sql +9 -0
  107. package/dist/assets/migrations/20260918113807_a_client_reads_and_hands_over.sql +96 -0
  108. package/dist/assets/migrations/20260918114322_two_definer_functions_check_their_caller.sql +75 -0
  109. package/dist/assets/migrations/20260918130000_every_company_somebody_keeps.sql +177 -0
  110. package/dist/assets/migrations/20260918140000_a_session_nobody_prepared.sql +40 -0
  111. package/dist/assets/migrations/20260918141107_a_line_keeps_the_tax_it_was_posted_with.sql +256 -0
  112. package/dist/assets/migrations/20260918141342_a_company_has_an_electronic_address.sql +66 -0
  113. package/dist/assets/migrations/20260918141605_an_invoice_reads_whole_from_the_views.sql +313 -0
  114. package/dist/assets/migrations/20260918141627_a_policy_asks_once.sql +132 -0
  115. package/dist/assets/migrations/20260918143352_a_suggestion_reads_the_words_once.sql +204 -0
  116. package/dist/assets/migrations/20260918143417_a_reference_of_the_caller_and_a_rehearsal.sql +115 -0
  117. package/dist/assets/migrations/20260918150712_a_company_leaves_with_its_books.sql +1119 -0
  118. package/dist/assets/migrations/20260918150931_a_statement_is_imported_once.sql +654 -0
  119. package/dist/assets/migrations/20260918161204_a_posted_document_does_not_move.sql +437 -0
  120. package/dist/assets/migrations/20260918161538_a_posted_entry_does_not_move.sql +252 -0
  121. package/dist/assets/migrations/20260918171946_an_entry_is_posted_by_post_entry.sql +289 -0
  122. package/dist/assets/migrations/20260918174312_schema_version_0_4_0.sql +53 -0
  123. package/dist/assets/modules/assets/module.json +13 -0
  124. package/dist/assets/modules/assets/supabase/migrations/20260913081447_assets.sql +1190 -0
  125. package/dist/assets/modules/assets/supabase/migrations/20260913104234_foreign_key_indexes.sql +47 -0
  126. package/dist/assets/modules/assets/supabase/migrations/20260913104500_assets_capabilities.sql +145 -0
  127. package/dist/assets/modules/assets/supabase/migrations/20260914122100_assets_round_at_the_currency.sql +564 -0
  128. package/dist/assets/modules/assets/supabase/migrations/20260914145012_assets_accounts_in_use.sql +37 -0
  129. package/dist/assets/modules/assets/supabase/migrations/20260914151530_assets_grants_its_own_rights.sql +63 -0
  130. package/dist/assets/modules/assets/supabase/migrations/20260918113912_assets_client_reads.sql +20 -0
  131. package/dist/assets/modules/assets/supabase/migrations/20260918150801_assets_leave_with_the_company.sql +41 -0
  132. package/dist/assets/modules/budgets/module.json +12 -0
  133. package/dist/assets/modules/budgets/supabase/migrations/20260913083012_budgets.sql +210 -0
  134. package/dist/assets/modules/budgets/supabase/migrations/20260913104233_foreign_key_indexes.sql +37 -0
  135. package/dist/assets/modules/budgets/supabase/migrations/20260913104501_budgets_capabilities.sql +72 -0
  136. package/dist/assets/modules/budgets/supabase/migrations/20260914122400_budgets_round_at_the_currency.sql +68 -0
  137. package/dist/assets/modules/budgets/supabase/migrations/20260914145048_budgets_accounts_in_use.sql +30 -0
  138. package/dist/assets/modules/budgets/supabase/migrations/20260914151812_budgets_grants_its_own_rights.sql +31 -0
  139. package/dist/assets/modules/budgets/supabase/migrations/20260918113954_budgets_client_reads.sql +20 -0
  140. package/dist/assets/modules/budgets/supabase/migrations/20260918150823_budgets_leave_with_the_company.sql +37 -0
  141. package/dist/assets/modules/schema/module.1.json +69 -0
  142. package/dist/assets/seed/00_currencies.sql +15 -0
  143. package/dist/assets/seed/00_territories.sql +280 -0
  144. package/dist/assets/seed/05_framework_generic.sql +110 -0
  145. package/dist/assets/seed/10_pack_be.sql +1284 -0
  146. package/dist/assets/seed/11_pack_fr.sql +1112 -0
  147. package/dist/assets/seed/12_pack_lu.sql +2484 -0
  148. package/dist/assets/seed/13_pack_ee.sql +733 -0
  149. package/dist/assets/seed/14_pack_gb.sql +796 -0
  150. package/dist/assets/seed/15_pack_us.sql +751 -0
  151. package/dist/assets/seed/90_demo_company.sql +368 -0
  152. package/dist/assets/seed/modules/assets/10_pack_be.sql +58 -0
  153. package/dist/assets/seed/modules/assets/11_pack_fr.sql +57 -0
  154. package/dist/assets/seed/modules/assets/14_pack_gb.sql +58 -0
  155. package/dist/assets/seed/modules/assets/15_pack_us.sql +60 -0
  156. package/dist/auth.d.ts +59 -0
  157. package/dist/auth.d.ts.map +1 -0
  158. package/dist/auth.js +134 -0
  159. package/dist/auth.js.map +1 -0
  160. package/dist/bin.d.ts +6 -0
  161. package/dist/bin.d.ts.map +1 -0
  162. package/dist/bin.js +8 -0
  163. package/dist/bin.js.map +1 -0
  164. package/dist/books.d.ts +60 -0
  165. package/dist/books.d.ts.map +1 -0
  166. package/dist/books.js +168 -0
  167. package/dist/books.js.map +1 -0
  168. package/dist/bootstrap.d.ts +236 -0
  169. package/dist/bootstrap.d.ts.map +1 -0
  170. package/dist/bootstrap.js +459 -0
  171. package/dist/bootstrap.js.map +1 -0
  172. package/dist/bundle.d.ts +27 -0
  173. package/dist/bundle.d.ts.map +1 -0
  174. package/dist/bundle.js +47 -0
  175. package/dist/bundle.js.map +1 -0
  176. package/dist/checklist.d.ts +35 -0
  177. package/dist/checklist.d.ts.map +1 -0
  178. package/dist/checklist.js +64 -0
  179. package/dist/checklist.js.map +1 -0
  180. package/dist/cli.d.ts +14 -0
  181. package/dist/cli.d.ts.map +1 -0
  182. package/dist/cli.js +318 -0
  183. package/dist/cli.js.map +1 -0
  184. package/dist/commands/company.d.ts +59 -0
  185. package/dist/commands/company.d.ts.map +1 -0
  186. package/dist/commands/company.js +287 -0
  187. package/dist/commands/company.js.map +1 -0
  188. package/dist/commands/contact.d.ts +9 -0
  189. package/dist/commands/contact.d.ts.map +1 -0
  190. package/dist/commands/contact.js +82 -0
  191. package/dist/commands/contact.js.map +1 -0
  192. package/dist/commands/demo.d.ts +19 -0
  193. package/dist/commands/demo.d.ts.map +1 -0
  194. package/dist/commands/demo.js +72 -0
  195. package/dist/commands/demo.js.map +1 -0
  196. package/dist/commands/doctor.d.ts +12 -0
  197. package/dist/commands/doctor.d.ts.map +1 -0
  198. package/dist/commands/doctor.js +68 -0
  199. package/dist/commands/doctor.js.map +1 -0
  200. package/dist/commands/document.d.ts +17 -0
  201. package/dist/commands/document.d.ts.map +1 -0
  202. package/dist/commands/document.js +217 -0
  203. package/dist/commands/document.js.map +1 -0
  204. package/dist/commands/init.d.ts +31 -0
  205. package/dist/commands/init.d.ts.map +1 -0
  206. package/dist/commands/init.js +535 -0
  207. package/dist/commands/init.js.map +1 -0
  208. package/dist/commands/login.d.ts +24 -0
  209. package/dist/commands/login.d.ts.map +1 -0
  210. package/dist/commands/login.js +128 -0
  211. package/dist/commands/login.js.map +1 -0
  212. package/dist/commands/migrate.d.ts +30 -0
  213. package/dist/commands/migrate.d.ts.map +1 -0
  214. package/dist/commands/migrate.js +115 -0
  215. package/dist/commands/migrate.js.map +1 -0
  216. package/dist/commands/module.d.ts +32 -0
  217. package/dist/commands/module.d.ts.map +1 -0
  218. package/dist/commands/module.js +285 -0
  219. package/dist/commands/module.js.map +1 -0
  220. package/dist/commands/pack.d.ts +22 -0
  221. package/dist/commands/pack.d.ts.map +1 -0
  222. package/dist/commands/pack.js +481 -0
  223. package/dist/commands/pack.js.map +1 -0
  224. package/dist/commands/payment.d.ts +15 -0
  225. package/dist/commands/payment.d.ts.map +1 -0
  226. package/dist/commands/payment.js +97 -0
  227. package/dist/commands/payment.js.map +1 -0
  228. package/dist/commands/register.d.ts +20 -0
  229. package/dist/commands/register.d.ts.map +1 -0
  230. package/dist/commands/register.js +134 -0
  231. package/dist/commands/register.js.map +1 -0
  232. package/dist/commands/status.d.ts +8 -0
  233. package/dist/commands/status.d.ts.map +1 -0
  234. package/dist/commands/status.js +134 -0
  235. package/dist/commands/status.js.map +1 -0
  236. package/dist/commands/whoami.d.ts +21 -0
  237. package/dist/commands/whoami.d.ts.map +1 -0
  238. package/dist/commands/whoami.js +133 -0
  239. package/dist/commands/whoami.js.map +1 -0
  240. package/dist/company.d.ts +16 -0
  241. package/dist/company.d.ts.map +1 -0
  242. package/dist/company.js +26 -0
  243. package/dist/company.js.map +1 -0
  244. package/dist/config.d.ts +24 -0
  245. package/dist/config.d.ts.map +1 -0
  246. package/dist/config.js +50 -0
  247. package/dist/config.js.map +1 -0
  248. package/dist/connection.d.ts +79 -0
  249. package/dist/connection.d.ts.map +1 -0
  250. package/dist/connection.js +116 -0
  251. package/dist/connection.js.map +1 -0
  252. package/dist/context.d.ts +47 -0
  253. package/dist/context.d.ts.map +1 -0
  254. package/dist/context.js +93 -0
  255. package/dist/context.js.map +1 -0
  256. package/dist/doctor.d.ts +45 -0
  257. package/dist/doctor.d.ts.map +1 -0
  258. package/dist/doctor.js +448 -0
  259. package/dist/doctor.js.map +1 -0
  260. package/dist/grants.d.ts +88 -0
  261. package/dist/grants.d.ts.map +1 -0
  262. package/dist/grants.js +174 -0
  263. package/dist/grants.js.map +1 -0
  264. package/dist/identity.d.ts +54 -0
  265. package/dist/identity.d.ts.map +1 -0
  266. package/dist/identity.js +132 -0
  267. package/dist/identity.js.map +1 -0
  268. package/dist/index.d.ts +44 -0
  269. package/dist/index.d.ts.map +1 -0
  270. package/dist/index.js +43 -0
  271. package/dist/index.js.map +1 -0
  272. package/dist/inventory.d.ts +155 -0
  273. package/dist/inventory.d.ts.map +1 -0
  274. package/dist/inventory.js +303 -0
  275. package/dist/inventory.js.map +1 -0
  276. package/dist/migrations.d.ts +76 -0
  277. package/dist/migrations.d.ts.map +1 -0
  278. package/dist/migrations.js +226 -0
  279. package/dist/migrations.js.map +1 -0
  280. package/dist/module/read.d.ts +91 -0
  281. package/dist/module/read.d.ts.map +1 -0
  282. package/dist/module/read.js +172 -0
  283. package/dist/module/read.js.map +1 -0
  284. package/dist/output.d.ts +110 -0
  285. package/dist/output.d.ts.map +1 -0
  286. package/dist/output.js +173 -0
  287. package/dist/output.js.map +1 -0
  288. package/dist/pack/certification.d.ts +27 -0
  289. package/dist/pack/certification.d.ts.map +1 -0
  290. package/dist/pack/certification.js +38 -0
  291. package/dist/pack/certification.js.map +1 -0
  292. package/dist/pack/compile.d.ts +75 -0
  293. package/dist/pack/compile.d.ts.map +1 -0
  294. package/dist/pack/compile.js +801 -0
  295. package/dist/pack/compile.js.map +1 -0
  296. package/dist/pack/filing.d.ts +48 -0
  297. package/dist/pack/filing.d.ts.map +1 -0
  298. package/dist/pack/filing.js +56 -0
  299. package/dist/pack/filing.js.map +1 -0
  300. package/dist/pack/read.d.ts +766 -0
  301. package/dist/pack/read.d.ts.map +1 -0
  302. package/dist/pack/read.js +2336 -0
  303. package/dist/pack/read.js.map +1 -0
  304. package/dist/pack/schema.d.ts +26 -0
  305. package/dist/pack/schema.d.ts.map +1 -0
  306. package/dist/pack/schema.js +181 -0
  307. package/dist/pack/schema.js.map +1 -0
  308. package/dist/pack/territories.d.ts +98 -0
  309. package/dist/pack/territories.d.ts.map +1 -0
  310. package/dist/pack/territories.js +0 -0
  311. package/dist/pack/territories.js.map +1 -0
  312. package/dist/pack/upgrade.d.ts +86 -0
  313. package/dist/pack/upgrade.d.ts.map +1 -0
  314. package/dist/pack/upgrade.js +76 -0
  315. package/dist/pack/upgrade.js.map +1 -0
  316. package/dist/pack/vat-codes.d.ts +273 -0
  317. package/dist/pack/vat-codes.d.ts.map +1 -0
  318. package/dist/pack/vat-codes.js +513 -0
  319. package/dist/pack/vat-codes.js.map +1 -0
  320. package/dist/profiles.d.ts +67 -0
  321. package/dist/profiles.d.ts.map +1 -0
  322. package/dist/profiles.js +187 -0
  323. package/dist/profiles.js.map +1 -0
  324. package/dist/prompt.d.ts +40 -0
  325. package/dist/prompt.d.ts.map +1 -0
  326. package/dist/prompt.js +120 -0
  327. package/dist/prompt.js.map +1 -0
  328. package/dist/registry.d.ts +84 -0
  329. package/dist/registry.d.ts.map +1 -0
  330. package/dist/registry.js +104 -0
  331. package/dist/registry.js.map +1 -0
  332. package/dist/rest.d.ts +81 -0
  333. package/dist/rest.d.ts.map +1 -0
  334. package/dist/rest.js +245 -0
  335. package/dist/rest.js.map +1 -0
  336. package/dist/schema.d.ts +15 -0
  337. package/dist/schema.d.ts.map +1 -0
  338. package/dist/schema.js +15 -0
  339. package/dist/schema.js.map +1 -0
  340. package/dist/seeds.d.ts +27 -0
  341. package/dist/seeds.d.ts.map +1 -0
  342. package/dist/seeds.js +47 -0
  343. package/dist/seeds.js.map +1 -0
  344. package/dist/session.d.ts +39 -0
  345. package/dist/session.d.ts.map +1 -0
  346. package/dist/session.js +92 -0
  347. package/dist/session.js.map +1 -0
  348. package/dist/sql.d.ts +48 -0
  349. package/dist/sql.d.ts.map +1 -0
  350. package/dist/sql.js +112 -0
  351. package/dist/sql.js.map +1 -0
  352. package/dist/status.d.ts +94 -0
  353. package/dist/status.d.ts.map +1 -0
  354. package/dist/status.js +120 -0
  355. package/dist/status.js.map +1 -0
  356. package/dist/ui.d.ts +55 -0
  357. package/dist/ui.d.ts.map +1 -0
  358. package/dist/ui.js +114 -0
  359. package/dist/ui.js.map +1 -0
  360. package/package.json +29 -0
  361. package/schema/output.1.json +613 -0
@@ -0,0 +1,262 @@
1
+ -- Ekwo OS — inviting somebody who does not have an account yet.
2
+ --
3
+ -- `company_members.user_id` deliberately carries no foreign key to
4
+ -- `auth.users`, precisely so that a membership can be written before the
5
+ -- person has signed up. What was missing is the act that produces it: until
6
+ -- now somebody had to know the invitee's `auth.users.id` — which means the
7
+ -- invitee had to exist, and somebody had to go and read it out of the Auth
8
+ -- dashboard.
9
+ --
10
+ -- An invitation is the missing half. It names an address, a preset and any
11
+ -- capability granted on top of it, and it carries a secret that is handed over
12
+ -- once and stored only as a hash. The person signs up with their own address
13
+ -- in the customer's Supabase Auth, and `accept_invitation()` turns the
14
+ -- invitation into a membership — after checking that the address they signed
15
+ -- in with is the one that was invited.
16
+ --
17
+ -- **Three rules and their reasons.**
18
+ --
19
+ -- The token is never stored. `token_hash` is a sha256 of it, computed by
20
+ -- Postgres' own `sha256()` — core since PostgreSQL 11, so no extension, which
21
+ -- `pgcrypto` would have been and PGlite does not carry. A database backup
22
+ -- therefore hands nobody an invitation.
23
+ --
24
+ -- The address is matched, not trusted. A token alone would let whoever
25
+ -- receives a forwarded e-mail join the books; `auth.email()` has to equal the
26
+ -- address the invitation was written for, case-insensitively.
27
+ --
28
+ -- An invitation is used once and expires. Accepting sets `accepted_at`, and
29
+ -- every later attempt is refused rather than silently re-adding a member
30
+ -- somebody may have removed on purpose.
31
+
32
+ create table company_invitations (
33
+ id uuid primary key default gen_random_uuid(),
34
+ company_id uuid not null references companies(id) on delete cascade,
35
+ -- Lower-cased on the way in: an address is not case-sensitive in its domain
36
+ -- and, in practice, not in its local part either. `citext` would have said
37
+ -- the same thing and is an extension.
38
+ email text not null,
39
+ role member_role not null default 'viewer',
40
+ capabilities_granted text[] not null default '{}',
41
+ token_hash text not null unique,
42
+ invited_by uuid,
43
+ created_at timestamptz not null default now(),
44
+ expires_at timestamptz not null,
45
+ accepted_at timestamptz,
46
+ accepted_by uuid,
47
+ revoked_at timestamptz,
48
+ constraint company_invitations_email_lowercase check (email = lower(email)),
49
+ constraint company_invitations_email_shape check (email like '%_@_%'),
50
+ constraint company_invitations_expiry check (expires_at > created_at),
51
+ constraint company_invitations_accepted_together
52
+ check ((accepted_at is null) = (accepted_by is null))
53
+ );
54
+
55
+ comment on table company_invitations is
56
+ 'Pending and past invitations into a company. The token is handed over once and kept only as a sha256 hash.';
57
+ comment on column company_invitations.email is
58
+ 'The address the invitation is for, lower-cased. accept_invitation() refuses anyone signed in with another.';
59
+ comment on column company_invitations.capabilities_granted is
60
+ 'Capabilities the new member holds on top of their preset, written onto company_members when the invitation is accepted.';
61
+ comment on column company_invitations.invited_by is
62
+ 'auth.users.id of whoever issued it. No foreign key, for the same reason company_members has none.';
63
+
64
+ create index company_invitations_company_idx on company_invitations (company_id, created_at desc);
65
+ create index company_invitations_email_idx on company_invitations (lower(email));
66
+
67
+ -- ---------------------------------------------------------------------------
68
+ -- Issuing one
69
+ --
70
+ -- The secret is two `gen_random_uuid()` with their dashes removed: 256 bits
71
+ -- from the same source the primary keys come from, and no extension. It is
72
+ -- returned by this call and by nothing else, ever.
73
+ -- ---------------------------------------------------------------------------
74
+
75
+ create or replace function invite_member(
76
+ p_company_id uuid,
77
+ p_email text,
78
+ p_role member_role default 'viewer',
79
+ -- A list crosses as JSON, the way `opening_balance` takes its lines: over
80
+ -- PostgREST an argument is a JSON body, and a Postgres driver handed a
81
+ -- JavaScript array builds an array literal instead. One spelling that both
82
+ -- routes read the same way.
83
+ p_capabilities jsonb default '[]'::jsonb,
84
+ p_valid_for interval default interval '14 days'
85
+ )
86
+ returns table (invitation_id uuid, token text, expires_at timestamptz)
87
+ language plpgsql
88
+ security definer
89
+ set search_path = public, pg_temp
90
+ as $$
91
+ declare
92
+ v_email text := lower(trim(p_email));
93
+ v_token text;
94
+ v_unknown text;
95
+ v_caps text[] := coalesce(
96
+ (select array_agg(value) from jsonb_array_elements_text(coalesce(p_capabilities, '[]'::jsonb)) as value),
97
+ '{}');
98
+ v_row company_invitations%rowtype;
99
+ begin
100
+ if auth.uid() is not null and not has_capability(p_company_id, 'members.manage')
101
+ and not is_instance_admin() then
102
+ raise exception 'not_allowed: inviting somebody into this company needs members.manage'
103
+ using errcode = '42501';
104
+ end if;
105
+
106
+ if v_email is null or v_email not like '%_@_%' then
107
+ raise exception 'bad_email: % is not an address an invitation can be sent to', p_email;
108
+ end if;
109
+
110
+ select c into v_unknown
111
+ from unnest(v_caps) as c
112
+ where c not in (select code from capabilities)
113
+ limit 1;
114
+ if v_unknown is not null then
115
+ raise exception 'unknown_capability: % is not a capability of this installation', v_unknown;
116
+ end if;
117
+
118
+ -- Re-inviting the same address replaces the pending invitation rather than
119
+ -- leaving two tokens alive for one seat.
120
+ update company_invitations
121
+ set revoked_at = now()
122
+ where company_id = p_company_id
123
+ and email = v_email
124
+ and accepted_at is null
125
+ and revoked_at is null;
126
+
127
+ v_token := replace(gen_random_uuid()::text, '-', '') ||
128
+ replace(gen_random_uuid()::text, '-', '');
129
+
130
+ insert into company_invitations (company_id, email, role, capabilities_granted,
131
+ token_hash, invited_by, expires_at)
132
+ values (p_company_id, v_email, p_role, v_caps,
133
+ encode(sha256(convert_to(v_token, 'UTF8')), 'hex'), auth.uid(), now() + p_valid_for)
134
+ returning * into v_row;
135
+
136
+ return query select v_row.id, v_token, v_row.expires_at;
137
+ end;
138
+ $$;
139
+
140
+ comment on function invite_member(uuid, text, member_role, jsonb, interval) is
141
+ 'Invites an address into a company and returns the token once. Only the hash is stored; re-inviting the same address revokes the pending invitation.';
142
+
143
+ -- ---------------------------------------------------------------------------
144
+ -- Accepting one
145
+ --
146
+ -- Definer, because the invitee is nobody yet: they cannot read
147
+ -- `company_invitations` and cannot write `company_members`. What the function
148
+ -- checks is everything the policies would have: a live invitation, and the
149
+ -- address it was written for.
150
+ -- ---------------------------------------------------------------------------
151
+
152
+ create or replace function accept_invitation(p_token text)
153
+ returns company_members
154
+ language plpgsql
155
+ security definer
156
+ set search_path = public, pg_temp
157
+ as $$
158
+ declare
159
+ v_invitation company_invitations%rowtype;
160
+ v_member company_members%rowtype;
161
+ begin
162
+ if auth.uid() is null then
163
+ raise exception 'no_user: accepting an invitation is something a signed-in user does';
164
+ end if;
165
+
166
+ select * into v_invitation
167
+ from company_invitations
168
+ where token_hash = encode(sha256(convert_to(coalesce(p_token, ''), 'UTF8')), 'hex');
169
+
170
+ if v_invitation.id is null then
171
+ raise exception 'unknown_invitation: that is not an invitation of this installation';
172
+ end if;
173
+ if v_invitation.revoked_at is not null then
174
+ raise exception 'invitation_revoked: that invitation was withdrawn on %', v_invitation.revoked_at;
175
+ end if;
176
+ if v_invitation.accepted_at is not null then
177
+ raise exception 'invitation_already_accepted: that invitation was used on %', v_invitation.accepted_at;
178
+ end if;
179
+ if v_invitation.expires_at <= now() then
180
+ raise exception 'invitation_expired: that invitation expired on %', v_invitation.expires_at;
181
+ end if;
182
+ if lower(coalesce(auth.email(), '')) <> v_invitation.email then
183
+ raise exception 'invitation_not_yours: that invitation was sent to another address'
184
+ using errcode = '42501';
185
+ end if;
186
+
187
+ insert into company_members (company_id, user_id, role, capabilities_granted)
188
+ values (v_invitation.company_id, auth.uid(), v_invitation.role,
189
+ v_invitation.capabilities_granted)
190
+ on conflict (company_id, user_id) do nothing
191
+ returning * into v_member;
192
+
193
+ if v_member.user_id is null then
194
+ select * into v_member
195
+ from company_members
196
+ where company_id = v_invitation.company_id and user_id = auth.uid();
197
+ end if;
198
+
199
+ update company_invitations
200
+ set accepted_at = now(), accepted_by = auth.uid()
201
+ where id = v_invitation.id;
202
+
203
+ return v_member;
204
+ end;
205
+ $$;
206
+
207
+ comment on function accept_invitation(text) is
208
+ 'Turns an invitation into a membership for the signed-in user, whose address has to be the one invited. Single use, and refused once expired.';
209
+
210
+ create or replace function revoke_invitation(p_invitation_id uuid)
211
+ returns company_invitations
212
+ language plpgsql
213
+ security definer
214
+ set search_path = public, pg_temp
215
+ as $$
216
+ declare
217
+ v_row company_invitations%rowtype;
218
+ begin
219
+ select * into v_row from company_invitations where id = p_invitation_id;
220
+ if v_row.id is null then
221
+ raise exception 'unknown_invitation: no invitation with that id';
222
+ end if;
223
+
224
+ if auth.uid() is not null and not has_capability(v_row.company_id, 'members.manage')
225
+ and not is_instance_admin() then
226
+ raise exception 'not_allowed: withdrawing an invitation needs members.manage'
227
+ using errcode = '42501';
228
+ end if;
229
+
230
+ if v_row.accepted_at is not null then
231
+ raise exception 'invitation_already_accepted: it became a membership on %; remove the member instead',
232
+ v_row.accepted_at;
233
+ end if;
234
+
235
+ update company_invitations set revoked_at = coalesce(revoked_at, now())
236
+ where id = p_invitation_id
237
+ returning * into v_row;
238
+
239
+ return v_row;
240
+ end;
241
+ $$;
242
+
243
+ comment on function revoke_invitation(uuid) is
244
+ 'Withdraws an invitation that has not been accepted. An accepted one is a member, and members are removed from company_members.';
245
+
246
+ -- ---------------------------------------------------------------------------
247
+ -- Row level security
248
+ --
249
+ -- Reading an invitation is administering members, so it is `members.manage`
250
+ -- and the instance administrator. Nothing writes through the API: the three
251
+ -- functions above are the only way in, and each one checks what a policy
252
+ -- would have.
253
+ -- ---------------------------------------------------------------------------
254
+
255
+ alter table company_invitations enable row level security;
256
+
257
+ create policy company_invitations_select on company_invitations
258
+ for select using (
259
+ has_capability(company_id, 'members.manage') or is_instance_admin()
260
+ );
261
+
262
+ revoke execute on all functions in schema public from public;
@@ -0,0 +1,376 @@
1
+ -- Ekwo OS — what a person prefers, and the one way a label is chosen.
2
+ --
3
+ -- Two things that turned out to be the same thing.
4
+ --
5
+ -- **A preference belongs to a person, not to a company.** Which company they
6
+ -- work in by default, the language they read in, their timezone, how they
7
+ -- like a date and a number written. None of it has a default in the schema,
8
+ -- for the reason P0-7 and P0-8 gave before: a default language is one
9
+ -- country's answer handed to everybody who has not spoken, and null means
10
+ -- "ask the company, then the pack, then the client" rather than "English".
11
+ --
12
+ -- **A label is chosen in one place.** `name_i18n` and `text_i18n` sit on the
13
+ -- chart, the journals, the declaration boxes, the statement lines and the
14
+ -- legal mentions, and the way to read one was written out as
15
+ -- `coalesce(nullif(x.name_i18n ->> lang, ''), x.name)` wherever it was
16
+ -- needed. That is a formula, and a formula written twice is a formula that
17
+ -- will one day disagree with itself. `label_for(name, name_i18n, languages)`
18
+ -- is now the only spelling of it, it takes a **list** of languages rather
19
+ -- than one, and `preferred_languages(company)` builds that list: the user's
20
+ -- own, then the company's, then the pack's.
21
+ --
22
+ -- `install_country_template()` is republished on top of it. It keeps passing
23
+ -- one language and the company's, deliberately: a chart of accounts is copied
24
+ -- in the language the books are kept in, not in the language of whoever
25
+ -- happened to run the installer.
26
+
27
+ -- ---------------------------------------------------------------------------
28
+ -- label_for — the one way a translated label is picked
29
+ -- ---------------------------------------------------------------------------
30
+
31
+ create or replace function label_for(p_name text, p_i18n jsonb, p_languages text[])
32
+ returns text
33
+ language sql
34
+ immutable
35
+ as $$
36
+ select coalesce(
37
+ (select nullif(p_i18n ->> lang, '')
38
+ from unnest(coalesce(p_languages, '{}')) with ordinality as l(lang, ord)
39
+ where nullif(p_i18n ->> lang, '') is not null
40
+ order by l.ord
41
+ limit 1),
42
+ p_name);
43
+ $$;
44
+
45
+ comment on function label_for(text, jsonb, text[]) is
46
+ 'The label in the first language of the list that has one, and the row''s own name when none of them does. The only place a translated label is chosen.';
47
+
48
+ -- ---------------------------------------------------------------------------
49
+ -- user_preferences
50
+ -- ---------------------------------------------------------------------------
51
+
52
+ create table user_preferences (
53
+ user_id uuid primary key references auth.users(id) on delete cascade,
54
+ preferred_company_id uuid references companies(id) on delete set null,
55
+ language text,
56
+ timezone text,
57
+ -- `*_display_format` and not `date_format` / `number_format`: the second of
58
+ -- those is already a column of `country_defaults`, where it is the pattern
59
+ -- a document number is built from. Two questions that have nothing to do
60
+ -- with each other should not answer to one name — a reader meeting both
61
+ -- would have to know which table they were in to know what they had.
62
+ date_display_format text,
63
+ number_display_format text,
64
+ theme text,
65
+ created_at timestamptz not null default now(),
66
+ updated_at timestamptz not null default now(),
67
+ constraint user_preferences_language_shape
68
+ check (language is null or language ~ '^[a-z]{2}(-[A-Za-z0-9]{2,8})?$')
69
+ );
70
+
71
+ comment on table user_preferences is
72
+ 'What one person prefers, across every company they are a member of. Every column is nullable and none has a default: null means "take the company''s answer, then the pack''s".';
73
+ comment on column user_preferences.preferred_company_id is
74
+ 'The company an interface opens on. Cleared rather than kept when that company is deleted.';
75
+ comment on column user_preferences.language is
76
+ 'Language this person reads labels in. First in the list preferred_languages() builds.';
77
+ comment on column user_preferences.theme is
78
+ 'A client''s business. The core stores it and interprets nothing.';
79
+
80
+ create trigger user_preferences_set_updated_at
81
+ before update on user_preferences
82
+ for each row execute function set_updated_at();
83
+
84
+ -- ---------------------------------------------------------------------------
85
+ -- preferred_languages — the chain a reader follows
86
+ -- ---------------------------------------------------------------------------
87
+
88
+ create or replace function preferred_languages(p_company_id uuid default null)
89
+ returns text[]
90
+ language sql
91
+ stable
92
+ security definer
93
+ set search_path = public, pg_temp
94
+ as $$
95
+ select array_remove(array[
96
+ (select p.language from user_preferences p where p.user_id = auth.uid()),
97
+ (select c.language from companies c where c.id = p_company_id),
98
+ (select d.language_default
99
+ from companies c join country_defaults d on d.country = c.country
100
+ where c.id = p_company_id)
101
+ ], null);
102
+ $$;
103
+
104
+ comment on function preferred_languages(uuid) is
105
+ 'The languages to try, in order: the user''s own, then the company''s, then the one the country pack declares. Feed it to label_for().';
106
+
107
+ -- ---------------------------------------------------------------------------
108
+ -- install_country_template, republished on label_for
109
+ --
110
+ -- Unchanged but for one expression: the chart is copied under
111
+ -- `label_for(t.name, t.name_i18n, array[v_language])` instead of the coalesce
112
+ -- that was written out here. Same answer, one formula.
113
+ -- ---------------------------------------------------------------------------
114
+
115
+ create or replace function install_country_template(
116
+ p_company_id uuid,
117
+ p_country char(2),
118
+ p_language char(2) default null,
119
+ p_chart_code text default null
120
+ )
121
+ returns void
122
+ language plpgsql
123
+ as $$
124
+ declare
125
+ v_defaults country_defaults%rowtype;
126
+ r record;
127
+ v_tax_id uuid;
128
+ v_language char(2);
129
+ v_version text;
130
+ v_chart text;
131
+ v_charts text;
132
+ begin
133
+ if not exists (select 1 from companies where id = p_company_id) then
134
+ raise exception 'unknown_company: %', p_company_id;
135
+ end if;
136
+
137
+ -- Which chart. The caller names one, or the pack's default stands. A named
138
+ -- chart that does not exist is an error that lists what does: guessing here
139
+ -- would install the wrong plan of accounts and nobody would notice for a
140
+ -- year.
141
+ if p_chart_code is null then
142
+ select c.code into v_chart
143
+ from chart_templates c
144
+ where c.country = p_country and c.is_default;
145
+ if v_chart is null then
146
+ -- No pack for this country, or a pack that names no default chart.
147
+ raise exception 'unknown_country_template: no chart of accounts seeded for %', p_country;
148
+ end if;
149
+ else
150
+ select c.code into v_chart
151
+ from chart_templates c
152
+ where c.country = p_country and c.code = p_chart_code;
153
+ if v_chart is null then
154
+ select string_agg(c.code, ', ' order by c.code) into v_charts
155
+ from chart_templates c where c.country = p_country;
156
+ raise exception 'unknown_chart: % has no chart %. It has: %',
157
+ p_country, p_chart_code, coalesce(v_charts, 'none');
158
+ end if;
159
+ end if;
160
+
161
+ if not exists (
162
+ select 1 from account_templates
163
+ where country = p_country and chart_code = v_chart
164
+ ) then
165
+ raise exception 'unknown_country_template: chart % of % holds no account', v_chart, p_country;
166
+ end if;
167
+
168
+ -- The language of the copy: what the caller asked for, else what the
169
+ -- company keeps its books in.
170
+ select coalesce(p_language, c.language) into v_language
171
+ from companies c where c.id = p_company_id;
172
+
173
+ -- 1. Accounts, without the hierarchy.
174
+ insert into accounts (company_id, code, name, name_i18n, statement_hint,
175
+ account_type, reconcilable)
176
+ select p_company_id, t.code,
177
+ label_for(t.name, t.name_i18n, array[v_language]),
178
+ t.name_i18n, t.statement_hint, t.account_type, t.reconcilable
179
+ from account_templates t
180
+ where t.country = p_country
181
+ and t.chart_code = v_chart
182
+ on conflict (company_id, code) do nothing;
183
+
184
+ -- 2. Hierarchy, now that every code exists.
185
+ update accounts a
186
+ set parent_id = p.id
187
+ from account_templates t
188
+ join accounts p on p.company_id = p_company_id and p.code = t.parent_code
189
+ where a.company_id = p_company_id
190
+ and a.code = t.code
191
+ and t.country = p_country
192
+ and t.chart_code = v_chart
193
+ and t.parent_code is not null
194
+ and a.parent_id is null;
195
+
196
+ -- 3. Journals. Common to every chart of the country.
197
+ insert into journals (company_id, code, name, journal_type)
198
+ select p_company_id, t.code, t.name, t.journal_type
199
+ from journal_templates t
200
+ where t.country = p_country
201
+ on conflict (company_id, code) do nothing;
202
+
203
+ -- 4. Taxes and their postings. Also common: a chart changes how a company
204
+ -- presents itself, not what it owes.
205
+ for r in
206
+ select * from tax_templates where country = p_country order by sequence, code
207
+ loop
208
+ v_tax_id := null;
209
+ insert into taxes (company_id, code, name, description, amount_type, amount,
210
+ applies_to, treatment, country, valid_from, valid_to,
211
+ legal_reference, vat_category, exemption_code, sequence,
212
+ tax_kind, recoverable, jurisdiction, price_include,
213
+ cash_basis, cash_basis_transition_account_id)
214
+ values (p_company_id, r.code, r.name, r.description, r.amount_type, r.amount,
215
+ r.applies_to, r.treatment, p_country, r.valid_from, r.valid_to,
216
+ r.legal_reference, r.vat_category, r.exemption_code, r.sequence,
217
+ r.tax_kind, r.recoverable, r.jurisdiction, r.price_include,
218
+ r.cash_basis, account_id_by_code(p_company_id, r.cash_basis_transition_account_code))
219
+ on conflict (company_id, code) do nothing
220
+ returning id into v_tax_id;
221
+
222
+ if v_tax_id is null then
223
+ continue;
224
+ end if;
225
+
226
+ insert into tax_postings (tax_id, company_id, document_kind, posting_type,
227
+ factor_percent, account_id, declaration_box,
228
+ box_factor_percent, report_code, sequence)
229
+ select v_tax_id, p_company_id, tp.document_kind, tp.posting_type,
230
+ tp.factor_percent, account_id_by_code(p_company_id, tp.account_code),
231
+ tp.declaration_box, tp.box_factor_percent, tp.report_code, tp.sequence
232
+ from tax_posting_templates tp
233
+ where tp.tax_template_id = r.id
234
+ order by tp.sequence;
235
+ end loop;
236
+
237
+ -- 5. Roles.
238
+ select * into v_defaults from country_defaults where country = p_country;
239
+ if found then
240
+ update companies c
241
+ set receivable_account_id = coalesce(c.receivable_account_id, account_id_by_code(p_company_id, v_defaults.receivable_code)),
242
+ payable_account_id = coalesce(c.payable_account_id, account_id_by_code(p_company_id, v_defaults.payable_code)),
243
+ suspense_account_id = coalesce(c.suspense_account_id, account_id_by_code(p_company_id, v_defaults.suspense_code)),
244
+ rounding_account_id = coalesce(c.rounding_account_id, account_id_by_code(p_company_id, v_defaults.rounding_code)),
245
+ retained_earnings_account_id = coalesce(c.retained_earnings_account_id, account_id_by_code(p_company_id, v_defaults.retained_earnings_code)),
246
+ default_sales_account_id = coalesce(c.default_sales_account_id, account_id_by_code(p_company_id, v_defaults.sales_account_code)),
247
+ default_purchase_account_id = coalesce(c.default_purchase_account_id, account_id_by_code(p_company_id, v_defaults.purchase_account_code)),
248
+ sales_journal_id = coalesce(c.sales_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.sales_journal_code)),
249
+ purchase_journal_id = coalesce(c.purchase_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.purchase_journal_code)),
250
+ miscellaneous_journal_id = coalesce(c.miscellaneous_journal_id, (select id from journals where company_id = p_company_id and code = v_defaults.misc_journal_code))
251
+ where c.id = p_company_id;
252
+
253
+ -- 6. The financial journals point at their account.
254
+ update journals j
255
+ set default_account_id = account_id_by_code(p_company_id, v_defaults.bank_account_code)
256
+ where j.company_id = p_company_id
257
+ and j.journal_type = 'bank'
258
+ and j.default_account_id is null
259
+ and v_defaults.bank_account_code is not null;
260
+
261
+ update journals j
262
+ set default_account_id = account_id_by_code(p_company_id, v_defaults.cash_account_code)
263
+ where j.company_id = p_company_id
264
+ and j.journal_type = 'cash'
265
+ and j.default_account_id is null
266
+ and v_defaults.cash_account_code is not null;
267
+ end if;
268
+
269
+ -- 7. What was copied, from which version, and on which chart.
270
+ v_version := coalesce((select version from country_packs where country = p_country), '1.0.0');
271
+
272
+ insert into company_packs (company_id, country, version, chart_code)
273
+ values (p_company_id, p_country, v_version, v_chart)
274
+ on conflict (company_id, country) do update
275
+ set version = excluded.version,
276
+ chart_code = excluded.chart_code,
277
+ upgraded_at = now()
278
+ where company_packs.version is distinct from excluded.version
279
+ or company_packs.chart_code is distinct from excluded.chart_code;
280
+ end;
281
+ $$;
282
+
283
+ -- ---------------------------------------------------------------------------
284
+ -- Writing them
285
+ --
286
+ -- A patch rather than a row, and jsonb rather than seven arguments, for one
287
+ -- reason: with arguments there is no way to tell "leave the timezone alone"
288
+ -- from "clear the timezone". A key that is present is written — null
289
+ -- included, which is how a preference is cleared — and a key that is absent
290
+ -- is untouched. A key nobody declared is refused rather than dropped on the
291
+ -- floor, because a preference that silently did not save is worse than an
292
+ -- error.
293
+ --
294
+ -- It writes the caller's own row and no other: `auth.uid()` is the key, and
295
+ -- it is not an argument.
296
+ -- ---------------------------------------------------------------------------
297
+
298
+ create or replace function set_preferences(p_patch jsonb)
299
+ returns user_preferences
300
+ language plpgsql
301
+ as $$
302
+ declare
303
+ v_known text[] := array['preferred_company_id', 'language', 'timezone',
304
+ 'date_display_format', 'number_display_format', 'theme'];
305
+ v_unknown text;
306
+ v_row user_preferences%rowtype;
307
+ begin
308
+ if auth.uid() is null then
309
+ raise exception 'no_user: preferences belong to a signed-in user';
310
+ end if;
311
+ if p_patch is null or jsonb_typeof(p_patch) <> 'object' then
312
+ raise exception 'bad_patch: set_preferences takes an object of the preferences to change';
313
+ end if;
314
+
315
+ select k into v_unknown
316
+ from jsonb_object_keys(p_patch) as k
317
+ where k <> all (v_known)
318
+ limit 1;
319
+ if v_unknown is not null then
320
+ raise exception 'unknown_preference: % is not a preference this installation keeps (%)',
321
+ v_unknown, array_to_string(v_known, ', ');
322
+ end if;
323
+
324
+ insert into user_preferences (user_id, preferred_company_id, language, timezone,
325
+ date_display_format, number_display_format, theme)
326
+ values (auth.uid(),
327
+ nullif(p_patch ->> 'preferred_company_id', '')::uuid,
328
+ nullif(p_patch ->> 'language', ''),
329
+ nullif(p_patch ->> 'timezone', ''),
330
+ nullif(p_patch ->> 'date_display_format', ''),
331
+ nullif(p_patch ->> 'number_display_format', ''),
332
+ nullif(p_patch ->> 'theme', ''))
333
+ on conflict (user_id) do update set
334
+ preferred_company_id = case when p_patch ? 'preferred_company_id'
335
+ then nullif(p_patch ->> 'preferred_company_id', '')::uuid
336
+ else user_preferences.preferred_company_id end,
337
+ language = case when p_patch ? 'language'
338
+ then nullif(p_patch ->> 'language', '')
339
+ else user_preferences.language end,
340
+ timezone = case when p_patch ? 'timezone'
341
+ then nullif(p_patch ->> 'timezone', '')
342
+ else user_preferences.timezone end,
343
+ date_display_format = case when p_patch ? 'date_display_format'
344
+ then nullif(p_patch ->> 'date_display_format', '')
345
+ else user_preferences.date_display_format end,
346
+ number_display_format = case when p_patch ? 'number_display_format'
347
+ then nullif(p_patch ->> 'number_display_format', '')
348
+ else user_preferences.number_display_format end,
349
+ theme = case when p_patch ? 'theme'
350
+ then nullif(p_patch ->> 'theme', '')
351
+ else user_preferences.theme end
352
+ returning * into v_row;
353
+
354
+ return v_row;
355
+ end;
356
+ $$;
357
+
358
+ comment on function set_preferences(jsonb) is
359
+ 'Writes the signed-in user''s preferences. A key that is present is written, null included; a key that is absent is left alone; a key nobody declared is refused.';
360
+
361
+ -- ---------------------------------------------------------------------------
362
+ -- Row level security
363
+ --
364
+ -- A preference is nobody else's business — not another member's, not an
365
+ -- instance administrator's. One policy, own row, both ways.
366
+ -- ---------------------------------------------------------------------------
367
+
368
+ alter table user_preferences enable row level security;
369
+
370
+ create policy user_preferences_own on user_preferences
371
+ for all using (user_id = auth.uid()) with check (user_id = auth.uid());
372
+
373
+ comment on policy user_preferences_own on user_preferences is
374
+ 'Yours and nobody else''s, including an administrator''s.';
375
+
376
+ revoke execute on all functions in schema public from public;