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,561 @@
1
+ -- Ekwo OS — who the money came from, and a memory of how we knew.
2
+ --
3
+ -- A statement line carries a name the bank wrote, an IBAN, a free-text
4
+ -- description and an amount. None of them is a contact. Somebody looks at the
5
+ -- line, decides it is Acme, and moves on — and the next month the same line
6
+ -- arrives and somebody decides the same thing again. That repetition is the
7
+ -- work this table removes.
8
+ --
9
+ -- What is stored is a **motif**: a fact about how this counterparty appears on
10
+ -- a statement of this company. Four kinds, and no fifth without a migration:
11
+ --
12
+ -- counterparty_account the account the money moves from or to
13
+ -- name_variation a form of the name, as the bank writes it
14
+ -- description_keyword a word that appears in the line, with exclusions
15
+ -- amount_range a band of amounts, for a recurring charge
16
+ --
17
+ -- The first one is **not** called `iban`, deliberately. An IBAN is ISO 13616
18
+ -- and about half the world does not use one: the United States identifies an
19
+ -- account by an ABA routing number and an account number, Canada by a transit
20
+ -- and an institution, Australia by a BSB. The rest of this schema has not
21
+ -- learnt that yet — `bank_accounts.iban`, `contacts.iban`,
22
+ -- `bank_transactions.counterpart_iban` and `documents.payee_iban` all name the
23
+ -- European answer, and the last of them cites BT-84 of EN 16931, which is
24
+ -- called *Payment account identifier* and is not an IBAN. That is written up
25
+ -- in `docs/international.md` and is somebody's next migration. This table does
26
+ -- not add to it: a motif holds the identifier the statement carried, whatever
27
+ -- scheme it was written in, and the day the columns above learn to say their
28
+ -- scheme, nothing here has to be renamed.
29
+ --
30
+ -- The vocabulary is closed; only the values are learned. Nothing here is
31
+ -- executed, nothing is a regular expression, and nothing chooses a contact on
32
+ -- its own: `suggest_contacts()` answers with candidates and the reason for
33
+ -- each, and a person — or an agent acting as one — decides.
34
+ --
35
+ -- The design is not new: it has been running in production in a commercial
36
+ -- accounting system since 2025, which is why it carries two incidents of that
37
+ -- production as tests rather than as comments. Both are in
38
+ -- `tests/contact_matching.test.ts`.
39
+
40
+ create type contact_pattern_kind as enum (
41
+ 'counterparty_account', 'name_variation', 'description_keyword', 'amount_range'
42
+ );
43
+
44
+ comment on type contact_pattern_kind is
45
+ 'What a motif remembers. Four values, closed: the account the money moved from or to, a spelling of a name, a word of the description, or a band of amounts. A fifth needs a migration, which is the point — a pack or a user cannot invent a rule here.';
46
+
47
+ create type contact_pattern_source as enum ('declared', 'learned');
48
+
49
+ comment on type contact_pattern_source is
50
+ 'Where the motif came from: `declared` if somebody wrote it down, `learned` if a confirmation taught it. The two are kept apart because a declared motif is an instruction and a learned one is a statistic.';
51
+
52
+ -- ---------------------------------------------------------------------------
53
+ -- contact_patterns
54
+ -- ---------------------------------------------------------------------------
55
+
56
+ create table contact_patterns (
57
+ id uuid primary key default gen_random_uuid(),
58
+ company_id uuid not null references companies(id) on delete cascade,
59
+ contact_id uuid not null references contacts(id) on delete cascade,
60
+ kind contact_pattern_kind not null,
61
+ -- The value, for the three motifs that are a piece of text. Null for
62
+ -- `amount_range`, which is the three numeric columns below.
63
+ value text,
64
+ -- Words that take the motif back. `description_keyword` only: "insurance"
65
+ -- identifies the broker except when the line also says "car", which is the
66
+ -- other broker. A list of words, never an expression.
67
+ exclude_words text[],
68
+ amount_min numeric(16, 2),
69
+ amount_max numeric(16, 2),
70
+ amount_typical numeric(16, 2),
71
+ currency_code char(3) references currencies(code),
72
+ -- What the motif has been worth so far. `confidence` is derived from the two
73
+ -- counters by `contact_pattern_confidence()`; it is stored so a query can
74
+ -- order by it without recomputing, and a trigger keeps it honest.
75
+ usage_count integer not null default 0 check (usage_count >= 0),
76
+ success_count integer not null default 0 check (success_count >= 0),
77
+ confidence numeric(4, 3) not null default 0.5
78
+ check (confidence >= 0 and confidence <= 1),
79
+ last_matched_at timestamptz,
80
+ source contact_pattern_source not null,
81
+ active boolean not null default true,
82
+ created_at timestamptz not null default now(),
83
+ updated_at timestamptz not null default now(),
84
+ foreign key (contact_id, company_id) references contacts(id, company_id),
85
+ constraint contact_patterns_success_within_usage
86
+ check (success_count <= usage_count),
87
+ -- Each kind fills its own columns and no others. A motif that is half an
88
+ -- amount range and half a keyword is not a motif, it is a bug that would be
89
+ -- read by whichever branch ran first.
90
+ constraint contact_patterns_shape check (
91
+ case kind
92
+ when 'amount_range' then
93
+ value is null and exclude_words is null
94
+ and amount_min is not null and amount_max is not null
95
+ and currency_code is not null and amount_min <= amount_max
96
+ when 'description_keyword' then
97
+ value is not null
98
+ and amount_min is null and amount_max is null
99
+ and amount_typical is null and currency_code is null
100
+ else
101
+ value is not null and exclude_words is null
102
+ and amount_min is null and amount_max is null
103
+ and amount_typical is null and currency_code is null
104
+ end
105
+ )
106
+ );
107
+
108
+ comment on table contact_patterns is
109
+ 'How a counterparty shows up on this company''s statements: one row per learned or declared motif, of four closed kinds. Read by suggest_contacts(), written by confirm_contact(). The core still chooses no contact for anybody — it answers with candidates and says why.';
110
+
111
+ comment on column contact_patterns.value is
112
+ 'The account identifier as the statement wrote it, the spelling of the name, or the keyword. Null exactly when the kind is amount_range. An account identifier is not assumed to be an IBAN: half the world does not have one, and the scheme it is written in is a question the rest of this schema cannot answer yet.';
113
+ comment on column contact_patterns.exclude_words is
114
+ 'Words that disqualify a line the keyword would otherwise claim. Only a description_keyword carries them.';
115
+ comment on column contact_patterns.amount_typical is
116
+ 'The amount most often seen inside the band. Never a condition — it orders two candidates that are otherwise equal.';
117
+ comment on column contact_patterns.usage_count is
118
+ 'How many times this motif has been in front of a decision.';
119
+ comment on column contact_patterns.success_count is
120
+ 'How many of those decisions confirmed the contact it names.';
121
+ comment on column contact_patterns.confidence is
122
+ 'success + 1 over usage + 2 — the two counters and nothing else. A motif nobody has used yet is worth 0.5 and says so, instead of claiming certainty from one lucky match.';
123
+ comment on column contact_patterns.source is
124
+ 'declared by a person, or learned from a confirmation.';
125
+ comment on column contact_patterns.active is
126
+ 'A motif turned off without being forgotten. An inactive motif is never read; its counters stay, so turning it back on does not restart the learning.';
127
+
128
+ create unique index contact_patterns_unique_value_idx
129
+ on contact_patterns (company_id, contact_id, kind, value)
130
+ where value is not null;
131
+ create unique index contact_patterns_unique_range_idx
132
+ on contact_patterns (company_id, contact_id, kind, amount_min, amount_max, currency_code)
133
+ where value is null;
134
+ create index contact_patterns_company_kind_idx
135
+ on contact_patterns (company_id, kind) where active;
136
+ create index contact_patterns_contact_idx on contact_patterns (contact_id, company_id);
137
+ create index contact_patterns_currency_idx on contact_patterns (currency_code);
138
+
139
+ create trigger contact_patterns_set_updated_at
140
+ before update on contact_patterns
141
+ for each row execute function set_updated_at();
142
+
143
+ alter table contact_patterns enable row level security;
144
+
145
+ create policy contact_patterns_select on contact_patterns
146
+ for select using (has_capability(company_id, 'contacts.read'));
147
+ create policy contact_patterns_write on contact_patterns
148
+ for all using (has_capability(company_id, 'contacts.write'))
149
+ with check (has_capability(company_id, 'contacts.write'));
150
+
151
+ comment on policy contact_patterns_select on contact_patterns is
152
+ 'Whoever may read a contact may read what is known about how it appears on a statement.';
153
+ comment on policy contact_patterns_write on contact_patterns is
154
+ 'contacts.write: a motif is knowledge about a contact, and changing it changes who future money is attributed to.';
155
+
156
+ grant select, insert, update, delete on table contact_patterns
157
+ to authenticated, service_role;
158
+
159
+ create trigger contact_patterns_audit
160
+ after insert or update or delete on contact_patterns
161
+ for each row execute function audit_changes('{"company":"company_id","key":["kind","value"]}');
162
+
163
+ -- ---------------------------------------------------------------------------
164
+ -- The numbers the matching runs on
165
+ --
166
+ -- The system this comes from holds them as literals inside a function: 0.60 to
167
+ -- accept a name, one cent of tolerance on an amount, two on a sum, ninety days
168
+ -- of window. They are good numbers — they come from two years of use — and a
169
+ -- literal is still the wrong place for them, because the moment a company
170
+ -- disagrees the only way to say so is a fork.
171
+ --
172
+ -- They are also not all the same kind of number. A tolerance in *cents* is a
173
+ -- statement about the euro; a quarter of the world's currencies has no cents
174
+ -- and three currencies have three decimals. So a tolerance here is counted in
175
+ -- **units of the currency's own smallest denomination**, resolved through
176
+ -- `rounding_of()`, and never in hundredths of anything.
177
+ -- ---------------------------------------------------------------------------
178
+
179
+ create table matching_settings (
180
+ company_id uuid primary key references companies(id) on delete cascade,
181
+ name_threshold numeric(4, 3) check (name_threshold >= 0 and name_threshold <= 1),
182
+ amount_tolerance_units integer check (amount_tolerance_units >= 0),
183
+ sum_tolerance_units integer check (sum_tolerance_units >= 0),
184
+ date_window_days integer check (date_window_days >= 0),
185
+ minimum_word_length integer check (minimum_word_length >= 1),
186
+ created_at timestamptz not null default now(),
187
+ updated_at timestamptz not null default now()
188
+ );
189
+
190
+ comment on table matching_settings is
191
+ 'What this company considers close enough. Every column is nullable and null means the shipped answer, which lives in matching_policy_of() and nowhere else — so a reader always gets a number and a company that never had an opinion has no row.';
192
+ comment on column matching_settings.name_threshold is
193
+ 'How much of a name has to agree before a contact is proposed at all, between 0 and 1.';
194
+ comment on column matching_settings.amount_tolerance_units is
195
+ 'How many smallest units of the currency two amounts may differ by and still be the same payment. Units, not cents: the currency says how much a unit is worth.';
196
+ comment on column matching_settings.sum_tolerance_units is
197
+ 'The same, for a transaction that pays several documents at once, where each of them was rounded on its own.';
198
+ comment on column matching_settings.date_window_days is
199
+ 'How far from a document a payment may sit and still be proposed for it.';
200
+ comment on column matching_settings.minimum_word_length is
201
+ 'How long a word has to be to count as evidence of a name.';
202
+
203
+ create trigger matching_settings_set_updated_at
204
+ before update on matching_settings
205
+ for each row execute function set_updated_at();
206
+
207
+ alter table matching_settings enable row level security;
208
+
209
+ create policy matching_settings_select on matching_settings
210
+ for select using (is_company_member(company_id));
211
+ create policy matching_settings_write on matching_settings
212
+ for all using (has_capability(company_id, 'company.write'))
213
+ with check (has_capability(company_id, 'company.write'));
214
+
215
+ comment on policy matching_settings_select on matching_settings is
216
+ 'A member reads how their company matches, the way they read the company itself.';
217
+ comment on policy matching_settings_write on matching_settings is
218
+ 'company.write: these numbers decide what gets attributed automatically, which is a decision about the company and not about one contact.';
219
+
220
+ grant select, insert, update, delete on table matching_settings
221
+ to authenticated, service_role;
222
+
223
+ create trigger matching_settings_audit
224
+ after insert or update or delete on matching_settings
225
+ for each row execute function audit_changes('{"company":"company_id"}');
226
+
227
+ create type matching_policy as (
228
+ name_threshold numeric,
229
+ amount_tolerance_units integer,
230
+ sum_tolerance_units integer,
231
+ date_window_days integer,
232
+ minimum_word_length integer
233
+ );
234
+
235
+ comment on type matching_policy is
236
+ 'The five numbers the matching runs on, resolved for one company.';
237
+
238
+ create or replace function matching_policy_of(p_company_id uuid)
239
+ returns matching_policy
240
+ language sql
241
+ stable
242
+ security invoker
243
+ set search_path = public
244
+ as $$
245
+ -- The shipped answers, in the one place they exist. Each comes from rules
246
+ -- that have run in production; none is a fact about a country, a currency or
247
+ -- a language, which is why they can be shipped at all.
248
+ --
249
+ -- 0.600 a name is evidence at three fifths of agreement
250
+ -- 1 one smallest unit of the currency between two amounts
251
+ -- 2 two of them across a sum of documents rounded separately
252
+ -- 90 a payment more than a quarter away from its document is a
253
+ -- question, not a match
254
+ -- 4 a word of three letters or fewer is a legal form or a
255
+ -- preposition, never a party
256
+ select row(
257
+ coalesce(s.name_threshold, 0.600),
258
+ coalesce(s.amount_tolerance_units, 1),
259
+ coalesce(s.sum_tolerance_units, 2),
260
+ coalesce(s.date_window_days, 90),
261
+ coalesce(s.minimum_word_length, 4)
262
+ )::matching_policy
263
+ from (select p_company_id as company_id) c
264
+ left join matching_settings s on s.company_id = c.company_id;
265
+ $$;
266
+
267
+ comment on function matching_policy_of(uuid) is
268
+ 'The five numbers, for this company: its own where it has an opinion, the shipped ones otherwise. The only function that carries a default.';
269
+
270
+ revoke execute on function matching_policy_of(uuid) from public, anon;
271
+ grant execute on function matching_policy_of(uuid) to authenticated, service_role;
272
+
273
+ -- ---------------------------------------------------------------------------
274
+ -- The words of a name
275
+ --
276
+ -- Two names agree when they share a word long enough to mean something. What
277
+ -- is deliberately **not** here is a list of legal forms to ignore — `sarl`,
278
+ -- `bvba`, `gmbh`, `llc`. Such a list is country data, it would be a literal in
279
+ -- the core, and this repository refuses those on principle.
280
+ --
281
+ -- It is not needed either, because a better rule falls out of the data: a word
282
+ -- that reaches several contacts of the company identifies none of them.
283
+ -- Thirty suppliers called "… SARL" make `sarl` worthless without anybody
284
+ -- having to say so, and the same rule catches the word this list would have
285
+ -- forgotten — a town, a trade, the name of a group. `suggest_contacts()`
286
+ -- therefore counts how many contacts each piece of evidence reaches and says
287
+ -- so, and a caller applying a match on its own is expected to refuse anything
288
+ -- that reached more than one.
289
+ --
290
+ -- This was learned the hard way on 16 April 2026: matching on the first five
291
+ -- characters of a name made `SARL` equal to `SASU` and `SPRL`, and unrelated
292
+ -- suppliers were matched to each other. `tests/contact_matching.test.ts` keeps
293
+ -- that incident as a test.
294
+ -- ---------------------------------------------------------------------------
295
+
296
+ create or replace function significant_words(p_text text, p_minimum_length integer)
297
+ returns text[]
298
+ language sql
299
+ immutable
300
+ as $$
301
+ select coalesce(array_agg(distinct w), '{}'::text[])
302
+ from regexp_split_to_table(
303
+ lower(translate(coalesce(p_text, ''), '.,&()/-_''"+*:;#', ' ')),
304
+ '\s+'
305
+ ) as w
306
+ where length(w) >= p_minimum_length;
307
+ $$;
308
+
309
+ comment on function significant_words(text, integer) is
310
+ 'The words of a name that are long enough to be evidence, lowercased and deduplicated. No stop list: a word shared by several contacts is disqualified by the count of what it reaches, which is a fact about this company rather than an opinion about a language.';
311
+
312
+ revoke execute on function significant_words(text, integer) from public, anon;
313
+ grant execute on function significant_words(text, integer) to authenticated, service_role;
314
+
315
+ -- ---------------------------------------------------------------------------
316
+ -- suggest_contacts
317
+ --
318
+ -- Read-only, and it decides nothing. It answers with every contact the line
319
+ -- could be, the score of each, the evidence, and — the column that matters —
320
+ -- how many contacts that same evidence reached. One is a recognition; two is
321
+ -- a coincidence with a name on it.
322
+ -- ---------------------------------------------------------------------------
323
+
324
+ create or replace function suggest_contacts(p_transaction_id uuid)
325
+ returns table (
326
+ contact_id uuid,
327
+ score numeric,
328
+ method text,
329
+ because text,
330
+ alternatives integer
331
+ )
332
+ language plpgsql
333
+ stable
334
+ security invoker
335
+ as $$
336
+ declare
337
+ v_tx bank_transactions;
338
+ v_policy matching_policy;
339
+ v_words text[];
340
+ begin
341
+ -- Row level security answers this select, so a member of another company
342
+ -- gets nothing and is told nothing.
343
+ select * into v_tx from bank_transactions t where t.id = p_transaction_id;
344
+ if not found then
345
+ raise exception 'not_found: bank transaction %', p_transaction_id
346
+ using errcode = 'no_data_found';
347
+ end if;
348
+
349
+ v_policy := matching_policy_of(v_tx.company_id);
350
+ v_words := significant_words(
351
+ coalesce(v_tx.counterpart_name, v_tx.description),
352
+ v_policy.minimum_word_length);
353
+
354
+ return query
355
+ with
356
+ -- 1. The account the money moved from or to. An account identifier is not a
357
+ -- resemblance: either the statement carried the one recorded against the
358
+ -- contact, or it did not. Hence a score of 1, and the only evidence that
359
+ -- needs no threshold.
360
+ by_account as (
361
+ select c.id, 1::numeric as score, 'account'::text as method,
362
+ format('the statement carries the account recorded against %s', c.name) as because
363
+ from contacts c
364
+ where c.company_id = v_tx.company_id
365
+ and v_tx.counterpart_iban is not null
366
+ and c.iban is not null
367
+ and upper(replace(c.iban, ' ', '')) = upper(replace(v_tx.counterpart_iban, ' ', ''))
368
+ union
369
+ select p.contact_id, 1::numeric, 'account',
370
+ format('a motif of %s carries this account', c.name)
371
+ from contact_patterns p
372
+ join contacts c on c.id = p.contact_id
373
+ where p.company_id = v_tx.company_id
374
+ and p.active
375
+ and p.kind = 'counterparty_account'
376
+ and v_tx.counterpart_iban is not null
377
+ and upper(replace(p.value, ' ', '')) = upper(replace(v_tx.counterpart_iban, ' ', ''))
378
+ ),
379
+ -- 2. A motif that was learned or declared. Its score is its confidence: what
380
+ -- it has been worth so far, and nothing else.
381
+ by_pattern as (
382
+ select p.contact_id as id, p.confidence as score,
383
+ ('pattern:' || p.kind::text)::text as method,
384
+ format('%s, a motif of %s confirmed %s of %s times',
385
+ coalesce(p.value, format('%s to %s', p.amount_min, p.amount_max)),
386
+ c.name, p.success_count, p.usage_count) as because
387
+ from contact_patterns p
388
+ join contacts c on c.id = p.contact_id
389
+ where p.company_id = v_tx.company_id
390
+ and p.active
391
+ and case p.kind
392
+ when 'name_variation' then
393
+ v_tx.counterpart_name is not null
394
+ and lower(trim(v_tx.counterpart_name)) = lower(trim(p.value))
395
+ when 'description_keyword' then
396
+ v_tx.description is not null
397
+ and position(lower(p.value) in lower(v_tx.description)) > 0
398
+ and not exists (
399
+ select 1 from unnest(coalesce(p.exclude_words, '{}'::text[])) as x(word)
400
+ where position(lower(x.word) in lower(v_tx.description)) > 0
401
+ )
402
+ when 'amount_range' then
403
+ p.currency_code = v_tx.currency_code
404
+ and abs(v_tx.amount) between p.amount_min and p.amount_max
405
+ else false
406
+ end
407
+ ),
408
+ -- 3. The name, compared word by word. Three shapes, from the one that says
409
+ -- the most to the one that says the least, and the last of them is why
410
+ -- `alternatives` exists.
411
+ by_name as (
412
+ select c.id, s.score, 'name'::text as method, s.because
413
+ from contacts c
414
+ cross join lateral (
415
+ select significant_words(c.name, v_policy.minimum_word_length) as words
416
+ ) w
417
+ cross join lateral (
418
+ select
419
+ case
420
+ when lower(trim(c.name)) = lower(trim(coalesce(v_tx.counterpart_name, ''))) then 0.950
421
+ when cardinality(w.words) > 0 and w.words <@ v_words then 0.900
422
+ when cardinality(w.words) > 0 and v_words <@ w.words then 0.900
423
+ when cardinality(array(select unnest(w.words) intersect select unnest(v_words))) > 0 then
424
+ 0.650 + 0.250 * (
425
+ cardinality(array(select unnest(w.words) intersect select unnest(v_words)))::numeric
426
+ / least(cardinality(w.words), cardinality(v_words))
427
+ )
428
+ else null
429
+ end as score,
430
+ format('the name on the statement shares %s with %s',
431
+ array_to_string(array(select unnest(w.words) intersect select unnest(v_words)), ', '),
432
+ c.name) as because
433
+ ) s
434
+ where c.company_id = v_tx.company_id
435
+ and c.active
436
+ and cardinality(v_words) > 0
437
+ and s.score is not null
438
+ and s.score >= v_policy.name_threshold
439
+ ),
440
+ all_candidates as (
441
+ select * from by_account
442
+ union all select * from by_pattern
443
+ union all select * from by_name
444
+ )
445
+ select a.id, a.score, a.method, a.because,
446
+ count(*) over (partition by a.method)::integer
447
+ from all_candidates a
448
+ order by a.score desc, a.method;
449
+ end;
450
+ $$;
451
+
452
+ comment on function suggest_contacts(uuid) is
453
+ 'Who this statement line could be, with the score, the evidence in a sentence, and how many contacts that same evidence reached. It writes nothing and decides nothing: a caller that applies a match on its own is expected to require alternatives = 1.';
454
+
455
+ revoke execute on function suggest_contacts(uuid) from public, anon;
456
+ grant execute on function suggest_contacts(uuid) to authenticated, service_role;
457
+
458
+ -- ---------------------------------------------------------------------------
459
+ -- confirm_contact
460
+ --
461
+ -- The only place a motif is learned, because the only thing worth learning
462
+ -- from is a decision somebody stands behind.
463
+ --
464
+ -- It also does the unglamorous half: a motif that pointed at somebody else is
465
+ -- charged a use and not a success, so being wrong costs confidence. Without
466
+ -- that, every motif converges on certainty and the oldest mistake wins.
467
+ -- ---------------------------------------------------------------------------
468
+
469
+ create or replace function confirm_contact(p_transaction_id uuid, p_contact_id uuid)
470
+ returns bank_transactions
471
+ language plpgsql
472
+ volatile
473
+ security invoker
474
+ as $$
475
+ declare
476
+ v_tx bank_transactions;
477
+ v_contact contacts;
478
+ v_policy matching_policy;
479
+ begin
480
+ select * into v_tx from bank_transactions t where t.id = p_transaction_id;
481
+ if not found then
482
+ raise exception 'not_found: bank transaction %', p_transaction_id
483
+ using errcode = 'no_data_found';
484
+ end if;
485
+
486
+ select * into v_contact
487
+ from contacts c
488
+ where c.id = p_contact_id and c.company_id = v_tx.company_id;
489
+ if not found then
490
+ raise exception 'contact_not_of_company: % is not a contact of the company this line belongs to',
491
+ p_contact_id;
492
+ end if;
493
+
494
+ v_policy := matching_policy_of(v_tx.company_id);
495
+
496
+ -- Every motif that named somebody else has now been in front of a decision
497
+ -- and lost it. Charged first, so that a motif that also named the right
498
+ -- contact is not punished for it below.
499
+ update contact_patterns p
500
+ set usage_count = p.usage_count + 1,
501
+ confidence = (p.success_count + 1)::numeric / (p.usage_count + 3)
502
+ where p.id in (
503
+ select cp.id
504
+ from contact_patterns cp
505
+ join suggest_contacts(p_transaction_id) s
506
+ on s.contact_id = cp.contact_id
507
+ and s.method = 'pattern:' || cp.kind::text
508
+ where cp.company_id = v_tx.company_id
509
+ and cp.contact_id <> p_contact_id
510
+ );
511
+
512
+ -- The account, when the statement carried one. This is the motif worth the
513
+ -- most, and the one a bank writes identically every time.
514
+ if v_tx.counterpart_iban is not null then
515
+ insert into contact_patterns (company_id, contact_id, kind, value, source,
516
+ usage_count, success_count, confidence, last_matched_at)
517
+ values (v_tx.company_id, p_contact_id, 'counterparty_account',
518
+ upper(replace(v_tx.counterpart_iban, ' ', '')), 'learned',
519
+ 1, 1, 2::numeric / 3, now())
520
+ on conflict (company_id, contact_id, kind, value) where value is not null
521
+ do update set usage_count = contact_patterns.usage_count + 1,
522
+ success_count = contact_patterns.success_count + 1,
523
+ confidence = (contact_patterns.success_count + 2)::numeric
524
+ / (contact_patterns.usage_count + 3),
525
+ last_matched_at = now(),
526
+ active = true;
527
+ end if;
528
+
529
+ -- The name as this bank writes it, which is rarely the name in the ledger.
530
+ -- Stored only when it carries a word worth comparing: a line whose name is
531
+ -- "PAIEMENT" teaches nothing.
532
+ if v_tx.counterpart_name is not null
533
+ and cardinality(significant_words(v_tx.counterpart_name, v_policy.minimum_word_length)) > 0
534
+ then
535
+ insert into contact_patterns (company_id, contact_id, kind, value, source,
536
+ usage_count, success_count, confidence, last_matched_at)
537
+ values (v_tx.company_id, p_contact_id, 'name_variation',
538
+ trim(v_tx.counterpart_name), 'learned', 1, 1, 2::numeric / 3, now())
539
+ on conflict (company_id, contact_id, kind, value) where value is not null
540
+ do update set usage_count = contact_patterns.usage_count + 1,
541
+ success_count = contact_patterns.success_count + 1,
542
+ confidence = (contact_patterns.success_count + 2)::numeric
543
+ / (contact_patterns.usage_count + 3),
544
+ last_matched_at = now(),
545
+ active = true;
546
+ end if;
547
+
548
+ update bank_transactions t
549
+ set contact_id = p_contact_id
550
+ where t.id = p_transaction_id
551
+ returning * into v_tx;
552
+
553
+ return v_tx;
554
+ end;
555
+ $$;
556
+
557
+ comment on function confirm_contact(uuid, uuid) is
558
+ 'Attributes a statement line to a contact and learns from it: the account and the name as this bank writes them become motifs, and every motif that had named somebody else is charged a use without a success. Knowing who the money came from is not knowing what it pays — this function never touches a document and never reconciles anything.';
559
+
560
+ revoke execute on function confirm_contact(uuid, uuid) from public, anon;
561
+ grant execute on function confirm_contact(uuid, uuid) to authenticated, service_role;