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,188 @@
1
+ -- Ekwo OS — a guard that answers NULL is a guard that never fires.
2
+ --
3
+ -- `company_role()` returns NULL for somebody who is not a member of the
4
+ -- company, which is the honest answer to "which preset are they on". Two
5
+ -- helpers were built straight on top of it and inherited the NULL:
6
+ --
7
+ -- is_company_owner(c) = company_role(c) = 'owner'
8
+ -- can_write_company(c) = company_role(c) in ('owner', 'accountant')
9
+ --
10
+ -- Inside a policy that is harmless — `USING (NULL)` admits nothing. Inside a
11
+ -- procedural guard it is the opposite of harmless:
12
+ --
13
+ -- if not is_company_owner(p_company_id) then raise …
14
+ --
15
+ -- `not NULL` is NULL, `if NULL then` does not branch, and the stranger walks
16
+ -- past the exception into the body of a SECURITY DEFINER function. That is
17
+ -- exactly what `enable_module()` and `disable_module()` did, on a table —
18
+ -- `company_modules` — that has no write policy at all, so the function was
19
+ -- the only door and the door was open.
20
+ --
21
+ -- Two changes, because either alone would leave the trap in place for the
22
+ -- next function:
23
+ --
24
+ -- 1. the two helpers answer `false` where they answered NULL, so the shape
25
+ -- `if not <helper>(…)` is safe wherever somebody writes it next;
26
+ -- 2. the two module functions stop asking about a *role* and ask about the
27
+ -- capability the act needs — `company.write`, which the owner preset
28
+ -- holds and the accountant preset does not, so who is refused today is
29
+ -- exactly who was refused before the bug.
30
+ --
31
+ -- `has_capability()` has always coalesced to false, which is why every guard
32
+ -- written on it was sound. A test in `hardening.test.ts` now refuses any
33
+ -- function body that tests a helper which can answer NULL.
34
+ --
35
+ -- ---------------------------------------------------------------------------
36
+ -- The installer, named rather than inferred
37
+ --
38
+ -- A guard that asks for a capability has to let the installation itself
39
+ -- through: `ekwo migrate`, the seeds and `ekwo module enable` hold a database
40
+ -- connection and no session, so `auth.uid()` is null and `has_capability()`
41
+ -- says no to everything. Until now the schema wrote that exemption as
42
+ -- `auth.uid() is not null and not has_capability(…)`, which reads as "a
43
+ -- signed-in caller is checked" and means "an unsigned caller is not".
44
+ --
45
+ -- `is_installer()` states it instead, and states it narrowly: the runner sets
46
+ -- `ekwo.installing` on the connection it opened, nothing else sets it, and it
47
+ -- is refused outright when a machine key is presenting itself or when there is
48
+ -- a session. A caller that reaches the database through PostgREST cannot set
49
+ -- a GUC, and a caller holding a key has `ekwo.api_key` set by `use_api_key()`
50
+ -- for the length of its transaction. The next migration moves the whole
51
+ -- `auth.uid() is not null` family onto it.
52
+ -- ---------------------------------------------------------------------------
53
+
54
+ create or replace function is_installer()
55
+ returns boolean
56
+ language sql
57
+ stable
58
+ as $$
59
+ select auth.uid() is null
60
+ and nullif(current_setting('ekwo.api_key', true), '') is null
61
+ and current_setting('ekwo.installing', true) = 'on';
62
+ $$;
63
+
64
+ comment on function is_installer() is
65
+ 'Whether the caller is the installation itself — the migration runner, the seeds, the CLI — rather than a person or a machine key. Set by the runner on its own connection; a session or a key can never be it.';
66
+
67
+ -- ---------------------------------------------------------------------------
68
+ -- The two helpers that could answer NULL
69
+ -- ---------------------------------------------------------------------------
70
+
71
+ create or replace function is_company_owner(p_company_id uuid)
72
+ returns boolean
73
+ language sql
74
+ stable
75
+ as $$
76
+ select coalesce(company_role(p_company_id) = 'owner', false);
77
+ $$;
78
+
79
+ comment on function is_company_owner(uuid) is
80
+ 'Whether the current user is on the owner preset of a company. False, never NULL, for somebody who is not a member — a guard written as `if not is_company_owner(…)` has to fire for a stranger.';
81
+
82
+ create or replace function can_write_company(p_company_id uuid)
83
+ returns boolean
84
+ language sql
85
+ stable
86
+ as $$
87
+ select coalesce(has_capability(p_company_id, 'entries.write'), false);
88
+ $$;
89
+
90
+ comment on function can_write_company(uuid) is
91
+ 'Whether the current caller may write the books of a company. One capability, not a role, and false rather than NULL for a stranger.';
92
+
93
+ -- ---------------------------------------------------------------------------
94
+ -- Enabling and disabling a module
95
+ --
96
+ -- Republished whole: the body is the one from 20260913074512 with the guard
97
+ -- rewritten. `company_modules` still has no write policy, so these two
98
+ -- functions are still the only way in.
99
+ -- ---------------------------------------------------------------------------
100
+
101
+ create or replace function enable_module(
102
+ p_company_id uuid,
103
+ p_code text,
104
+ p_settings jsonb default null
105
+ )
106
+ returns company_modules
107
+ language plpgsql
108
+ security definer
109
+ set search_path = public, pg_temp
110
+ as $$
111
+ declare
112
+ v_module modules%rowtype;
113
+ v_row company_modules%rowtype;
114
+ begin
115
+ if not is_installer() and not has_capability(p_company_id, 'company.write') then
116
+ raise exception 'not_allowed: enabling a module on this company needs company.write'
117
+ using errcode = '42501';
118
+ end if;
119
+
120
+ select * into v_module from modules where code = p_code;
121
+ if not found then
122
+ raise exception 'unknown_module: % is not installed on this instance; apply its migrations first', p_code;
123
+ end if;
124
+
125
+ if v_module.status = 'draft' and not module_is_enabled(p_company_id, p_code) then
126
+ raise exception 'module_not_available: % is still a draft on this installation', p_code;
127
+ end if;
128
+ if v_module.status = 'deprecated' and not module_is_enabled(p_company_id, p_code) then
129
+ raise exception 'module_deprecated: % is deprecated and takes no new company', p_code;
130
+ end if;
131
+
132
+ insert into company_modules (company_id, module_code, enabled_by, settings)
133
+ values (p_company_id, p_code, auth.uid(), coalesce(p_settings, '{}'::jsonb))
134
+ on conflict (company_id, module_code) do update
135
+ set settings = coalesce(p_settings, company_modules.settings)
136
+ returning * into v_row;
137
+
138
+ return v_row;
139
+ end;
140
+ $$;
141
+
142
+ comment on function enable_module(uuid, text, jsonb) is
143
+ 'Enables a module on a company, and updates its settings when it is already enabled. Needs company.write, checked here because the table has no write policy.';
144
+
145
+ create or replace function disable_module(p_company_id uuid, p_code text)
146
+ returns void
147
+ language plpgsql
148
+ security definer
149
+ set search_path = public, pg_temp
150
+ as $$
151
+ declare
152
+ v_module modules%rowtype;
153
+ v_check regprocedure;
154
+ v_reason text;
155
+ begin
156
+ if not is_installer() and not has_capability(p_company_id, 'company.write') then
157
+ raise exception 'not_allowed: disabling a module on this company needs company.write'
158
+ using errcode = '42501';
159
+ end if;
160
+
161
+ select * into v_module from modules where code = p_code;
162
+ if not found then
163
+ raise exception 'unknown_module: % is not installed on this instance', p_code;
164
+ end if;
165
+
166
+ if not module_is_enabled(p_company_id, p_code) then
167
+ return;
168
+ end if;
169
+
170
+ v_check := to_regprocedure(format('%I.can_disable(uuid)', v_module.schema_name));
171
+ if v_check is not null then
172
+ execute format('select %I.can_disable($1)', v_module.schema_name)
173
+ into v_reason using p_company_id;
174
+ if v_reason is not null then
175
+ raise exception 'module_holds_data: % cannot be disabled on this company — %', p_code, v_reason
176
+ using errcode = '55006';
177
+ end if;
178
+ end if;
179
+
180
+ delete from company_modules
181
+ where company_id = p_company_id and module_code = p_code;
182
+ end;
183
+ $$;
184
+
185
+ comment on function disable_module(uuid, text) is
186
+ 'Disables a module on a company, unless the module says it still holds data — `<schema>.can_disable(company)` returning a sentence refuses, returning null allows. Nothing the module wrote is deleted. Needs company.write.';
187
+
188
+ revoke execute on all functions in schema public from public;
@@ -0,0 +1,462 @@
1
+ -- Ekwo OS — "there is no session" was never a synonym for "this is the installer".
2
+ --
3
+ -- Eleven functions and triggers were written in this shape:
4
+ --
5
+ -- if auth.uid() is not null and not has_capability(…) then raise …
6
+ --
7
+ -- It reads as "a signed-in caller is checked". What it *means* is "a caller
8
+ -- with no session is not checked at all", and that was fine for exactly as
9
+ -- long as the only caller with no session was `ekwo migrate`.
10
+ --
11
+ -- Then `20260913085932` added machine keys. A key is deliberately not a
12
+ -- session: `use_api_key()` puts its fingerprint in `ekwo.api_key` and
13
+ -- `auth.uid()` stays null, which is the whole design — a key is answered for
14
+ -- by `has_capability()` and by nothing else. Except here. Every one of these
15
+ -- guards read the null and stood aside, so a key issued with
16
+ -- `["entries.read"]` could post an entry, book a document, close a financial
17
+ -- year, draw a number, match, invite a member, revoke somebody else's
18
+ -- invitation, create a company, take over a numbered book — and issue itself
19
+ -- a second key carrying every capability of the installation. The narrowest
20
+ -- caller in the schema was the widest.
21
+ --
22
+ -- The exemption is real and has to stay: the migration runner, the seeds and
23
+ -- the CLI hold a database connection and no session, and a guard that asks
24
+ -- for a capability would refuse the installation itself. So it is *named*
25
+ -- instead of inferred. `is_installer()` (20260913101536) is true only when
26
+ -- the runner set `ekwo.installing` on its own connection, and it is false
27
+ -- outright when there is a session or when a key is presenting itself. A
28
+ -- caller that reaches the database through PostgREST cannot set a GUC, and a
29
+ -- caller holding a key has `ekwo.api_key` set for the length of its
30
+ -- transaction — so neither can ever be the installer, whatever they do.
31
+ --
32
+ -- `has_capability()` is now the only authority over both people and machines,
33
+ -- which is what ST13 said it was.
34
+ --
35
+ -- `post_entry()` is the one member of the family that was *not* a hole, and
36
+ -- it changes for the other reason. Its test reads
37
+ -- `auth.uid() is not null and has_capability(…, 'entries.import')` as a
38
+ -- requirement rather than as an exemption, so a key was already refused — but
39
+ -- so was a key that legitimately held `entries.import`. It now asks for the
40
+ -- capability and nothing else, which admits the key and keeps the installer
41
+ -- bound by a rule that is a law and not a permission.
42
+ --
43
+ -- Every function below is republished whole, with its body unchanged but for
44
+ -- that one line. They come from `20260913083216` (the three triggers and the
45
+ -- two counters), `20260913085436` (the counter that reads the pack),
46
+ -- `20260913085932`, `20260913083901`, `20260913090216` and `20260913092527`.
47
+
48
+ create or replace function assert_may_post_entry()
49
+ returns trigger
50
+ language plpgsql
51
+ as $$
52
+ begin
53
+ if not is_installer() and not has_capability(new.company_id, 'entries.post') then
54
+ raise exception 'not_allowed: posting an entry in this company needs entries.post'
55
+ using errcode = '42501';
56
+ end if;
57
+ return new;
58
+ end;
59
+ $$;
60
+
61
+ create or replace function assert_may_post_document()
62
+ returns trigger
63
+ language plpgsql
64
+ as $$
65
+ begin
66
+ if not is_installer() and not has_capability(new.company_id, 'documents.post') then
67
+ raise exception 'not_allowed: booking a document in this company needs documents.post'
68
+ using errcode = '42501';
69
+ end if;
70
+ return new;
71
+ end;
72
+ $$;
73
+
74
+ create or replace function assert_may_close_year()
75
+ returns trigger
76
+ language plpgsql
77
+ as $$
78
+ begin
79
+ if not is_installer() and not has_capability(new.company_id, 'year_end.close') then
80
+ raise exception 'not_allowed: closing or re-opening a financial year needs year_end.close'
81
+ using errcode = '42501';
82
+ end if;
83
+ return new;
84
+ end;
85
+ $$;
86
+
87
+ create or replace function next_matching_number(p_company_id uuid)
88
+ returns text
89
+ language plpgsql
90
+ security definer
91
+ set search_path = public, pg_temp
92
+ as $$
93
+ declare
94
+ v_number integer;
95
+ begin
96
+ if not is_installer() and not has_capability(p_company_id, 'reconcile.write') then
97
+ raise exception 'not_allowed: matching in this company needs reconcile.write'
98
+ using errcode = '42501';
99
+ end if;
100
+
101
+ insert into matching_sequences (company_id, last_number)
102
+ values (p_company_id, 1)
103
+ on conflict (company_id)
104
+ do update set last_number = matching_sequences.last_number + 1
105
+ returning last_number into v_number;
106
+
107
+ return 'A' || lpad(v_number::text, 4, '0');
108
+ end;
109
+ $$;
110
+
111
+ create or replace function next_entry_number(p_journal_id uuid, p_date date)
112
+ returns text
113
+ language plpgsql
114
+ security definer
115
+ set search_path = public, pg_temp
116
+ as $$
117
+ declare
118
+ v_code text;
119
+ v_company uuid;
120
+ v_format text;
121
+ v_period smallint;
122
+ v_number integer;
123
+ begin
124
+ select j.code, j.company_id into v_code, v_company from journals j where j.id = p_journal_id;
125
+ if v_code is null then
126
+ raise exception 'unknown_journal: journal % does not exist', p_journal_id;
127
+ end if;
128
+
129
+ if not is_installer() and not has_capability(v_company, 'entries.post') then
130
+ raise exception 'not_allowed: drawing a number in this company needs entries.post'
131
+ using errcode = '42501';
132
+ end if;
133
+
134
+ select number_format into v_format from numbering_rules(v_company);
135
+ if v_format is null or v_format = '' then
136
+ raise exception 'no_number_format: the country pack of this company declares no documents.number_format, and there is no default to fall back on';
137
+ end if;
138
+
139
+ -- A pattern that carries the year restarts with it; one that does not is a
140
+ -- single series, kept under the period that is not a year.
141
+ v_period := case when v_format ~ '\{(YYYY|YY)\}'
142
+ then extract(year from p_date)::smallint
143
+ else 0::smallint end;
144
+
145
+ insert into journal_sequences (journal_id, year, last_number)
146
+ values (p_journal_id, v_period, 1)
147
+ on conflict (journal_id, year)
148
+ do update set last_number = journal_sequences.last_number + 1
149
+ returning last_number into v_number;
150
+
151
+ return format_number(v_format, v_code, p_date, v_number);
152
+ end;
153
+ $$;
154
+
155
+ create or replace function create_api_key(
156
+ p_company_id uuid,
157
+ p_name text,
158
+ p_capabilities jsonb,
159
+ p_expires_at timestamptz default null
160
+ )
161
+ returns table (api_key_id uuid, secret text, prefix text, expires_at timestamptz)
162
+ language plpgsql
163
+ security definer
164
+ set search_path = public, pg_temp
165
+ as $$
166
+ declare
167
+ v_caps text[] := coalesce(
168
+ (select array_agg(value) from jsonb_array_elements_text(coalesce(p_capabilities, '[]'::jsonb)) as value),
169
+ '{}');
170
+ v_prefix text := left(replace(gen_random_uuid()::text, '-', ''), 12);
171
+ v_secret text;
172
+ v_cap text;
173
+ v_row api_keys%rowtype;
174
+ begin
175
+ if not is_installer() and not has_capability(p_company_id, 'members.manage') then
176
+ raise exception 'not_allowed: issuing a key for this company needs members.manage'
177
+ using errcode = '42501';
178
+ end if;
179
+
180
+ if cardinality(v_caps) = 0 then
181
+ raise exception 'api_key_without_capability: name what this key may do; a key that may do nothing is a secret to look after for no reason';
182
+ end if;
183
+
184
+ foreach v_cap in array v_caps loop
185
+ if v_cap not in (select code from capabilities) then
186
+ raise exception 'unknown_capability: % is not a capability of this installation', v_cap;
187
+ end if;
188
+ -- Nobody mints a key stronger than they are. An owner issuing a key is
189
+ -- bounded by their own capabilities, which is also what makes revoking a
190
+ -- person's capability revoke the keys they left behind.
191
+ if not is_installer() and not has_capability(p_company_id, v_cap) then
192
+ raise exception 'not_allowed: you do not hold % yourself, so you cannot put it on a key', v_cap
193
+ using errcode = '42501';
194
+ end if;
195
+ end loop;
196
+
197
+ v_secret := 'ekwo_' || v_prefix || '_' ||
198
+ replace(gen_random_uuid()::text, '-', '') ||
199
+ replace(gen_random_uuid()::text, '-', '');
200
+
201
+ insert into api_keys (company_id, name, prefix, key_hash, capabilities, created_by, expires_at)
202
+ values (p_company_id, p_name, v_prefix,
203
+ encode(sha256(convert_to(v_secret, 'UTF8')), 'hex'),
204
+ v_caps, auth.uid(), p_expires_at)
205
+ returning * into v_row;
206
+
207
+ return query select v_row.id, v_secret, v_row.prefix, v_row.expires_at;
208
+ end;
209
+ $$;
210
+
211
+ create or replace function revoke_api_key(p_api_key_id uuid)
212
+ returns api_keys
213
+ language plpgsql
214
+ security definer
215
+ set search_path = public, pg_temp
216
+ as $$
217
+ declare
218
+ v_row api_keys%rowtype;
219
+ begin
220
+ select * into v_row from api_keys where id = p_api_key_id;
221
+ if v_row.id is null then
222
+ raise exception 'unknown_api_key: no key with that id';
223
+ end if;
224
+ if not is_installer() and not has_capability(v_row.company_id, 'members.manage') then
225
+ raise exception 'not_allowed: withdrawing a key needs members.manage'
226
+ using errcode = '42501';
227
+ end if;
228
+
229
+ update api_keys set revoked_at = coalesce(revoked_at, now())
230
+ where id = p_api_key_id
231
+ returning * into v_row;
232
+
233
+ return v_row;
234
+ end;
235
+ $$;
236
+
237
+ create or replace function invite_member(
238
+ p_company_id uuid,
239
+ p_email text,
240
+ p_role member_role default 'viewer',
241
+ -- A list crosses as JSON, the way `opening_balance` takes its lines: over
242
+ -- PostgREST an argument is a JSON body, and a Postgres driver handed a
243
+ -- JavaScript array builds an array literal instead. One spelling that both
244
+ -- routes read the same way.
245
+ p_capabilities jsonb default '[]'::jsonb,
246
+ p_valid_for interval default interval '14 days'
247
+ )
248
+ returns table (invitation_id uuid, token text, expires_at timestamptz)
249
+ language plpgsql
250
+ security definer
251
+ set search_path = public, pg_temp
252
+ as $$
253
+ declare
254
+ v_email text := lower(trim(p_email));
255
+ v_token text;
256
+ v_unknown text;
257
+ v_caps text[] := coalesce(
258
+ (select array_agg(value) from jsonb_array_elements_text(coalesce(p_capabilities, '[]'::jsonb)) as value),
259
+ '{}');
260
+ v_row company_invitations%rowtype;
261
+ begin
262
+ if not is_installer() and not has_capability(p_company_id, 'members.manage')
263
+ and not is_instance_admin() then
264
+ raise exception 'not_allowed: inviting somebody into this company needs members.manage'
265
+ using errcode = '42501';
266
+ end if;
267
+
268
+ if v_email is null or v_email not like '%_@_%' then
269
+ raise exception 'bad_email: % is not an address an invitation can be sent to', p_email;
270
+ end if;
271
+
272
+ select c into v_unknown
273
+ from unnest(v_caps) as c
274
+ where c not in (select code from capabilities)
275
+ limit 1;
276
+ if v_unknown is not null then
277
+ raise exception 'unknown_capability: % is not a capability of this installation', v_unknown;
278
+ end if;
279
+
280
+ -- Re-inviting the same address replaces the pending invitation rather than
281
+ -- leaving two tokens alive for one seat.
282
+ update company_invitations
283
+ set revoked_at = now()
284
+ where company_id = p_company_id
285
+ and email = v_email
286
+ and accepted_at is null
287
+ and revoked_at is null;
288
+
289
+ v_token := replace(gen_random_uuid()::text, '-', '') ||
290
+ replace(gen_random_uuid()::text, '-', '');
291
+
292
+ insert into company_invitations (company_id, email, role, capabilities_granted,
293
+ token_hash, invited_by, expires_at)
294
+ values (p_company_id, v_email, p_role, v_caps,
295
+ encode(sha256(convert_to(v_token, 'UTF8')), 'hex'), auth.uid(), now() + p_valid_for)
296
+ returning * into v_row;
297
+
298
+ return query select v_row.id, v_token, v_row.expires_at;
299
+ end;
300
+ $$;
301
+
302
+ create or replace function revoke_invitation(p_invitation_id uuid)
303
+ returns company_invitations
304
+ language plpgsql
305
+ security definer
306
+ set search_path = public, pg_temp
307
+ as $$
308
+ declare
309
+ v_row company_invitations%rowtype;
310
+ begin
311
+ select * into v_row from company_invitations where id = p_invitation_id;
312
+ if v_row.id is null then
313
+ raise exception 'unknown_invitation: no invitation with that id';
314
+ end if;
315
+
316
+ if not is_installer() and not has_capability(v_row.company_id, 'members.manage')
317
+ and not is_instance_admin() then
318
+ raise exception 'not_allowed: withdrawing an invitation needs members.manage'
319
+ using errcode = '42501';
320
+ end if;
321
+
322
+ if v_row.accepted_at is not null then
323
+ raise exception 'invitation_already_accepted: it became a membership on %; remove the member instead',
324
+ v_row.accepted_at;
325
+ end if;
326
+
327
+ update company_invitations set revoked_at = coalesce(revoked_at, now())
328
+ where id = p_invitation_id
329
+ returning * into v_row;
330
+
331
+ return v_row;
332
+ end;
333
+ $$;
334
+
335
+ create or replace function create_company(
336
+ p_name text,
337
+ p_country char(2),
338
+ p_currency_code char(3) default null,
339
+ p_language char(2) default null,
340
+ p_chart_code text default null,
341
+ p_fiscal_year integer default null,
342
+ p_fiscal_year_start date default null,
343
+ p_owner_user_id uuid default null
344
+ )
345
+ returns companies
346
+ language plpgsql
347
+ as $$
348
+ declare
349
+ v_defaults country_defaults%rowtype;
350
+ v_currency char(3);
351
+ v_language char(2);
352
+ v_owner uuid := coalesce(p_owner_user_id, auth.uid());
353
+ v_year integer := coalesce(p_fiscal_year, extract(year from coalesce(p_fiscal_year_start, current_date))::integer);
354
+ v_bounds record;
355
+ v_company companies%rowtype;
356
+ begin
357
+ if not is_installer() and not is_instance_admin() then
358
+ raise exception 'not_instance_admin: creating a company is an instance-level act'
359
+ using errcode = '42501';
360
+ end if;
361
+
362
+ select * into v_defaults from country_defaults where country = p_country;
363
+
364
+ -- The currency and the language have to be settled before the insert: both
365
+ -- columns are not null with a default, so there is no later moment at which
366
+ -- they are empty and the pack could fill them. The pack answers, or the
367
+ -- caller does, and there is no third answer written here.
368
+ v_currency := upper(coalesce(p_currency_code, v_defaults.currency_code));
369
+ if v_currency is null then
370
+ raise exception 'no_currency: the % pack names no currency; name one', p_country;
371
+ end if;
372
+ v_language := lower(coalesce(p_language, v_defaults.language_default));
373
+ if v_language is null then
374
+ raise exception 'no_language: the % pack names no language for its labels; name one', p_country;
375
+ end if;
376
+
377
+ select * into v_bounds from fiscal_year_bounds(p_country, v_year, p_fiscal_year_start);
378
+
379
+ insert into companies (name, country, fiscal_country, currency_code, language)
380
+ values (p_name, p_country, p_country, v_currency, v_language)
381
+ returning * into v_company;
382
+
383
+ if v_owner is not null then
384
+ insert into company_members (company_id, user_id, role)
385
+ values (v_company.id, v_owner, 'owner')
386
+ on conflict (company_id, user_id) do nothing;
387
+ end if;
388
+
389
+ perform install_country_template(v_company.id, p_country, v_language, p_chart_code);
390
+
391
+ insert into fiscal_years (company_id, name, start_date, end_date)
392
+ values (v_company.id, 'FY' || v_year::text, v_bounds.start_date, v_bounds.end_date);
393
+
394
+ select * into v_company from companies where id = v_company.id;
395
+ return v_company;
396
+ end;
397
+ $$;
398
+
399
+ create or replace function post_entry(p_entry_id uuid)
400
+ returns entries
401
+ language plpgsql
402
+ as $$
403
+ declare
404
+ v_entry entries%rowtype;
405
+ v_lines integer;
406
+ v_gapless boolean;
407
+ begin
408
+ select * into v_entry from entries where id = p_entry_id for update;
409
+ if not found then
410
+ raise exception 'unknown_entry: entry % does not exist', p_entry_id;
411
+ end if;
412
+ if v_entry.state = 'posted' then
413
+ return v_entry;
414
+ end if;
415
+ if v_entry.state = 'cancelled' then
416
+ raise exception 'entry_cancelled: entry % cannot be posted', p_entry_id;
417
+ end if;
418
+
419
+ select count(*) into v_lines from entry_lines where entry_id = p_entry_id;
420
+ if v_lines = 0 then
421
+ raise exception 'entry_empty: entry % has no lines', p_entry_id;
422
+ end if;
423
+
424
+ -- Re-read the totals maintained by the line trigger.
425
+ select * into v_entry from entries where id = p_entry_id;
426
+ if v_entry.total_debit <> v_entry.total_credit then
427
+ raise exception 'entry_unbalanced: entry % has debit % and credit %',
428
+ p_entry_id, v_entry.total_debit, v_entry.total_credit;
429
+ end if;
430
+
431
+ -- Where the law forbids a hole, the number comes from the counter and from
432
+ -- nowhere else — unless the caller holds `entries.import`, which is how a
433
+ -- set of books that already has numbers is taken over. A duplicate is
434
+ -- refused either way, by `entries_company_number_idx`.
435
+ if v_entry.number is not null then
436
+ select numbering_gapless into v_gapless from numbering_rules(v_entry.company_id);
437
+ if v_gapless and not has_capability(v_entry.company_id, 'entries.import') then
438
+ raise exception 'numbering_gapless: this country forbids a hole in the sequence, so entry % may not be posted under a number chosen by hand. Leave entries.number empty and the journal counter draws it, or hold entries.import to bring in books that already have numbers',
439
+ p_entry_id;
440
+ end if;
441
+ end if;
442
+
443
+ perform assert_period_open(v_entry.company_id, v_entry.entry_date, true);
444
+
445
+ update entries
446
+ set number = coalesce(number, next_entry_number(journal_id, entry_date)),
447
+ fiscal_year_id = coalesce(fiscal_year_id, fiscal_year_at(company_id, entry_date)),
448
+ state = 'posted',
449
+ posted_at = now()
450
+ where id = p_entry_id
451
+ returning * into v_entry;
452
+
453
+ -- An imported number advances the counter it interrupted, so the next
454
+ -- automatic one continues the series. A number drawn from the counter is
455
+ -- already at it and this is a no-op.
456
+ perform catch_up_journal_sequence(v_entry.journal_id, v_entry.entry_date, v_entry.number);
457
+
458
+ return v_entry;
459
+ end;
460
+ $$;
461
+
462
+ revoke execute on all functions in schema public from public;