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,303 @@
1
+ -- Ekwo OS — a key for a machine, and what it is deliberately not.
2
+ --
3
+ -- Everything in this schema answers to `auth.uid()`, which comes from a JWT
4
+ -- the customer's own GoTrue issued. That is right for a person and impossible
5
+ -- for a script: a nightly import, a till, a bank feed has no browser to sign
6
+ -- in with, and the two usual answers are both wrong. Handing it the
7
+ -- `service_role` key gives it every company of the installation and every
8
+ -- table in it. Creating a fake user for it puts a password in a crontab and
9
+ -- makes the audit trail say a person did it.
10
+ --
11
+ -- So a key is a **third kind of caller**, and it is narrower than both by
12
+ -- construction:
13
+ --
14
+ -- * it belongs to one company and can never reach another;
15
+ -- * it carries an explicit list of capabilities, which the person who
16
+ -- issued it must hold themselves — nobody mints a key stronger than they
17
+ -- are;
18
+ -- * it expires when it is told to, and it is withdrawn by one update;
19
+ -- * it is stored as a sha256 and shown once, like an invitation token.
20
+ --
21
+ -- **How a key authenticates, and why it is this way.** A holder calls
22
+ -- `use_api_key(secret)` at the start of a transaction. That function checks
23
+ -- the hash, refuses a withdrawn or expired key, records the use, and puts the
24
+ -- key's fingerprint in `ekwo.api_key` — transaction-locally, so it is gone
25
+ -- when the transaction ends. `has_capability()` then answers for the key as
26
+ -- well as for the user, and every policy in the schema follows without being
27
+ -- rewritten.
28
+ --
29
+ -- The alternative considered was exchanging a key for a GoTrue session. It
30
+ -- needs GoTrue to know about our keys, which means either a fake user per key
31
+ -- — the thing this is here to avoid — or a service that mints tokens, which
32
+ -- is a second secret to hold. The setting is simpler and its blast radius is
33
+ -- one transaction.
34
+ --
35
+ -- **What it cannot do, stated rather than discovered.** A key is not a
36
+ -- session. `auth.uid()` stays null, so the policies that ask for a signed-in
37
+ -- user rather than for a capability — the reference tables, the company row
38
+ -- itself — stay closed to it. And because PostgREST runs each request in its
39
+ -- own transaction, `use_api_key()` cannot be a separate request over that
40
+ -- route: a key is for a client that holds a connection and opens a
41
+ -- transaction, which is the self-hosted route the MCP server already has.
42
+ --
43
+ -- **The setting cannot be forged into a privilege.** What goes into
44
+ -- `ekwo.api_key` is the hash, not the id, and the hash is only readable by
45
+ -- somebody who already holds `members.manage` on that company — who can
46
+ -- simply issue a key. There is nothing to escalate to.
47
+
48
+ create table api_keys (
49
+ id uuid primary key default gen_random_uuid(),
50
+ company_id uuid not null references companies(id) on delete cascade,
51
+ name text not null,
52
+ -- The visible half, so a list of keys can be read by a human and a leaked
53
+ -- secret can be matched to the row it belongs to.
54
+ prefix text not null,
55
+ key_hash text not null unique,
56
+ capabilities text[] not null,
57
+ created_by uuid,
58
+ created_at timestamptz not null default now(),
59
+ expires_at timestamptz,
60
+ last_used_at timestamptz,
61
+ revoked_at timestamptz,
62
+ constraint api_keys_has_capability check (cardinality(capabilities) > 0),
63
+ constraint api_keys_expiry check (expires_at is null or expires_at > created_at)
64
+ );
65
+
66
+ comment on table api_keys is
67
+ 'Machine access to one company. Hashed at rest, scoped to an explicit list of capabilities, and never wider than the person who issued it.';
68
+ comment on column api_keys.prefix is
69
+ 'The readable head of the secret. It identifies a key without being one.';
70
+ comment on column api_keys.capabilities is
71
+ 'Exactly what this key may do. Not a role: a machine has a job, not a job title.';
72
+ comment on column api_keys.created_by is
73
+ 'auth.users.id of whoever issued it. No foreign key, for the same reason company_members has none.';
74
+
75
+ create index api_keys_company_idx on api_keys (company_id, created_at desc);
76
+ create index api_keys_prefix_idx on api_keys (prefix);
77
+
78
+ -- ---------------------------------------------------------------------------
79
+ -- Issuing one
80
+ -- ---------------------------------------------------------------------------
81
+
82
+ create or replace function create_api_key(
83
+ p_company_id uuid,
84
+ p_name text,
85
+ p_capabilities jsonb,
86
+ p_expires_at timestamptz default null
87
+ )
88
+ returns table (api_key_id uuid, secret text, prefix text, expires_at timestamptz)
89
+ language plpgsql
90
+ security definer
91
+ set search_path = public, pg_temp
92
+ as $$
93
+ declare
94
+ v_caps text[] := coalesce(
95
+ (select array_agg(value) from jsonb_array_elements_text(coalesce(p_capabilities, '[]'::jsonb)) as value),
96
+ '{}');
97
+ v_prefix text := left(replace(gen_random_uuid()::text, '-', ''), 12);
98
+ v_secret text;
99
+ v_cap text;
100
+ v_row api_keys%rowtype;
101
+ begin
102
+ if auth.uid() is not null and not has_capability(p_company_id, 'members.manage') then
103
+ raise exception 'not_allowed: issuing a key for this company needs members.manage'
104
+ using errcode = '42501';
105
+ end if;
106
+
107
+ if cardinality(v_caps) = 0 then
108
+ 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';
109
+ end if;
110
+
111
+ foreach v_cap in array v_caps loop
112
+ if v_cap not in (select code from capabilities) then
113
+ raise exception 'unknown_capability: % is not a capability of this installation', v_cap;
114
+ end if;
115
+ -- Nobody mints a key stronger than they are. An owner issuing a key is
116
+ -- bounded by their own capabilities, which is also what makes revoking a
117
+ -- person's capability revoke the keys they left behind.
118
+ if auth.uid() is not null and not has_capability(p_company_id, v_cap) then
119
+ raise exception 'not_allowed: you do not hold % yourself, so you cannot put it on a key', v_cap
120
+ using errcode = '42501';
121
+ end if;
122
+ end loop;
123
+
124
+ v_secret := 'ekwo_' || v_prefix || '_' ||
125
+ replace(gen_random_uuid()::text, '-', '') ||
126
+ replace(gen_random_uuid()::text, '-', '');
127
+
128
+ insert into api_keys (company_id, name, prefix, key_hash, capabilities, created_by, expires_at)
129
+ values (p_company_id, p_name, v_prefix,
130
+ encode(sha256(convert_to(v_secret, 'UTF8')), 'hex'),
131
+ v_caps, auth.uid(), p_expires_at)
132
+ returning * into v_row;
133
+
134
+ return query select v_row.id, v_secret, v_row.prefix, v_row.expires_at;
135
+ end;
136
+ $$;
137
+
138
+ comment on function create_api_key(uuid, text, jsonb, timestamptz) is
139
+ 'Issues a machine key on one company and returns the secret once. Only the hash is stored, and no capability can be put on a key that the person issuing it does not hold.';
140
+
141
+ -- ---------------------------------------------------------------------------
142
+ -- Recording that a key was used
143
+ --
144
+ -- Its own function because it is its own act: `use_api_key()` calls it, and a
145
+ -- caller that wants to record a use it made through another path can too.
146
+ -- Definer and unconditional — a key that is in use records it whether or not
147
+ -- the call it is about to make succeeds.
148
+ -- ---------------------------------------------------------------------------
149
+
150
+ create or replace function touch_api_key(p_api_key_id uuid)
151
+ returns void
152
+ language sql
153
+ security definer
154
+ set search_path = public, pg_temp
155
+ as $$
156
+ update api_keys set last_used_at = now() where id = p_api_key_id;
157
+ $$;
158
+
159
+ comment on function touch_api_key(uuid) is
160
+ 'Records that a key was used just now. A key that has never been used, and one that has not been used for a year, are both things an operator should be able to see.';
161
+
162
+ -- ---------------------------------------------------------------------------
163
+ -- Presenting one
164
+ -- ---------------------------------------------------------------------------
165
+
166
+ create or replace function use_api_key(p_secret text)
167
+ returns api_keys
168
+ language plpgsql
169
+ security definer
170
+ set search_path = public, pg_temp
171
+ as $$
172
+ declare
173
+ v_row api_keys%rowtype;
174
+ begin
175
+ select * into v_row
176
+ from api_keys
177
+ where key_hash = encode(sha256(convert_to(coalesce(p_secret, ''), 'UTF8')), 'hex');
178
+
179
+ if v_row.id is null then
180
+ raise exception 'unknown_api_key: that is not a key of this installation'
181
+ using errcode = '42501';
182
+ end if;
183
+ if v_row.revoked_at is not null then
184
+ raise exception 'api_key_revoked: that key was withdrawn on %', v_row.revoked_at
185
+ using errcode = '42501';
186
+ end if;
187
+ if v_row.expires_at is not null and v_row.expires_at <= now() then
188
+ raise exception 'api_key_expired: that key expired on %', v_row.expires_at
189
+ using errcode = '42501';
190
+ end if;
191
+
192
+ -- Transaction-local: the key is presented for the work at hand and not for
193
+ -- the life of a connection somebody else may inherit from a pool.
194
+ perform set_config('ekwo.api_key', v_row.key_hash, true);
195
+ perform touch_api_key(v_row.id);
196
+
197
+ return v_row;
198
+ end;
199
+ $$;
200
+
201
+ comment on function use_api_key(text) is
202
+ 'Presents a machine key for the current transaction: has_capability() answers for it until the transaction ends. Refuses a key that is unknown, withdrawn or expired.';
203
+
204
+ create or replace function current_api_key()
205
+ returns api_keys
206
+ language sql
207
+ stable
208
+ security definer
209
+ set search_path = public, pg_temp
210
+ as $$
211
+ select k.* from api_keys k
212
+ where k.key_hash = nullif(current_setting('ekwo.api_key', true), '')
213
+ and k.revoked_at is null
214
+ and (k.expires_at is null or k.expires_at > now());
215
+ $$;
216
+
217
+ comment on function current_api_key() is
218
+ 'The key presented in this transaction, or nothing. What a client reads back to know what it may do.';
219
+
220
+ create or replace function revoke_api_key(p_api_key_id uuid)
221
+ returns api_keys
222
+ language plpgsql
223
+ security definer
224
+ set search_path = public, pg_temp
225
+ as $$
226
+ declare
227
+ v_row api_keys%rowtype;
228
+ begin
229
+ select * into v_row from api_keys where id = p_api_key_id;
230
+ if v_row.id is null then
231
+ raise exception 'unknown_api_key: no key with that id';
232
+ end if;
233
+ if auth.uid() is not null and not has_capability(v_row.company_id, 'members.manage') then
234
+ raise exception 'not_allowed: withdrawing a key needs members.manage'
235
+ using errcode = '42501';
236
+ end if;
237
+
238
+ update api_keys set revoked_at = coalesce(revoked_at, now())
239
+ where id = p_api_key_id
240
+ returning * into v_row;
241
+
242
+ return v_row;
243
+ end;
244
+ $$;
245
+
246
+ comment on function revoke_api_key(uuid) is
247
+ 'Withdraws a key. There is no un-withdraw: a secret that has been out of the building is issued again, not brought back.';
248
+
249
+ -- ---------------------------------------------------------------------------
250
+ -- has_capability, which now answers for a key as well as for a user
251
+ --
252
+ -- The member answer first, because that is the common case and it is the one
253
+ -- a policy on a table of a company is written for. A key is consulted only
254
+ -- when there is no member answer, so presenting a key can never take away
255
+ -- what the signed-in person already had.
256
+ -- ---------------------------------------------------------------------------
257
+
258
+ create or replace function has_capability(p_company_id uuid, p_capability text)
259
+ returns boolean
260
+ language sql
261
+ stable
262
+ security definer
263
+ set search_path = public, pg_temp
264
+ as $$
265
+ select coalesce(
266
+ (select case
267
+ when p_capability = any (m.capabilities_revoked) then false
268
+ when p_capability = any (m.capabilities_granted) then true
269
+ else exists (
270
+ select 1 from role_capabilities rc
271
+ where rc.role = m.role and rc.capability = p_capability
272
+ )
273
+ end
274
+ from company_members m
275
+ where m.company_id = p_company_id
276
+ and m.user_id = auth.uid()),
277
+ (select p_capability = any (k.capabilities)
278
+ from api_keys k
279
+ where k.key_hash = nullif(current_setting('ekwo.api_key', true), '')
280
+ and k.company_id = p_company_id
281
+ and k.revoked_at is null
282
+ and (k.expires_at is null or k.expires_at > now())),
283
+ false);
284
+ $$;
285
+
286
+ comment on function has_capability(uuid, text) is
287
+ 'Whether the current caller may do one named thing in one company — a signed-in member by their preset and their adjustments, or a machine key by its own list. Revoked beats granted, and a non-member holding no key holds nothing.';
288
+
289
+ -- ---------------------------------------------------------------------------
290
+ -- Row level security
291
+ --
292
+ -- Reading the keys of a company is administering its members, and nothing
293
+ -- writes the table through the API: the four functions above are the way in.
294
+ -- ---------------------------------------------------------------------------
295
+
296
+ alter table api_keys enable row level security;
297
+
298
+ create policy api_keys_select on api_keys
299
+ for select using (
300
+ has_capability(company_id, 'members.manage') or is_instance_admin()
301
+ );
302
+
303
+ revoke execute on all functions in schema public from public;
@@ -0,0 +1,159 @@
1
+ -- Ekwo OS — the first financial year is a parameter too.
2
+ --
3
+ -- `ekwo init` opened the first year on 1 January and closed it on 31
4
+ -- December, in two string literals. That is right for Belgium and for France
5
+ -- and wrong for the United Kingdom, for India, for Australia and for every
6
+ -- company whose year does not follow the calendar — and the country model has
7
+ -- carried the answer since P0-7: `country_defaults.fiscal_year_default`, one
8
+ -- of `calendar`, `april`, `july` or `october`, with no default of its own.
9
+ --
10
+ -- This is its reader. It is in the schema rather than in the CLI because
11
+ -- three callers ask the same question — `ekwo init`, the MCP server creating
12
+ -- a company, and whoever adds the year after that — and three answers to it
13
+ -- is how a company ends up with two overlapping first years.
14
+ --
15
+ -- **A day, not a duration.** The bounds are the opening day and the day
16
+ -- before the same day next year, so a year opened on 1 April closes on 31
17
+ -- March, and a leap day changes nothing. The 52/53-week year some retailers
18
+ -- keep is not this function's business: `fiscal_years` takes any two dates,
19
+ -- and a company that keeps one names them.
20
+ --
21
+ -- **No fallback.** A pack that says nothing gets `no_fiscal_year_default`,
22
+ -- naming the field it has to fill and the flag that answers for it. Opening
23
+ -- somebody's first year on a date nobody chose is the kind of wrong that is
24
+ -- found a year later, in a closing.
25
+
26
+ create or replace function fiscal_year_bounds(
27
+ p_country char(2),
28
+ p_year integer,
29
+ p_start date default null,
30
+ out start_date date,
31
+ out end_date date
32
+ )
33
+ language plpgsql
34
+ stable
35
+ as $$
36
+ declare
37
+ v_opening text;
38
+ v_month integer;
39
+ begin
40
+ if p_start is not null then
41
+ start_date := p_start;
42
+ else
43
+ select fiscal_year_default into v_opening
44
+ from country_defaults where country = p_country;
45
+
46
+ if v_opening is null then
47
+ raise exception 'no_fiscal_year_default: the % pack declares no defaults.fiscal_year_default, so there is no month to open the first year on. Name the first day instead', p_country;
48
+ end if;
49
+
50
+ v_month := case v_opening
51
+ when 'calendar' then 1
52
+ when 'april' then 4
53
+ when 'july' then 7
54
+ when 'october' then 10
55
+ end;
56
+ if v_month is null then
57
+ raise exception 'unknown_fiscal_year_default: % is not one of calendar, april, july, october', v_opening;
58
+ end if;
59
+
60
+ start_date := make_date(p_year, v_month, 1);
61
+ end if;
62
+
63
+ end_date := (start_date + interval '1 year' - interval '1 day')::date;
64
+ end;
65
+ $$;
66
+
67
+ comment on function fiscal_year_bounds(char, integer, date) is
68
+ 'The first and last day of a financial year opening in a given calendar year, on the month the country pack declares — or on a day the caller names. Raises rather than assuming January.';
69
+
70
+ -- ---------------------------------------------------------------------------
71
+ -- create_company — a company, its owner, its chart and its first year
72
+ --
73
+ -- The four statements a client had to get right in the right order, with the
74
+ -- two that carry a rule delegated rather than restated:
75
+ -- `install_country_template()` copies the chart, the journals and the taxes,
76
+ -- and `fiscal_year_bounds()` decides the two dates. What is left here is the
77
+ -- sequence.
78
+ --
79
+ -- `ekwo init` keeps its own sequence, and deliberately: the installer is
80
+ -- check-then-act — it reports "already there" for every step and can be run
81
+ -- twice on a half-finished project — where this function creates or raises.
82
+ -- What they share is every rule; what differs is what they do when the thing
83
+ -- already exists.
84
+ --
85
+ -- Creating a company is an instance-level act, which is the policy on
86
+ -- `companies` and the doctrine behind it: an administrator creates companies
87
+ -- and invites members, and is not thereby on anybody's books. So the creator
88
+ -- is made the first member here, explicitly, because being able to create a
89
+ -- company is not being able to read one.
90
+ -- ---------------------------------------------------------------------------
91
+
92
+ create or replace function create_company(
93
+ p_name text,
94
+ p_country char(2),
95
+ p_currency_code char(3) default null,
96
+ p_language char(2) default null,
97
+ p_chart_code text default null,
98
+ p_fiscal_year integer default null,
99
+ p_fiscal_year_start date default null,
100
+ p_owner_user_id uuid default null
101
+ )
102
+ returns companies
103
+ language plpgsql
104
+ as $$
105
+ declare
106
+ v_defaults country_defaults%rowtype;
107
+ v_currency char(3);
108
+ v_language char(2);
109
+ v_owner uuid := coalesce(p_owner_user_id, auth.uid());
110
+ v_year integer := coalesce(p_fiscal_year, extract(year from coalesce(p_fiscal_year_start, current_date))::integer);
111
+ v_bounds record;
112
+ v_company companies%rowtype;
113
+ begin
114
+ if auth.uid() is not null and not is_instance_admin() then
115
+ raise exception 'not_instance_admin: creating a company is an instance-level act'
116
+ using errcode = '42501';
117
+ end if;
118
+
119
+ select * into v_defaults from country_defaults where country = p_country;
120
+
121
+ -- The currency and the language have to be settled before the insert: both
122
+ -- columns are not null with a default, so there is no later moment at which
123
+ -- they are empty and the pack could fill them. The pack answers, or the
124
+ -- caller does, and there is no third answer written here.
125
+ v_currency := upper(coalesce(p_currency_code, v_defaults.currency_code));
126
+ if v_currency is null then
127
+ raise exception 'no_currency: the % pack names no currency; name one', p_country;
128
+ end if;
129
+ v_language := lower(coalesce(p_language, v_defaults.language_default));
130
+ if v_language is null then
131
+ raise exception 'no_language: the % pack names no language for its labels; name one', p_country;
132
+ end if;
133
+
134
+ select * into v_bounds from fiscal_year_bounds(p_country, v_year, p_fiscal_year_start);
135
+
136
+ insert into companies (name, country, fiscal_country, currency_code, language)
137
+ values (p_name, p_country, p_country, v_currency, v_language)
138
+ returning * into v_company;
139
+
140
+ if v_owner is not null then
141
+ insert into company_members (company_id, user_id, role)
142
+ values (v_company.id, v_owner, 'owner')
143
+ on conflict (company_id, user_id) do nothing;
144
+ end if;
145
+
146
+ perform install_country_template(v_company.id, p_country, v_language, p_chart_code);
147
+
148
+ insert into fiscal_years (company_id, name, start_date, end_date)
149
+ values (v_company.id, 'FY' || v_year::text, v_bounds.start_date, v_bounds.end_date);
150
+
151
+ select * into v_company from companies where id = v_company.id;
152
+ return v_company;
153
+ end;
154
+ $$;
155
+
156
+ comment on function create_company(text, char, char, char, text, integer, date, uuid) is
157
+ 'Creates a company, makes the caller its first member, copies the country pack into it and opens its first financial year on the month that pack declares. An instance-level act, like the policy on companies.';
158
+
159
+ revoke execute on all functions in schema public from public;
@@ -0,0 +1,219 @@
1
+ -- Ekwo OS — taking over books that already have numbers.
2
+ --
3
+ -- ST13 gave `country_defaults.numbering_gapless` a reader: `post_entry()`
4
+ -- refuses a number chosen by hand where the law forbids a hole in the
5
+ -- sequence, because a number that skips the counter is exactly how a hole
6
+ -- appears. That entry also wrote down what it would cost — "the day an import
7
+ -- of somebody's old entries under their old numbers is written, this is the
8
+ -- rule it will have to argue with" — and the argument arrived immediately: a
9
+ -- company moving from another system arrives with three years of entries that
10
+ -- already carry numbers, and those numbers are the ones its VAT returns, its
11
+ -- filings and its auditor already know.
12
+ --
13
+ -- So the rule stands and gains an exception that has a name.
14
+ --
15
+ -- **`entries.import` is a capability, and it is in no preset.** Not a flag on
16
+ -- the call, not a session setting, not an argument somebody can pass by
17
+ -- accident: the one thing that lets an explicit number through is a
18
+ -- capability an owner has to grant, on purpose, to the person doing the
19
+ -- import — and take back afterwards. It is in `capabilities` so it can be
20
+ -- granted and revoked like every other, and out of `role_capabilities` so
21
+ -- that no role carries it by default. Importing is not something an
22
+ -- accountant does on a Tuesday.
23
+ --
24
+ -- **What it does not relax.** A duplicate is still refused, by the unique
25
+ -- index `entries_company_number_idx` that has been there since the first
26
+ -- release — `(company_id, number)`, which is stricter than per journal and
27
+ -- was already the right key. And an explicit number on a normal entry, from
28
+ -- somebody without the capability, is refused exactly as it was yesterday.
29
+ --
30
+ -- **It applies to everybody, including the installer.** A gapless sequence is
31
+ -- a rule about the books and not a permission, so this guard does not carry
32
+ -- the `auth.uid() is null` exemption the permission checks do: a superuser
33
+ -- connection is refused like anybody else. An importer running over such a
34
+ -- connection sets the request claim for the user it is acting for — which is
35
+ -- what `ekwo demo` already does to satisfy `claim_instance_admin()` — and
36
+ -- that user holds `entries.import`.
37
+ --
38
+ -- **The counter catches up.** An imported number that is written in the
39
+ -- country's own pattern advances `journal_sequences` to it, so the first
40
+ -- entry booked after the import continues the series instead of restarting
41
+ -- at one and colliding. A number that does not parse against the pattern —
42
+ -- a series from another system, in another shape — leaves the counter alone,
43
+ -- because there is nothing in it the counter could learn.
44
+
45
+ insert into capabilities (code, area, description) values
46
+ ('entries.import', 'entries',
47
+ 'Post an entry under a number chosen by hand, where the country forbids a hole in the sequence. For taking over books that already have numbers. In no preset: it is granted for an import and taken back after it.');
48
+
49
+ -- ---------------------------------------------------------------------------
50
+ -- Reading a counter back out of a number
51
+ --
52
+ -- The pattern is the pack's, so the way to read a number is the pattern read
53
+ -- backwards: the literal text of the format is literal, `{CODE}`, `{YYYY}`,
54
+ -- `{YY}` and `{MM}` are what they are, and the `{N…}` token is the number we
55
+ -- are after. Anything that does not match returns null, which is an answer.
56
+ -- ---------------------------------------------------------------------------
57
+
58
+ create or replace function number_counter(p_format text, p_number text)
59
+ returns integer
60
+ language plpgsql
61
+ immutable
62
+ as $$
63
+ declare
64
+ v_pattern text;
65
+ v_found text;
66
+ begin
67
+ if p_format is null or p_number is null then
68
+ return null;
69
+ end if;
70
+
71
+ -- Everything that is not a token becomes literal; the tokens become what
72
+ -- they can match. `regexp_replace` with the `g` flag walks the format once.
73
+ v_pattern := regexp_replace(p_format, '([.^$*+?()\[\]{}|\\])', '\\\1', 'g');
74
+ -- The escaping above also escaped the braces of the tokens, so the tokens
75
+ -- are matched in their escaped shape.
76
+ v_pattern := replace(v_pattern, '\{CODE\}', '.+');
77
+ v_pattern := replace(v_pattern, '\{YYYY\}', '[0-9]{4}');
78
+ v_pattern := replace(v_pattern, '\{YY\}', '[0-9]{2}');
79
+ v_pattern := replace(v_pattern, '\{MM\}', '[0-9]{2}');
80
+ v_pattern := regexp_replace(v_pattern, '\\\{N+\\\}', '([0-9]+)');
81
+
82
+ if v_pattern !~ '\(\[0-9\]\+\)' then
83
+ return null;
84
+ end if;
85
+
86
+ v_found := (regexp_match(p_number, '^' || v_pattern || '$'))[1];
87
+ if v_found is null then
88
+ return null;
89
+ end if;
90
+ return v_found::integer;
91
+ exception
92
+ when others then
93
+ return null;
94
+ end;
95
+ $$;
96
+
97
+ comment on function number_counter(text, text) is
98
+ 'The counter inside a number, read back through the pattern it was written with, or null when the number does not follow that pattern. What lets an import advance the sequence it interrupted.';
99
+
100
+ -- ---------------------------------------------------------------------------
101
+ -- Catching the counter up
102
+ --
103
+ -- Definer for the reason the counters are definer: `journal_sequences` has a
104
+ -- select policy and no other, and nobody writes it by hand. The caller has
105
+ -- already been checked by `post_entry()`.
106
+ -- ---------------------------------------------------------------------------
107
+
108
+ create or replace function catch_up_journal_sequence(p_journal_id uuid, p_date date, p_number text)
109
+ returns void
110
+ language plpgsql
111
+ security definer
112
+ set search_path = public, pg_temp
113
+ as $$
114
+ declare
115
+ v_company uuid;
116
+ v_format text;
117
+ v_period smallint;
118
+ v_counter integer;
119
+ begin
120
+ select j.company_id into v_company from journals j where j.id = p_journal_id;
121
+ if v_company is null then
122
+ return;
123
+ end if;
124
+
125
+ select number_format into v_format from numbering_rules(v_company);
126
+ v_counter := number_counter(v_format, p_number);
127
+ if v_counter is null then
128
+ return;
129
+ end if;
130
+
131
+ v_period := case when v_format ~ '\{(YYYY|YY)\}'
132
+ then extract(year from p_date)::smallint
133
+ else 0::smallint end;
134
+
135
+ insert into journal_sequences (journal_id, year, last_number)
136
+ values (p_journal_id, v_period, v_counter)
137
+ on conflict (journal_id, year)
138
+ do update set last_number = greatest(journal_sequences.last_number, excluded.last_number);
139
+ end;
140
+ $$;
141
+
142
+ comment on function catch_up_journal_sequence(uuid, date, text) is
143
+ 'Advances a journal counter to an imported number, so the next automatic one continues the series rather than colliding with it. Does nothing for a number that does not follow the country''s pattern.';
144
+
145
+ -- ---------------------------------------------------------------------------
146
+ -- post_entry, with the exception that has a name
147
+ --
148
+ -- Unchanged but for the guard and the catching-up: everything else is the
149
+ -- function as published.
150
+ -- ---------------------------------------------------------------------------
151
+
152
+ create or replace function post_entry(p_entry_id uuid)
153
+ returns entries
154
+ language plpgsql
155
+ as $$
156
+ declare
157
+ v_entry entries%rowtype;
158
+ v_lines integer;
159
+ v_gapless boolean;
160
+ begin
161
+ select * into v_entry from entries where id = p_entry_id for update;
162
+ if not found then
163
+ raise exception 'unknown_entry: entry % does not exist', p_entry_id;
164
+ end if;
165
+ if v_entry.state = 'posted' then
166
+ return v_entry;
167
+ end if;
168
+ if v_entry.state = 'cancelled' then
169
+ raise exception 'entry_cancelled: entry % cannot be posted', p_entry_id;
170
+ end if;
171
+
172
+ select count(*) into v_lines from entry_lines where entry_id = p_entry_id;
173
+ if v_lines = 0 then
174
+ raise exception 'entry_empty: entry % has no lines', p_entry_id;
175
+ end if;
176
+
177
+ -- Re-read the totals maintained by the line trigger.
178
+ select * into v_entry from entries where id = p_entry_id;
179
+ if v_entry.total_debit <> v_entry.total_credit then
180
+ raise exception 'entry_unbalanced: entry % has debit % and credit %',
181
+ p_entry_id, v_entry.total_debit, v_entry.total_credit;
182
+ end if;
183
+
184
+ -- Where the law forbids a hole, the number comes from the counter and from
185
+ -- nowhere else — unless the caller holds `entries.import`, which is how a
186
+ -- set of books that already has numbers is taken over. A duplicate is
187
+ -- refused either way, by `entries_company_number_idx`.
188
+ if v_entry.number is not null then
189
+ select numbering_gapless into v_gapless from numbering_rules(v_entry.company_id);
190
+ if v_gapless and not (auth.uid() is not null
191
+ and has_capability(v_entry.company_id, 'entries.import')) then
192
+ 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',
193
+ p_entry_id;
194
+ end if;
195
+ end if;
196
+
197
+ perform assert_period_open(v_entry.company_id, v_entry.entry_date, true);
198
+
199
+ update entries
200
+ set number = coalesce(number, next_entry_number(journal_id, entry_date)),
201
+ fiscal_year_id = coalesce(fiscal_year_id, fiscal_year_at(company_id, entry_date)),
202
+ state = 'posted',
203
+ posted_at = now()
204
+ where id = p_entry_id
205
+ returning * into v_entry;
206
+
207
+ -- An imported number advances the counter it interrupted, so the next
208
+ -- automatic one continues the series. A number drawn from the counter is
209
+ -- already at it and this is a no-op.
210
+ perform catch_up_journal_sequence(v_entry.journal_id, v_entry.entry_date, v_entry.number);
211
+
212
+ return v_entry;
213
+ end;
214
+ $$;
215
+
216
+ comment on function post_entry(uuid) is
217
+ 'Validates, numbers and posts an entry. Raises rather than warning: a swallowed error is a missing entry. Where the country forbids a hole in the sequence it refuses a number chosen by hand, unless the caller holds entries.import — and then the counter catches up to it.';
218
+
219
+ revoke execute on all functions in schema public from public;