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,654 @@
1
+ -- Ekwo OS — a statement is imported once.
2
+ --
3
+ -- `bank_statements` and `bank_transactions` have existed since the bank
4
+ -- migration, `suggest_matches()` and `auto_settle()` work on their lines, and
5
+ -- nothing wrote them but a test. This is the way in: `import_bank_statement()`
6
+ -- takes what a format brick read out of a file — statements and lines, as
7
+ -- jsonb — and writes the two tables.
8
+ --
9
+ -- **A statement never becomes an entry.** The function writes no ledger line,
10
+ -- no payment and no matching. A statement line becomes a payment when
11
+ -- `settle_from_statement()` says what it pays, and only then; an import that
12
+ -- booked would be a second way of booking money, with its own idea of the
13
+ -- counterpart account. It stops at `bank_transactions`, state `pending`.
14
+ --
15
+ -- **Idempotent, and the database is what refuses.** A statement is replayed
16
+ -- all the time: the same file twice, a whole month after its first fortnight,
17
+ -- two people importing the same extract. So every imported line carries an
18
+ -- `import_key`, unique per bank account, and the insert is `on conflict do
19
+ -- nothing` — it is the index that refuses the duplicate, not a lookup that
20
+ -- might race.
21
+ --
22
+ -- The key, and why it is that one:
23
+ --
24
+ -- * **When the bank gave the movement a reference** (`bankReference`, the
25
+ -- `AcctSvcrRef` of ISO 20022) the key is that reference, with the position
26
+ -- inside a split batch, the booking date and the amount. The reference is
27
+ -- the identity; date and amount are there because nothing promises a bank
28
+ -- never reuses a reference across years, and because they cost nothing: a
29
+ -- replayed movement has the same date and the same amount.
30
+ --
31
+ -- * **When it did not**, the movement has no identity of its own, and the
32
+ -- key is everything the statement says about it — dates, amount, currency,
33
+ -- counterparty, communications, end-to-end and mandate identifiers — plus
34
+ -- **which occurrence it is** among the lines of the same file that say
35
+ -- exactly the same. Two identical transfers on the same day are two lines,
36
+ -- numbered 1 and 2; they are both imported, and the same file replayed
37
+ -- numbers them 1 and 2 again and imports neither. A key without the
38
+ -- occurrence would silently drop the second rent of two tenants who pay
39
+ -- the same amount with the same words.
40
+ --
41
+ -- What this cannot tell apart: two *different* files, neither overlapping
42
+ -- the other, that each carry one line identical in every field, from a
43
+ -- bank that references nothing. The second is taken for the first. That
44
+ -- takes two statements on one day, no bank reference and no end-to-end
45
+ -- identifier at once; it is written in docs/decisions.md and not guessed
46
+ -- around here.
47
+ --
48
+ -- * The position of an entry inside its statement (`entryReference`,
49
+ -- `index`) is in neither key: a line is the sixteenth of a fortnight and
50
+ -- the sixteenth of a month only by accident.
51
+ --
52
+ -- **An overlapping statement imports what is new and lists everything.** A
53
+ -- line exists once, in `bank_transactions`; a statement *lists* lines, in
54
+ -- `bank_statement_lines`, whether it brought them or found them. So the month
55
+ -- that arrives after its first fortnight adds the second fortnight, lists
56
+ -- thirty-one days, and still proves its closing balance — which it could not
57
+ -- if a line belonged to one statement only. `balance_end_computed` reads the
58
+ -- list from now on.
59
+
60
+ -- ---------------------------------------------------------------------------
61
+ -- What an import has to remember about a statement
62
+ -- ---------------------------------------------------------------------------
63
+
64
+ alter table bank_statements
65
+ add column if not exists statement_ref text,
66
+ add column if not exists sequence_number numeric(18, 0),
67
+ add column if not exists period_start date,
68
+ add column if not exists source_format text,
69
+ add column if not exists source_file_name text,
70
+ add column if not exists source_checksum text;
71
+
72
+ comment on column bank_statements.statement_ref is
73
+ 'The identifier the bank gave the statement (Stmt/Id of a camt.053). With the account and the closing date it is what makes a replayed statement the same statement. Null on a statement entered by hand.';
74
+ comment on column bank_statements.sequence_number is
75
+ 'The bank''s own numbering of the statements of this account — the legal sequence number where the format has one, else the electronic one. A hole in it is a statement nobody imported; `bank_statement_continuity` shows it.';
76
+ comment on column bank_statements.period_start is
77
+ 'The day the statement opens on: the date of its opening balance, else the start of its period, else its first line. `statement_date` is the day it closes on.';
78
+ comment on column bank_statements.source_format is
79
+ 'What the statement was read from, as the reader named it — camt.053.001.08. Null on a statement entered by hand.';
80
+ comment on column bank_statements.source_file_name is
81
+ 'The name of the file the statement was first imported from, when the caller gave one.';
82
+ comment on column bank_statements.source_checksum is
83
+ 'The checksum of the file the statement was first imported from, as the caller computed it — the core never sees the bytes. Kept so that the file can be recognised later; not what makes an import idempotent, because the same statement arrives in files that differ by a timestamp.';
84
+
85
+ create unique index if not exists bank_statements_account_ref_idx
86
+ on bank_statements (bank_account_id, statement_ref, statement_date)
87
+ where statement_ref is not null;
88
+
89
+ alter table bank_transactions
90
+ add column if not exists import_key text;
91
+
92
+ comment on column bank_transactions.import_key is
93
+ 'What makes an imported line the same line when its statement is replayed or overlapped: `ref:` and a digest when the bank gave the movement a reference, `fp:` and a digest of everything the statement says about it, with its occurrence among identical lines, when it did not. Unique per bank account, and written by import_bank_statement() only. Null on a line entered by hand.';
94
+
95
+ create unique index if not exists bank_transactions_import_key_idx
96
+ on bank_transactions (bank_account_id, import_key)
97
+ where import_key is not null;
98
+
99
+ -- ---------------------------------------------------------------------------
100
+ -- A statement lists lines; a line exists once
101
+ -- ---------------------------------------------------------------------------
102
+
103
+ create table if not exists bank_statement_lines (
104
+ company_id uuid not null references companies(id) on delete cascade,
105
+ statement_id uuid not null,
106
+ transaction_id uuid not null references bank_transactions(id) on delete cascade,
107
+ position integer not null,
108
+ created_at timestamptz not null default now(),
109
+ primary key (statement_id, transaction_id),
110
+ foreign key (statement_id, company_id)
111
+ references bank_statements(id, company_id) on delete cascade
112
+ );
113
+
114
+ comment on table bank_statement_lines is
115
+ 'Which lines a statement lists. A line is stored once, in bank_transactions, under the first statement that brought it; a later statement that overlaps the first lists the same line here instead of duplicating it, and its closing balance is proved over the list.';
116
+ comment on column bank_statement_lines.position is
117
+ 'Where the line stands in this statement, from 1 — which is not where it stood in another.';
118
+
119
+ create index if not exists bank_statement_lines_transaction_idx
120
+ on bank_statement_lines (transaction_id);
121
+ create index if not exists bank_statement_lines_statement_company_idx
122
+ on bank_statement_lines (statement_id, company_id);
123
+ create index if not exists bank_statement_lines_company_idx
124
+ on bank_statement_lines (company_id);
125
+
126
+ alter table bank_statement_lines enable row level security;
127
+
128
+ -- The form every policy has had since `a_policy_asks_once`: the set of
129
+ -- companies is worked out once per statement, not once per row.
130
+ create policy bank_statement_lines_select on bank_statement_lines
131
+ for select using (company_id = any ((select companies_with_capability('bank.read'))::uuid[]));
132
+ create policy bank_statement_lines_write on bank_statement_lines
133
+ for all using (company_id = any ((select companies_with_capability('bank.write'))::uuid[]))
134
+ with check (company_id = any ((select companies_with_capability('bank.write'))::uuid[]));
135
+
136
+ grant select, insert, update, delete on table bank_statement_lines to authenticated, service_role;
137
+
138
+ -- The computed closing balance is over what the statement lists *or* holds: a
139
+ -- line entered by hand under a statement has no row in the list, and counts
140
+ -- as it always has.
141
+
142
+ create or replace function bank_statement_movement(p_statement_id uuid)
143
+ returns numeric
144
+ language sql
145
+ stable
146
+ security invoker
147
+ as $$
148
+ select coalesce(sum(t.amount), 0)
149
+ from bank_transactions t
150
+ where t.statement_id = p_statement_id
151
+ or exists (
152
+ select 1 from bank_statement_lines l
153
+ where l.statement_id = p_statement_id and l.transaction_id = t.id
154
+ );
155
+ $$;
156
+
157
+ comment on function bank_statement_movement(uuid) is
158
+ 'The sum of the lines a statement lists or holds, each counted once. What balance_end_computed adds to balance_start.';
159
+
160
+ revoke execute on function bank_statement_movement(uuid) from public, anon;
161
+ grant execute on function bank_statement_movement(uuid) to authenticated, service_role;
162
+
163
+ create or replace function bank_statements_refresh_balance()
164
+ returns trigger
165
+ language plpgsql
166
+ as $$
167
+ declare
168
+ v_transaction uuid;
169
+ v_new uuid;
170
+ v_old uuid;
171
+ begin
172
+ if tg_op <> 'DELETE' then
173
+ v_transaction := new.id;
174
+ v_new := new.statement_id;
175
+ end if;
176
+ if tg_op <> 'INSERT' then
177
+ v_transaction := old.id;
178
+ v_old := old.statement_id;
179
+ end if;
180
+ update bank_statements s
181
+ set balance_end_computed = s.balance_start + bank_statement_movement(s.id)
182
+ where s.id in (v_new, v_old)
183
+ or s.id in (select l.statement_id from bank_statement_lines l
184
+ where l.transaction_id = v_transaction);
185
+ return null;
186
+ end;
187
+ $$;
188
+
189
+ create or replace function bank_statements_recompute()
190
+ returns trigger
191
+ language plpgsql
192
+ as $$
193
+ begin
194
+ new.balance_end_computed := new.balance_start + bank_statement_movement(new.id);
195
+ return new;
196
+ end;
197
+ $$;
198
+
199
+ create or replace function bank_statement_lines_refresh_balance()
200
+ returns trigger
201
+ language plpgsql
202
+ as $$
203
+ declare
204
+ v_statement uuid;
205
+ begin
206
+ if tg_op = 'DELETE' then v_statement := old.statement_id; else v_statement := new.statement_id; end if;
207
+ update bank_statements s
208
+ set balance_end_computed = s.balance_start + bank_statement_movement(s.id)
209
+ where s.id = v_statement;
210
+ return null;
211
+ end;
212
+ $$;
213
+
214
+ revoke execute on function bank_statement_lines_refresh_balance() from public, anon, authenticated, service_role;
215
+
216
+ create trigger bank_statement_lines_refresh_balance
217
+ after insert or delete on bank_statement_lines
218
+ for each row execute function bank_statement_lines_refresh_balance();
219
+
220
+ -- ---------------------------------------------------------------------------
221
+ -- A missing statement shows
222
+ -- ---------------------------------------------------------------------------
223
+ --
224
+ -- The closing balance of one statement is the opening balance of the next. The
225
+ -- *previous* statement of an account is the latest one that closed on or
226
+ -- before the day this one opens **and opened before it** — the second
227
+ -- condition is what keeps a fortnight from being taken for the predecessor of
228
+ -- the month that contains it. Computed at each read: importing the missing
229
+ -- month afterwards closes the gap without anybody touching a row.
230
+
231
+ create or replace view bank_statement_continuity
232
+ with (security_invoker = true) as
233
+ select s.id as statement_id,
234
+ s.company_id,
235
+ s.bank_account_id,
236
+ s.statement_ref,
237
+ s.sequence_number,
238
+ coalesce(s.period_start, s.statement_date) as period_start,
239
+ s.statement_date,
240
+ s.balance_start,
241
+ p.id as previous_statement_id,
242
+ p.statement_ref as previous_statement_ref,
243
+ p.statement_date as previous_statement_date,
244
+ p.balance_end_declared as previous_balance_end,
245
+ s.balance_start - p.balance_end_declared as balance_gap,
246
+ case when s.sequence_number is not null and p.sequence_number is not null
247
+ then s.sequence_number - p.sequence_number - 1 end as missing_statements,
248
+ (p.id is not null and s.balance_start <> p.balance_end_declared) as is_broken
249
+ from bank_statements s
250
+ left join lateral (
251
+ select q.*
252
+ from bank_statements q
253
+ where q.bank_account_id = s.bank_account_id
254
+ and q.id <> s.id
255
+ and q.statement_date <= coalesce(s.period_start, s.statement_date)
256
+ and coalesce(q.period_start, q.statement_date) < coalesce(s.period_start, s.statement_date)
257
+ order by q.statement_date desc, coalesce(q.period_start, q.statement_date) desc, q.created_at desc
258
+ limit 1
259
+ ) p on true;
260
+
261
+ comment on view bank_statement_continuity is
262
+ 'Each statement beside the one before it on the same account: whether its opening balance is the closing balance of the previous one (`is_broken`, `balance_gap`), and how many statements the bank numbered in between (`missing_statements`). A break is shown and never refused: the statement that is there is still true, and the one that is missing can only be seen from here.';
263
+
264
+ grant select on bank_statement_continuity to authenticated, service_role;
265
+
266
+ -- ---------------------------------------------------------------------------
267
+ -- import_bank_statement
268
+ -- ---------------------------------------------------------------------------
269
+ --
270
+ -- What it reads, which is what `@ekwo-ai/camt053` returns and what any other
271
+ -- reader can return — the contract is this list, not that package:
272
+ --
273
+ -- p_file.version | .namespace what was read, kept in source_format
274
+ -- p_file.statements[] one or more
275
+ -- .id the bank's identifier of the statement
276
+ -- .legalSequenceNumber | .electronicSequenceNumber
277
+ -- .account.identifier { kind, value } `iban`, or anything else
278
+ -- .account.currency
279
+ -- .period { from, to }
280
+ -- .openingBalance | .closingBalance { amount, currency, date }
281
+ -- .lines[]
282
+ -- .index .detail .booked .bookingDate .valueDate .amount .currency
283
+ -- .bankReference .endToEndId .transactionId .mandateId
284
+ -- .counterparty { name, account { kind, value } }
285
+ -- .remittance { unstructured[], structured[] { reference } }
286
+ -- .additionalInformation
287
+ --
288
+ -- Amounts are decimal strings, signed from the account holder's side, and are
289
+ -- cast here and nowhere before: no figure of a statement is ever a float.
290
+ --
291
+ -- p_source is what the caller knows about the file and the core cannot:
292
+ -- { file_name, checksum, byte_size, mime_type, storage_path }. With a
293
+ -- `storage_path` the file is recorded in `attachments`, on the statement; the
294
+ -- bytes themselves are the caller's to store.
295
+
296
+ create or replace function import_bank_statement(
297
+ p_company_id uuid,
298
+ p_file jsonb,
299
+ p_source jsonb default null,
300
+ p_bank_account_id uuid default null
301
+ )
302
+ returns table (
303
+ statement_index integer,
304
+ statement_id uuid,
305
+ bank_account_id uuid,
306
+ statement_ref text,
307
+ already_imported boolean,
308
+ lines_read integer,
309
+ lines_imported integer,
310
+ lines_known integer,
311
+ lines_not_booked integer,
312
+ warnings jsonb
313
+ )
314
+ language plpgsql
315
+ security invoker
316
+ as $$
317
+ -- The columns this function returns share their names with columns it writes;
318
+ -- inside a statement a bare name is the column.
319
+ #variable_conflict use_column
320
+ declare
321
+ v_statements jsonb := p_file -> 'statements';
322
+ v_format text := coalesce(p_file ->> 'namespace', p_file ->> 'version');
323
+ v_stmt jsonb;
324
+ v_index integer := 0;
325
+ v_identifier jsonb;
326
+ v_value text;
327
+ v_account bank_accounts;
328
+ v_currency text;
329
+ v_opening numeric;
330
+ v_closing numeric;
331
+ v_movement numeric;
332
+ v_bad jsonb;
333
+ v_ref text;
334
+ v_start date;
335
+ v_end date;
336
+ v_existing bank_statements;
337
+ v_id uuid;
338
+ v_already boolean;
339
+ v_read integer;
340
+ v_imported integer;
341
+ v_listed integer;
342
+ v_skipped integer;
343
+ v_warnings jsonb;
344
+ v_chain record;
345
+ begin
346
+ if not exists (select 1 from companies c where c.id = p_company_id) then
347
+ raise exception 'unknown_company: %', p_company_id using errcode = 'no_data_found';
348
+ end if;
349
+ -- Row level security is what refuses; this is what says why. Without it a
350
+ -- viewer reads "new row violates row-level security policy", which names a
351
+ -- mechanism and not a reason.
352
+ -- Null-safe on purpose: `not NULL` is NULL and `if NULL` does not raise, which
353
+ -- is how a guard of this shape was once skipped for a caller with no session.
354
+ if not coalesce(is_installer(), false)
355
+ and not coalesce(has_capability(p_company_id, 'bank.write'), false) then
356
+ raise exception 'not_allowed: importing a statement into this company needs bank.write'
357
+ using errcode = '42501';
358
+ end if;
359
+ if v_statements is null or jsonb_typeof(v_statements) <> 'array'
360
+ or jsonb_array_length(v_statements) = 0 then
361
+ raise exception 'invalid_statement_file: no statement in what was given — expected { statements: [ … ] } as a format reader returns it';
362
+ end if;
363
+ if p_bank_account_id is not null and jsonb_array_length(v_statements) <> 1 then
364
+ raise exception 'invalid_statement_file: a bank account can be named for a file of one statement, and this one holds %',
365
+ jsonb_array_length(v_statements);
366
+ end if;
367
+
368
+ for v_stmt in select value from jsonb_array_elements(v_statements) loop
369
+ v_index := v_index + 1;
370
+ v_identifier := v_stmt #> '{account,identifier}';
371
+ v_value := upper(regexp_replace(coalesce(v_identifier ->> 'value', ''), '\s', '', 'g'));
372
+ v_ref := nullif(v_stmt ->> 'id', '');
373
+
374
+ if v_value = '' or v_ref is null then
375
+ raise exception 'invalid_statement_file: statement % has no identifier or names no account', v_index;
376
+ end if;
377
+
378
+ -- 1. The account. Known, or refused by name: an account created by an
379
+ -- import is an account nobody decided, mapped to no journal and no
380
+ -- ledger account.
381
+ if p_bank_account_id is not null then
382
+ select * into v_account from bank_accounts b
383
+ where b.id = p_bank_account_id and b.company_id = p_company_id;
384
+ if not found then
385
+ raise exception 'unknown_bank_account: % is not a bank account of this company', p_bank_account_id
386
+ using errcode = 'no_data_found';
387
+ end if;
388
+ if v_account.iban is not null and v_identifier ->> 'kind' = 'iban'
389
+ and upper(regexp_replace(v_account.iban, '\s', '', 'g')) <> v_value then
390
+ raise exception 'bank_account_mismatch: the statement is of account % and the bank account named holds %',
391
+ v_value, v_account.iban;
392
+ end if;
393
+ else
394
+ -- `bank_accounts.iban` is the only column the core has for what
395
+ -- identifies an account, and it is compared with whatever the statement
396
+ -- wrote — IBAN or not. The column is misnamed, which is a known gap with
397
+ -- a change of its own; a second, stricter lookup here would not fix it.
398
+ select * into v_account from bank_accounts b
399
+ where b.company_id = p_company_id
400
+ and b.iban is not null
401
+ and upper(regexp_replace(b.iban, '\s', '', 'g')) = v_value;
402
+ if not found then
403
+ raise exception 'unknown_bank_account: no bank account of this company is identified by % (%) — create it, or name the one this statement belongs to',
404
+ v_value, coalesce(v_identifier ->> 'kind', 'unknown kind')
405
+ using errcode = 'no_data_found';
406
+ end if;
407
+ end if;
408
+
409
+ -- 2. The currency. A statement in another currency than its account is
410
+ -- not converted; it is a statement of another account.
411
+ v_currency := coalesce(v_stmt #>> '{account,currency}', v_stmt #>> '{openingBalance,currency}',
412
+ v_stmt #>> '{closingBalance,currency}');
413
+ if v_currency is distinct from v_account.currency_code::text then
414
+ raise exception 'statement_currency_mismatch: statement % is in % and the bank account % in %',
415
+ v_ref, coalesce(v_currency, 'no currency'), v_account.name, v_account.currency_code;
416
+ end if;
417
+
418
+ -- 3. The lines that count: booked, readable, in the account's currency
419
+ -- and no finer than the column that will hold them — which is two
420
+ -- decimals whatever the currency, a limit of `bank_transactions.amount`
421
+ -- written in docs/international.md. Nothing is rounded here: a figure
422
+ -- the column cannot hold is refused, not adjusted.
423
+ select jsonb_agg(jsonb_build_object('line', l.value -> 'index', 'why',
424
+ case
425
+ when l.value ->> 'amount' is null then 'no readable amount'
426
+ when coalesce(l.value ->> 'bookingDate', l.value ->> 'valueDate') is null then 'no date'
427
+ when l.value ->> 'currency' is distinct from v_currency then
428
+ format('in %s, not converted', coalesce(l.value ->> 'currency', 'no currency'))
429
+ else 'more than two decimals'
430
+ end))
431
+ into v_bad
432
+ from jsonb_array_elements(coalesce(v_stmt -> 'lines', '[]'::jsonb)) l
433
+ where coalesce((l.value ->> 'booked')::boolean, false)
434
+ and (l.value ->> 'amount' is null
435
+ or coalesce(l.value ->> 'bookingDate', l.value ->> 'valueDate') is null
436
+ or l.value ->> 'currency' is distinct from v_currency
437
+ or scale(trim_scale((l.value ->> 'amount')::numeric)) > 2);
438
+ if v_bad is not null then
439
+ raise exception 'unreadable_statement_line: statement % holds booked lines that cannot be imported as they are: %',
440
+ v_ref, v_bad;
441
+ end if;
442
+
443
+ -- 4. The balance, recomputed here: what a client says it checked is not a
444
+ -- check. Opening plus what is booked is the closing, or nothing is
445
+ -- written — a statement that does not add up is a file that lost a
446
+ -- line on the way, and importing it would hide which.
447
+ if v_stmt #>> '{openingBalance,amount}' is null or v_stmt #>> '{closingBalance,amount}' is null then
448
+ raise exception 'statement_without_balances: statement % carries no opening or no closing balance, so nothing proves its lines are all there', v_ref;
449
+ end if;
450
+ v_opening := (v_stmt #>> '{openingBalance,amount}')::numeric;
451
+ v_closing := (v_stmt #>> '{closingBalance,amount}')::numeric;
452
+ select coalesce(sum((l.value ->> 'amount')::numeric), 0), count(*)
453
+ into v_movement, v_read
454
+ from jsonb_array_elements(coalesce(v_stmt -> 'lines', '[]'::jsonb)) l
455
+ where coalesce((l.value ->> 'booked')::boolean, false);
456
+ select count(*) into v_skipped
457
+ from jsonb_array_elements(coalesce(v_stmt -> 'lines', '[]'::jsonb)) l
458
+ where not coalesce((l.value ->> 'booked')::boolean, false);
459
+ if v_opening + v_movement <> v_closing then
460
+ raise exception 'unbalanced_statement: statement % opens at %, its booked lines add up to %, and it closes at % — a difference of %',
461
+ v_ref, v_opening, v_movement, v_closing, v_closing - (v_opening + v_movement);
462
+ end if;
463
+ if scale(trim_scale(v_opening)) > 2 or scale(trim_scale(v_closing)) > 2 then
464
+ raise exception 'unreadable_statement_line: the balances of statement % carry more than two decimals', v_ref;
465
+ end if;
466
+
467
+ -- 5. The statement: the same one, or a new one.
468
+ select min(coalesce(l.value ->> 'bookingDate', l.value ->> 'valueDate')::date),
469
+ max(coalesce(l.value ->> 'bookingDate', l.value ->> 'valueDate')::date)
470
+ into v_start, v_end
471
+ from jsonb_array_elements(coalesce(v_stmt -> 'lines', '[]'::jsonb)) l
472
+ where coalesce((l.value ->> 'booked')::boolean, false);
473
+ v_start := coalesce((v_stmt #>> '{openingBalance,date}')::date, (v_stmt #>> '{period,from}')::date, v_start);
474
+ v_end := coalesce((v_stmt #>> '{closingBalance,date}')::date, (v_stmt #>> '{period,to}')::date, v_end);
475
+ if v_end is null then
476
+ raise exception 'invalid_statement_file: statement % has no closing date, no period and no line to take one from', v_ref;
477
+ end if;
478
+
479
+ select * into v_existing from bank_statements s
480
+ where s.bank_account_id = v_account.id and s.statement_ref = v_ref and s.statement_date = v_end;
481
+ v_already := found;
482
+ if v_already then
483
+ if v_existing.balance_start <> v_opening or v_existing.balance_end_declared <> v_closing then
484
+ raise exception 'statement_conflict: statement % of % was imported with balances % → %, and this file says % → %',
485
+ v_ref, v_end, v_existing.balance_start, v_existing.balance_end_declared, v_opening, v_closing;
486
+ end if;
487
+ v_id := v_existing.id;
488
+ else
489
+ insert into bank_statements
490
+ (company_id, bank_account_id, name, statement_date, period_start, balance_start,
491
+ balance_end_declared, source, statement_ref, sequence_number, source_format,
492
+ source_file_name, source_checksum)
493
+ values
494
+ (p_company_id, v_account.id, v_ref, v_end, coalesce(v_start, v_end), v_opening,
495
+ v_closing, 'import', v_ref,
496
+ coalesce(v_stmt ->> 'legalSequenceNumber', v_stmt ->> 'electronicSequenceNumber')::numeric,
497
+ v_format, p_source ->> 'file_name', p_source ->> 'checksum')
498
+ returning id into v_id;
499
+ end if;
500
+
501
+ -- 6. The lines. Keyed, inserted where the key is new, listed either way.
502
+ with read as (
503
+ select l.value as line,
504
+ (l.value ->> 'index')::integer as position,
505
+ coalesce(l.value ->> 'bookingDate', l.value ->> 'valueDate')::date as booked_on,
506
+ (l.value ->> 'amount')::numeric as amount,
507
+ nullif(l.value ->> 'bankReference', '') as bank_reference,
508
+ (select string_agg(u.value, ' ' order by u.ordinality)
509
+ from jsonb_array_elements_text(coalesce(l.value #> '{remittance,unstructured}', '[]'::jsonb))
510
+ with ordinality u) as free_text,
511
+ (select string_agg(s.value ->> 'reference', ' ' order by s.ordinality)
512
+ from jsonb_array_elements(coalesce(l.value #> '{remittance,structured}', '[]'::jsonb))
513
+ with ordinality s) as structured
514
+ from jsonb_array_elements(coalesce(v_stmt -> 'lines', '[]'::jsonb)) l
515
+ where coalesce((l.value ->> 'booked')::boolean, false)
516
+ ),
517
+ described as (
518
+ select r.*,
519
+ case when r.bank_reference is not null
520
+ then concat_ws(chr(31), 'ref', r.bank_reference, coalesce(r.line ->> 'detail', '0'),
521
+ r.booked_on::text, trim_scale(r.amount)::text)
522
+ else concat_ws(chr(31), 'fp', r.booked_on::text, coalesce(r.line ->> 'valueDate', ''),
523
+ trim_scale(r.amount)::text, r.line ->> 'currency',
524
+ coalesce(upper(regexp_replace(r.line #>> '{counterparty,account,value}', '\s', '', 'g')), ''),
525
+ coalesce(r.line #>> '{counterparty,name}', ''),
526
+ coalesce(r.structured, ''), coalesce(r.free_text, ''),
527
+ coalesce(r.line ->> 'endToEndId', ''), coalesce(r.line ->> 'transactionId', ''),
528
+ coalesce(r.line ->> 'mandateId', ''),
529
+ coalesce(r.line ->> 'additionalInformation', ''))
530
+ end as said
531
+ from read r
532
+ ),
533
+ keyed as (
534
+ select d.*,
535
+ (case when d.bank_reference is not null then 'ref:' else 'fp:' end)
536
+ || encode(sha256(convert_to(
537
+ d.said || chr(31)
538
+ || (row_number() over (partition by d.said order by d.position))::text, 'UTF8')), 'hex')
539
+ as import_key
540
+ from described d
541
+ ),
542
+ inserted as (
543
+ insert into bank_transactions
544
+ (company_id, statement_id, bank_account_id, sequence, transaction_date, value_date,
545
+ amount, currency_code, description, counterpart_name, counterpart_iban, reference,
546
+ structured_reference, state, raw, import_key)
547
+ select p_company_id, v_id, v_account.id, k.position, k.booked_on,
548
+ (k.line ->> 'valueDate')::date, k.amount, v_account.currency_code,
549
+ coalesce(k.free_text, k.line ->> 'additionalInformation'),
550
+ k.line #>> '{counterparty,name}',
551
+ -- What the statement wrote, IBAN or not: it is what recognising a
552
+ -- counterparty compares, and `contact_patterns` already calls it
553
+ -- an account and not an IBAN.
554
+ upper(regexp_replace(k.line #>> '{counterparty,account,value}', '\s', '', 'g')),
555
+ k.bank_reference,
556
+ k.line #>> '{remittance,structured,0,reference}',
557
+ 'pending', k.line, k.import_key
558
+ from keyed k
559
+ on conflict (bank_account_id, import_key) where import_key is not null do nothing
560
+ returning id, import_key
561
+ ),
562
+ -- Listed either way: the lines this statement just brought, which the
563
+ -- table as this statement sees it does not hold yet, and the ones it found.
564
+ listed as (
565
+ insert into bank_statement_lines (company_id, statement_id, transaction_id, position)
566
+ select p_company_id, v_id, t.id, k.position
567
+ from keyed k
568
+ join (select i.id, i.import_key from inserted i
569
+ union all
570
+ select b.id, b.import_key from bank_transactions b
571
+ where b.bank_account_id = v_account.id and b.import_key is not null) t
572
+ on t.import_key = k.import_key
573
+ on conflict (statement_id, transaction_id) do nothing
574
+ returning 1
575
+ )
576
+ select (select count(*) from inserted), (select count(*) from listed) into v_imported, v_listed;
577
+
578
+ -- 7. The file, when the caller stored it somewhere.
579
+ if p_source ->> 'storage_path' is not null and not exists (
580
+ select 1 from attachments a
581
+ where a.entity_type = 'bank_statement' and a.entity_id = v_id
582
+ and a.checksum is not distinct from p_source ->> 'checksum'
583
+ and a.storage_path = p_source ->> 'storage_path'
584
+ ) then
585
+ insert into attachments
586
+ (company_id, entity_type, entity_id, file_name, mime_type, byte_size, storage_path,
587
+ checksum, uploaded_by)
588
+ values
589
+ (p_company_id, 'bank_statement', v_id,
590
+ coalesce(p_source ->> 'file_name', v_ref), p_source ->> 'mime_type',
591
+ (p_source ->> 'byte_size')::bigint, p_source ->> 'storage_path',
592
+ p_source ->> 'checksum', auth.uid());
593
+ end if;
594
+
595
+ -- 8. What is signalled and never refused: a missing statement.
596
+ v_warnings := '[]'::jsonb;
597
+ select * into v_chain from bank_statement_continuity c where c.statement_id = v_id;
598
+ if v_chain.is_broken then
599
+ v_warnings := v_warnings || jsonb_build_object(
600
+ 'code', 'balance_chain_broken',
601
+ 'message', format('statement %s opens at %s and the previous one, %s of %s, closed at %s: %s is unaccounted for between them',
602
+ v_ref, v_opening, coalesce(v_chain.previous_statement_ref, 'unnamed'),
603
+ v_chain.previous_statement_date, v_chain.previous_balance_end, v_chain.balance_gap),
604
+ 'previous_statement_id', v_chain.previous_statement_id,
605
+ 'balance_gap', v_chain.balance_gap);
606
+ end if;
607
+ if v_chain.missing_statements is distinct from 0 and v_chain.missing_statements is not null then
608
+ v_warnings := v_warnings || jsonb_build_object(
609
+ 'code', 'statement_number_gap',
610
+ 'message', format('statement %s is numbered %s and the previous one %s',
611
+ v_ref, v_chain.sequence_number, v_chain.sequence_number - v_chain.missing_statements - 1),
612
+ 'missing_statements', v_chain.missing_statements);
613
+ end if;
614
+
615
+ statement_index := v_index;
616
+ statement_id := v_id;
617
+ bank_account_id := v_account.id;
618
+ statement_ref := v_ref;
619
+ already_imported := v_already;
620
+ lines_read := v_read;
621
+ lines_imported := v_imported;
622
+ lines_known := v_read - v_imported;
623
+ lines_not_booked := v_skipped;
624
+ warnings := v_warnings;
625
+ return next;
626
+ end loop;
627
+ end;
628
+ $$;
629
+
630
+ comment on function import_bank_statement(uuid, jsonb, jsonb, uuid) is
631
+ 'Writes what a format reader read out of a bank file into bank_statements and bank_transactions, and nothing else: no entry, no payment, no matching. Idempotent on import_key — a replayed file imports nothing, an overlapping statement imports what is new and lists the rest. Refuses, by name and before writing anything: an account the company does not have (unknown_bank_account), a statement that does not add up (unbalanced_statement) or has no balances, a booked line it cannot hold as it is (unreadable_statement_line), a currency that is not the account''s, and the same statement with other balances (statement_conflict). Signals and does not refuse: an opening balance that is not the previous closing one, a hole in the bank''s numbering. One row per statement of the file; the whole file is imported or none of it.';
632
+
633
+ revoke execute on function import_bank_statement(uuid, jsonb, jsonb, uuid) from public, anon;
634
+ grant execute on function import_bank_statement(uuid, jsonb, jsonb, uuid) to authenticated, service_role;
635
+
636
+ -- ---------------------------------------------------------------------------
637
+ -- The column that said "no reader yet"
638
+ -- ---------------------------------------------------------------------------
639
+
640
+ comment on column country_defaults.bank_statement_formats is
641
+ 'Statement formats a bank of this country sends — coda, camt.053, cfonb120 — from the pack. **Read by clients, not by the socle**: import_bank_statement() takes what a format reader returned and does not ask which format it came from, so this list is what a client offers, and a name in it is a promise only where a reader exists. camt.053 has one; the others are owed, by name, in tests/bank_statement_formats.test.ts.';
642
+
643
+ -- ---------------------------------------------------------------------------
644
+ -- The list leaves with the company
645
+ -- ---------------------------------------------------------------------------
646
+ --
647
+ -- A company that leaves an installation takes its statements and their lines
648
+ -- with it. Which lines a statement lists is part of the statement: without it
649
+ -- an overlapping statement would arrive with a closing balance nothing proves.
650
+ -- Loaded after both of the tables it points at.
651
+
652
+ insert into company_archive_registry (table_name, disposition, load_order, via_column, via_table, reason) values
653
+ ('bank_statement_lines', 'exported', 59, null, null, 'Which lines a statement lists, so a statement that overlaps another still proves its closing balance where it arrives.')
654
+ on conflict (table_schema, table_name) do nothing;