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,649 @@
1
+ -- Ekwo OS — a price that already holds its tax.
2
+ --
3
+ -- `taxes.price_include` has been a column since the tax engine landed on
4
+ -- 12 September and has never been read. The note beside it said the
5
+ -- gross-to-net computation "arrives with the country that needs it", and the
6
+ -- United Kingdom is that country: a retail price there is quoted with the VAT
7
+ -- in it as a matter of course, and VAT Notice 700 publishes the fraction that
8
+ -- takes it back out — the rate over one hundred plus the rate, one sixth at
9
+ -- 20 per cent. Australia, Canada and most of what a shop sells to a consumer
10
+ -- anywhere work the same way. Until now a line carrying such a tax was booked
11
+ -- with the tax added *on top* of the price the customer had already paid, so
12
+ -- a till roll of 7 401,32 became an invoice of 8 881,58.
13
+ --
14
+ -- What an invoice has to say is not negotiable. EN 16931 states the line net
15
+ -- amount in BT-131 and the taxable amount of each group in BT-116, both
16
+ -- excluding the tax, and BR-CO-14 requires the tax of a group to be the
17
+ -- rounded figure of that one group and not a sum of line roundings. So the
18
+ -- gross price is an input, never an output: it is converted, once, per tax
19
+ -- group, and what the document stores is the net.
20
+ --
21
+ -- **The rule.** For every tax group of a document whose tax prices with the
22
+ -- tax in it:
23
+ --
24
+ -- gross = sum of the lines, each quantity x unit price less its discount
25
+ -- tax = round(gross - gross / (1 + rate/100)) once, at the currency
26
+ -- base = gross - tax
27
+ --
28
+ -- and `base` is then shared over the lines of the group in proportion to their
29
+ -- gross, the last line of the group taking whatever is left. That is the
30
+ -- technique `post_document` already uses to share a `tax_on_base` over the
31
+ -- accounts it lands on, and it is used here for the same reason: the shares
32
+ -- have to add up to the figure that was rounded once, or the invoice does not
33
+ -- foot.
34
+ --
35
+ -- **Why the base is subtracted rather than computed.** `base = gross x 100 /
36
+ -- (100 + rate)` rounded on its own, with the tax rounded on its own beside it,
37
+ -- gives a pair that misses the gross by a unit often enough to notice: at
38
+ -- 20 per cent, one price in about fifty. Subtracting makes `base + tax =
39
+ -- gross` true by construction, which is the one thing a retail customer can
40
+ -- check by looking at the receipt in their hand. HMRC's VAT Notice 700,
41
+ -- §§ 17.5 and 17.6, admits two rounding methods to a retailer — line by line
42
+ -- or invoice by invoice — and this is the second, which is also the one
43
+ -- BR-CO-14 requires of a European invoice. Nothing here invents a word for the
44
+ -- choice: `rounding_method` is the *arithmetic* of a country, not the unit the
45
+ -- arithmetic is applied to, and a pack that had to declare "per line" would be
46
+ -- declaring an invoice that fails validation. The gap, where a country really
47
+ -- does allow a trader to choose, is written up in `docs/international.md`.
48
+ --
49
+ -- **Why the tax cannot be re-derived from the base.** `document_tax_summary`
50
+ -- computed the tax of every group as `round(base x rate / 100)`. Applied to a
51
+ -- base that came out of the subtraction above, that answer is not always the
52
+ -- tax that produced it: 99,99 at 20 per cent gives a tax of 16,67 and a base of
53
+ -- 83,32, and 83,32 at 20 per cent rounds back to 16,66 — a penny short, and a
54
+ -- total of 99,98 against a price of 99,99. So the view computes the tax of an
55
+ -- inclusive group from the gross it was quoted at, and of every other group
56
+ -- exactly as before. No figure of any existing pack moves by a cent.
57
+ --
58
+ -- **Two columns on the line, and why they are columns.**
59
+ --
60
+ -- * `unit_price_includes_tax` is a snapshot, taken from the tax while the
61
+ -- document is a draft and frozen when it is posted — the rule BT-151 and
62
+ -- BT-152 already follow. A pack upgrade that turns the flag on a tax must
63
+ -- not rewrite an invoice somebody has already sent. It is *derived* and
64
+ -- never keyed, because it is a fact about the tax and not a choice of the
65
+ -- line.
66
+ -- * `amount_incl_tax` keeps the gross the line was quoted at, which is the
67
+ -- only figure a retailer recognises and the only one the conversion can be
68
+ -- redone from. Null on every line whose price excludes the tax, so the
69
+ -- column says which world it is in rather than holding a copy of
70
+ -- `amount_untaxed`.
71
+ --
72
+ -- **Where the arithmetic runs.** `document_lines.amount_untaxed` stays the
73
+ -- truth and stays derived. Its `before` trigger cannot see the other lines of
74
+ -- the group, so it writes what one line on its own comes to — which *is* the
75
+ -- group's answer when the group has one line, the ordinary retail case — and
76
+ -- the `after` trigger that already refreshes the document's totals shares the
77
+ -- group's base over the group first. The share is written only where it
78
+ -- differs, so the write that corrects a line does not start again.
79
+ --
80
+ -- **Three refusals, each by name and each as early as it can be made.**
81
+ --
82
+ -- * A fixed-amount tax cannot price with the tax in it: there is no rate to
83
+ -- divide by, and "the price includes 0,50" is a discount, not a tax. A
84
+ -- check constraint on `taxes` and on `tax_templates` refuses it, and
85
+ -- `ekwo pack check` refuses it before a seed is written.
86
+ -- * A line whose price includes a tax has to have a tax. A check constraint
87
+ -- on `document_lines`.
88
+ -- * A tax group cannot be half inclusive. The only way to build one is to
89
+ -- turn the flag on a tax between two line writes of the same draft;
90
+ -- `mixed_price_include` says so where it happens, rather than letting a
91
+ -- document be posted whose gross and net lines were added together.
92
+
93
+ -- ---------------------------------------------------------------------------
94
+ -- 1. A price that includes a tax needs a rate to take it back out
95
+ -- ---------------------------------------------------------------------------
96
+
97
+ alter table taxes
98
+ add constraint taxes_price_include_needs_a_rate
99
+ check (not price_include or amount_type = 'percent');
100
+
101
+ alter table tax_templates
102
+ add constraint tax_templates_price_include_needs_a_rate
103
+ check (not price_include or amount_type = 'percent');
104
+
105
+ comment on column taxes.price_include is
106
+ 'The unit price of a line carrying this tax is the gross price: the engine takes the tax out of it per group rather than adding it on top. Only a percentage tax may say so.';
107
+
108
+ comment on column tax_templates.price_include is
109
+ 'The unit price of a line carrying this tax is the gross price. Compiled from the pack and copied onto the tax a company installs.';
110
+
111
+ -- ---------------------------------------------------------------------------
112
+ -- 2. What the line keeps
113
+ -- ---------------------------------------------------------------------------
114
+
115
+ alter table document_lines
116
+ add column if not exists unit_price_includes_tax boolean not null default false,
117
+ add column if not exists amount_incl_tax numeric(16, 2);
118
+
119
+ comment on column document_lines.unit_price_includes_tax is
120
+ 'The unit price of this line was quoted with the tax in it. Derived from the tax while the document is a draft and frozen when it is posted, like BT-151 and BT-152, so a later change to the tax cannot rewrite an invoice that has been sent.';
121
+
122
+ comment on column document_lines.amount_incl_tax is
123
+ 'quantity x unit_price less the discount, tax included, as the line was quoted. Null where the price excludes the tax, which is every line of every pack but a retail one.';
124
+
125
+ alter table document_lines
126
+ add constraint document_lines_included_tax_needs_a_tax
127
+ check (not unit_price_includes_tax or tax_id is not null);
128
+
129
+ -- ---------------------------------------------------------------------------
130
+ -- 3. The line derives its own amounts
131
+ --
132
+ -- Same trigger, same name, one more thing to work out. Three cases:
133
+ --
134
+ -- * not a product line — nothing is billed and nothing is quoted;
135
+ -- * a price that excludes the tax — what the trigger has always done;
136
+ -- * a price that includes it — the gross is kept and the net is what this
137
+ -- line alone comes to. `documents_allocate_included_tax()` then replaces
138
+ -- that provisional with the line's share of a base rounded once on the
139
+ -- group. Where the group has one line the two are the same figure, which
140
+ -- is why the provisional is this and not the gross: a line that never
141
+ -- reached the allocator is out by at most one unit of the currency,
142
+ -- instead of out by the whole tax.
143
+ --
144
+ -- The provisional is recomputed only when the gross moved. That is what lets
145
+ -- the allocator write a share back through this trigger without it being
146
+ -- overwritten on the way in.
147
+ -- ---------------------------------------------------------------------------
148
+
149
+ create or replace function document_lines_amount_untaxed()
150
+ returns trigger
151
+ language plpgsql
152
+ as $$
153
+ declare
154
+ v_company uuid;
155
+ v_currency char(3);
156
+ v_state doc_state;
157
+ v_round money_rounding;
158
+ v_gross numeric;
159
+ v_rate numeric;
160
+ begin
161
+ if new.line_type <> 'product' then
162
+ new.amount_untaxed := 0;
163
+ new.amount_incl_tax := null;
164
+ new.unit_price_includes_tax := false;
165
+ return new;
166
+ end if;
167
+
168
+ -- Read in two steps: `select f(x) into a_composite` would assign the whole
169
+ -- row to the first field of the target, which is a trap plpgsql lays for
170
+ -- every function that returns a composite.
171
+ select d.company_id, d.currency_code, d.state
172
+ into v_company, v_currency, v_state
173
+ from documents d where d.id = new.document_id;
174
+ v_round := rounding_of(v_company, v_currency);
175
+
176
+ -- The snapshot. Taken from the tax for as long as the document is a draft,
177
+ -- and left alone from the moment it is posted.
178
+ if tg_op = 'INSERT' or v_state = 'draft' then
179
+ new.unit_price_includes_tax :=
180
+ coalesce((select t.price_include from taxes t where t.id = new.tax_id), false);
181
+ end if;
182
+
183
+ v_gross := round_amount(
184
+ new.quantity * new.unit_price * (1 - new.discount_percent / 100), v_round);
185
+
186
+ if not new.unit_price_includes_tax then
187
+ new.amount_incl_tax := null;
188
+ new.amount_untaxed := v_gross;
189
+ return new;
190
+ end if;
191
+
192
+ new.amount_incl_tax := v_gross;
193
+ if tg_op = 'INSERT'
194
+ or not old.unit_price_includes_tax
195
+ or old.amount_incl_tax is distinct from v_gross then
196
+ select t.amount into v_rate from taxes t where t.id = new.tax_id;
197
+ if coalesce(v_rate, 0) <= -100 then
198
+ raise exception 'unsupported_included_rate: a price cannot include a tax of % per cent', v_rate;
199
+ end if;
200
+ new.amount_untaxed :=
201
+ v_gross - round_amount(v_gross - v_gross / (1 + coalesce(v_rate, 0) / 100), v_round);
202
+ end if;
203
+ return new;
204
+ end;
205
+ $$;
206
+
207
+ comment on function document_lines_amount_untaxed() is
208
+ 'Derives a line''s amounts from its quantity, price and discount, rounded once at the decimals of the document''s currency: the net, the gross where the price includes the tax, and the snapshot of whether it does.';
209
+
210
+ -- ---------------------------------------------------------------------------
211
+ -- 4. The group is the unit of the rounding
212
+ --
213
+ -- One pass per tax group of the document whose lines are quoted with the tax
214
+ -- in them. The tax is rounded once on the gross of the group (BR-CO-14), the
215
+ -- base is the gross less that tax — so the two add back to the price that was
216
+ -- quoted, always — and the base is shared over the lines in proportion to
217
+ -- their gross, the last of them taking the remainder.
218
+ --
219
+ -- It writes each share only where it differs from what is stored. That is not
220
+ -- on its own enough to stop the loop it would otherwise start — the share goes
221
+ -- back through the `before` trigger and the column's own scale can round it, so
222
+ -- the comparison never settles in a currency with three decimals — which is why
223
+ -- the trigger below it allocates on the outermost write only.
224
+ -- ---------------------------------------------------------------------------
225
+
226
+ create or replace function documents_allocate_included_tax(p_document_id uuid)
227
+ returns void
228
+ language plpgsql
229
+ as $$
230
+ declare
231
+ v_company uuid;
232
+ v_currency char(3);
233
+ v_round money_rounding;
234
+ v_gross numeric;
235
+ v_tax numeric;
236
+ v_base numeric;
237
+ v_left numeric;
238
+ v_share numeric;
239
+ v_group record;
240
+ v_line record;
241
+ begin
242
+ -- The ordinary case, and the one this has to cost nothing in: a document
243
+ -- with no line quoted with its tax in it.
244
+ if not exists (
245
+ select 1 from document_lines dl
246
+ where dl.document_id = p_document_id
247
+ and dl.line_type = 'product'
248
+ and dl.unit_price_includes_tax
249
+ ) then
250
+ return;
251
+ end if;
252
+
253
+ select d.company_id, d.currency_code into v_company, v_currency
254
+ from documents d where d.id = p_document_id;
255
+ if not found then
256
+ return;
257
+ end if;
258
+ v_round := rounding_of(v_company, v_currency);
259
+
260
+ for v_group in
261
+ select dl.tax_id,
262
+ max(t.code) as tax_code,
263
+ max(t.amount) as rate,
264
+ bool_and(dl.unit_price_includes_tax) as all_inclusive,
265
+ coalesce(sum(dl.amount_incl_tax), 0) as gross
266
+ from document_lines dl
267
+ join taxes t on t.id = dl.tax_id
268
+ where dl.document_id = p_document_id
269
+ and dl.line_type = 'product'
270
+ group by dl.tax_id
271
+ having bool_or(dl.unit_price_includes_tax)
272
+ order by 1
273
+ loop
274
+ -- Half a group is not a group. The only way to build one is to change the
275
+ -- tax between two line writes of the same draft, and adding a gross line
276
+ -- to a net one would produce a base nobody can explain.
277
+ if not v_group.all_inclusive then
278
+ raise exception 'mixed_price_include: tax % prices some lines of document % with the tax in them and others without',
279
+ v_group.tax_code, p_document_id;
280
+ end if;
281
+ if coalesce(v_group.rate, 0) <= -100 then
282
+ raise exception 'unsupported_included_rate: a price cannot include a tax of % per cent', v_group.rate;
283
+ end if;
284
+
285
+ v_gross := v_group.gross;
286
+ v_tax := round_amount(v_gross - v_gross / (1 + coalesce(v_group.rate, 0) / 100), v_round);
287
+ v_base := v_gross - v_tax;
288
+ v_left := v_base;
289
+
290
+ for v_line in
291
+ select dl.id,
292
+ coalesce(dl.amount_incl_tax, 0) as gross,
293
+ row_number() over (order by dl.sequence, dl.id)
294
+ = count(*) over () as is_last
295
+ from document_lines dl
296
+ where dl.document_id = p_document_id
297
+ and dl.line_type = 'product'
298
+ and dl.tax_id = v_group.tax_id
299
+ order by dl.sequence, dl.id
300
+ loop
301
+ if v_line.is_last then
302
+ v_share := v_left;
303
+ else
304
+ v_share := case when v_gross = 0 then 0
305
+ else round_amount(v_base * v_line.gross / v_gross, v_round) end;
306
+ v_left := v_left - v_share;
307
+ end if;
308
+
309
+ update document_lines
310
+ set amount_untaxed = v_share
311
+ where id = v_line.id
312
+ and amount_untaxed is distinct from v_share;
313
+ end loop;
314
+ end loop;
315
+ end;
316
+ $$;
317
+
318
+ comment on function documents_allocate_included_tax(uuid) is
319
+ 'Turns the gross of every tax group quoted with the tax in it into a base: the tax rounded once on the group (BR-CO-14), the base the gross less that tax, shared over the lines in proportion to their gross with the remainder on the last. Refuses a group that is half inclusive, by name.';
320
+
321
+ revoke execute on function documents_allocate_included_tax(uuid) from public, anon;
322
+ grant execute on function documents_allocate_included_tax(uuid) to authenticated, service_role;
323
+
324
+ -- The allocation runs before the totals, because the totals read the bases it
325
+ -- writes.
326
+ create or replace function document_lines_refresh_totals()
327
+ returns trigger
328
+ language plpgsql
329
+ as $$
330
+ begin
331
+ -- The allocator writes its shares back through this same trigger, and what it
332
+ -- wrote is already the answer. Only the outermost write allocates: a second
333
+ -- pass would be work for nothing, and in a currency whose decimals the column
334
+ -- cannot hold — `amount_untaxed` is `numeric(16, 2)`, which is a gap of its
335
+ -- own — a share that comes back rounded would never compare equal and the
336
+ -- two would call each other for ever.
337
+ if pg_trigger_depth() <= 1 then
338
+ perform documents_allocate_included_tax(coalesce(new.document_id, old.document_id));
339
+ end if;
340
+ perform documents_refresh_totals(coalesce(new.document_id, old.document_id));
341
+ return null;
342
+ end;
343
+ $$;
344
+
345
+ comment on function document_lines_refresh_totals() is
346
+ 'After a line moves: shares out the base of every tax group quoted with its tax in it, then refreshes the three totals of the document.';
347
+
348
+ -- ---------------------------------------------------------------------------
349
+ -- 5. The breakdown reads the gross where there is one
350
+ --
351
+ -- `base_amount` is unchanged and needs to be: the bases the allocator wrote
352
+ -- add up to the group's base exactly. What changes is the tax beside it, for
353
+ -- an inclusive group only — computed from the gross the group was quoted at
354
+ -- rather than re-derived from the base, for the reason in the header.
355
+ -- `tax_charged` keeps its shape: the unrounded tax times the share of it that
356
+ -- reaches the other party, rounded once.
357
+ -- ---------------------------------------------------------------------------
358
+
359
+ create or replace view document_tax_summary
360
+ with (security_invoker = true) as
361
+ select l.document_id,
362
+ l.company_id,
363
+ d.doc_type,
364
+ l.tax_id,
365
+ t.code as tax_code,
366
+ t.name as tax_name,
367
+ t.vat_category,
368
+ t.amount as tax_rate,
369
+ sum(l.amount_untaxed) as base_amount,
370
+ -- Gross tax: what the VAT return reports.
371
+ round_amount(
372
+ case when bool_and(l.unit_price_includes_tax)
373
+ then sum(l.amount_incl_tax)
374
+ - sum(l.amount_incl_tax) / (1 + coalesce(t.amount, 0) / 100)
375
+ else sum(l.amount_untaxed) * coalesce(t.amount, 0) / 100
376
+ end,
377
+ rounding_of(l.company_id, d.currency_code)) as tax_amount,
378
+ -- Charged tax: what the other party actually pays. Zero when the tax
379
+ -- postings net out, which is exactly what self-assessment means.
380
+ round_amount(
381
+ case when bool_and(l.unit_price_includes_tax)
382
+ then sum(l.amount_incl_tax)
383
+ - sum(l.amount_incl_tax) / (1 + coalesce(t.amount, 0) / 100)
384
+ else sum(l.amount_untaxed) * coalesce(t.amount, 0) / 100
385
+ end
386
+ * coalesce((
387
+ select sum(tp.factor_percent)
388
+ from tax_postings tp
389
+ where tp.tax_id = t.id
390
+ and tp.posting_type in ('tax', 'tax_on_base')
391
+ and tp.document_kind = case
392
+ when d.doc_type in ('sale_credit_note', 'purchase_credit_note')
393
+ then 'credit_note'::tax_document_kind
394
+ else 'invoice'::tax_document_kind
395
+ end
396
+ ), 100) / 100,
397
+ rounding_of(l.company_id, d.currency_code)) as tax_charged
398
+ from document_lines l
399
+ join documents d on d.id = l.document_id
400
+ left join taxes t on t.id = l.tax_id
401
+ where l.line_type = 'product'
402
+ group by l.document_id, l.company_id, d.doc_type, d.currency_code, l.tax_id,
403
+ t.id, t.code, t.name, t.vat_category, t.amount;
404
+
405
+ comment on view document_tax_summary is
406
+ 'VAT breakdown of a document, one row per tax, rounded once on the group (EN 16931 BR-CO-14) — on the group''s base, or on the gross it was quoted at where the price includes the tax.';
407
+
408
+ -- ---------------------------------------------------------------------------
409
+ -- 6. The line items say which price they are showing
410
+ --
411
+ -- Two columns are appended: whether this line was quoted with its tax in it,
412
+ -- and the gross it was quoted at. A renderer that prints a till receipt needs
413
+ -- both, and so does anybody reading a base back to the price it came from.
414
+ --
415
+ -- `unit_price` keeps its own meaning — the price as it was keyed — and it is
416
+ -- therefore no longer BT-146 on a line quoted gross, because BT-146 is the net
417
+ -- price. Publishing the net one is a column this view does not get today: the
418
+ -- honest definition is the base divided by the quantity, which is the only one
419
+ -- that keeps BR-CO-10 true after the group's remainder has landed on a line,
420
+ -- and writing it needs a precision that is the *price* column's and not the
421
+ -- currency's — which `round_amount` does not express and a cast to
422
+ -- `numeric(16, 6)` would smuggle past the rule that there is one place where
423
+ -- decimals are decided. That is a gap and it is written up in
424
+ -- `docs/international.md` rather than solved by an exception here.
425
+ -- ---------------------------------------------------------------------------
426
+
427
+ create or replace view document_line_items
428
+ with (security_invoker = true) as
429
+ select l.id as document_line_id,
430
+ l.document_id,
431
+ l.company_id,
432
+ l.sequence,
433
+ l.line_type,
434
+ l.name as item_name, -- BT-153
435
+ l.description as item_description, -- BT-154
436
+ p.code as seller_item_identifier, -- BT-155
437
+ l.product_id,
438
+ p.kind as product_kind,
439
+ l.quantity, -- BT-129
440
+ l.unit_code, -- BT-130
441
+ l.unit_price, -- as keyed; gross where the tax is in it
442
+ l.discount_percent,
443
+ l.amount_untaxed, -- BT-131
444
+ l.tax_id,
445
+ l.vat_category, -- BT-151
446
+ l.vat_rate, -- BT-152
447
+ l.account_id,
448
+ t.treatment as tax_treatment,
449
+ t.exemption_code as tax_exemption_code, -- BT-121
450
+ t.cash_basis as tax_cash_basis,
451
+ l.unit_price_includes_tax,
452
+ l.amount_incl_tax
453
+ from document_lines l
454
+ left join products p on p.id = l.product_id
455
+ left join taxes t on t.id = l.tax_id;
456
+
457
+ comment on view document_line_items is
458
+ 'Document lines with the EN 16931 item terms — BT-153 name, BT-154 description, BT-155 the seller identifier — what decides a legal mention on the line, and, where the price was quoted with the tax in it, the gross it was quoted at. unit_price is the price as keyed, which is that gross: BT-146 is the net price and this view does not publish it yet.';
459
+
460
+ grant select on table document_tax_summary, document_line_items
461
+ to authenticated, service_role;
462
+
463
+ -- ---------------------------------------------------------------------------
464
+ -- 7. The invoice behind a link says which price it is showing
465
+ --
466
+ -- `shared_document()` names every field of a line by hand, so a column added
467
+ -- to `document_line_items` does not reach it: a customer following the link to
468
+ -- a retail invoice would be shown a `unit_price` that is gross, beside an
469
+ -- `amount_untaxed` that is net, with nothing saying which is which. The two
470
+ -- fields go into `lines[]` — `unit_price_includes_tax` and `amount_incl_tax`
471
+ -- — and a renderer can print a till receipt from the gross or an EN 16931
472
+ -- invoice from the net without guessing.
473
+ --
474
+ -- Everything else is `20260915191200` unchanged, republished because a
475
+ -- function is replaced whole. Its grants are restated below because a replaced
476
+ -- function keeps the privileges it had and this one is the single door an
477
+ -- anonymous reader comes through: saying so again is cheaper than trusting it.
478
+ --
479
+ -- The order matters and holds: this file recreates `document_line_items`
480
+ -- above, then the function that reads it. Between the two migrations nothing
481
+ -- is broken either — a function records no dependency on a view, so
482
+ -- `20260915191200`'s `shared_document()` kept working against the view as it
483
+ -- stood, and reads two more fields only from here on.
484
+ -- ---------------------------------------------------------------------------
485
+
486
+ create or replace function shared_document(p_token text)
487
+ returns jsonb
488
+ language plpgsql
489
+ security definer
490
+ set search_path = public, pg_temp
491
+ as $$
492
+ declare
493
+ v_share document_shares%rowtype;
494
+ v_doc documents%rowtype;
495
+ v_header document_header%rowtype;
496
+ v_last_payment date;
497
+ v_payload jsonb;
498
+ begin
499
+ select * into v_share
500
+ from document_shares
501
+ where token_hash = encode(sha256(convert_to(coalesce(p_token, ''), 'UTF8')), 'hex');
502
+
503
+ if v_share.id is null
504
+ or v_share.revoked_at is not null
505
+ or (v_share.expires_at is not null and v_share.expires_at <= now())
506
+ or v_share.subject_kind <> 'document' then
507
+ return null;
508
+ end if;
509
+
510
+ select * into v_doc from documents where id = v_share.document_id;
511
+ if v_doc.id is null or document_share_refusal(v_doc) is not null then
512
+ return null;
513
+ end if;
514
+
515
+ -- The link was used. Counted before the payload is built, so a reader who
516
+ -- gives up halfway is still a reader.
517
+ update document_shares
518
+ set view_count = view_count + 1,
519
+ last_viewed_at = now()
520
+ where id = v_share.id;
521
+
522
+ select * into v_header from document_header where document_id = v_doc.id;
523
+
524
+ -- When the last money against it arrived. The other side of every matching
525
+ -- on the document's own third-party lines, where that side is a payment.
526
+ select max(p.payment_date) into v_last_payment
527
+ from entry_lines dl
528
+ join reconciliations r
529
+ on r.debit_line_id = dl.id or r.credit_line_id = dl.id
530
+ join entry_lines ol
531
+ on ol.id = case when r.debit_line_id = dl.id then r.credit_line_id else r.debit_line_id end
532
+ join payments p on p.entry_id = ol.entry_id
533
+ where dl.entry_id = v_doc.entry_id;
534
+
535
+ v_payload := jsonb_build_object(
536
+ 'document', jsonb_build_object(
537
+ 'type', v_header.doc_type,
538
+ 'number', v_header.number,
539
+ 'document_date', v_header.document_date,
540
+ 'due_date', v_header.due_date,
541
+ 'delivery_date', v_header.delivery_date,
542
+ 'currency', v_header.currency_code,
543
+ -- The document's own, snapshotted when it was created and frozen when it
544
+ -- was posted. A customer who has since changed preference does not
545
+ -- change what was sent to them.
546
+ 'language', v_doc.language,
547
+ 'payment_terms', v_header.payment_terms,
548
+ 'payment_reference', v_header.payment_reference,
549
+ 'buyer_reference', v_header.buyer_reference,
550
+ 'order_reference', v_header.order_reference,
551
+ -- BT-22, the note the seller wrote on the invoice itself. An internal
552
+ -- remark about a customer is `contacts.notes` and is not in this object.
553
+ 'note', v_header.note
554
+ ),
555
+ 'seller', jsonb_build_object(
556
+ 'name', v_header.seller_name,
557
+ 'legal_name', v_header.seller_legal_name,
558
+ 'legal_form', v_header.seller_legal_form,
559
+ 'vat_number', v_header.seller_vat_number,
560
+ 'registration_number', v_header.seller_registration_number,
561
+ 'address_line1', v_header.seller_address_line1,
562
+ 'address_line2', v_header.seller_address_line2,
563
+ 'postal_code', v_header.seller_postal_code,
564
+ 'city', v_header.seller_city,
565
+ 'country', v_header.seller_country,
566
+ 'email', v_header.seller_email,
567
+ 'phone', v_header.seller_phone,
568
+ 'website', v_header.seller_website,
569
+ 'logo_url', v_header.seller_logo_url,
570
+ 'iban', v_header.payee_iban,
571
+ 'bic', v_header.payee_bic
572
+ ),
573
+ 'buyer', jsonb_build_object(
574
+ 'name', v_header.buyer_name,
575
+ 'vat_number', v_header.buyer_vat_number,
576
+ 'registration_number', v_header.buyer_registration_number,
577
+ 'address_line1', v_header.buyer_address_line1,
578
+ 'address_line2', v_header.buyer_address_line2,
579
+ 'postal_code', v_header.buyer_postal_code,
580
+ 'city', v_header.buyer_city,
581
+ 'country', v_header.buyer_country
582
+ ),
583
+ 'lines', coalesce((
584
+ select jsonb_agg(jsonb_build_object(
585
+ 'sequence', li.sequence,
586
+ 'type', li.line_type,
587
+ 'name', li.item_name,
588
+ 'description', li.item_description,
589
+ 'quantity', li.quantity::text,
590
+ 'unit_code', li.unit_code,
591
+ -- The price as it was keyed. On a line quoted with the tax
592
+ -- in it that is the gross one, and the two fields below say so
593
+ -- rather than leaving a reader to divide `amount_untaxed` by
594
+ -- the quantity and wonder.
595
+ 'unit_price', li.unit_price::text,
596
+ 'unit_price_includes_tax', li.unit_price_includes_tax,
597
+ 'discount_percent', li.discount_percent::text,
598
+ 'amount_untaxed', li.amount_untaxed::text,
599
+ 'amount_incl_tax', li.amount_incl_tax::text,
600
+ 'tax_category', li.vat_category,
601
+ 'tax_rate', li.vat_rate::text,
602
+ 'tax_exemption_code', li.tax_exemption_code)
603
+ order by li.sequence)
604
+ from document_line_items li
605
+ where li.document_id = v_doc.id), '[]'::jsonb),
606
+ 'tax_summary', coalesce((
607
+ select jsonb_agg(jsonb_build_object(
608
+ 'name', ts.tax_name,
609
+ 'category', ts.vat_category,
610
+ 'rate', ts.tax_rate::text,
611
+ 'base_amount', ts.base_amount::text,
612
+ -- What the other party actually pays, which is what an invoice
613
+ -- prints: zero where the tax self-assesses, and what the four
614
+ -- totals below add up from.
615
+ 'tax_amount', ts.tax_charged::text)
616
+ order by ts.tax_rate, ts.tax_name)
617
+ from document_tax_summary ts
618
+ where ts.document_id = v_doc.id), '[]'::jsonb),
619
+ 'totals', jsonb_build_object(
620
+ 'amount_untaxed', v_header.amount_untaxed::text,
621
+ 'amount_tax', v_header.amount_tax::text,
622
+ 'amount_total', v_header.amount_total::text
623
+ ),
624
+ -- The one thing that legitimately moves after the document was sent, so a
625
+ -- link a customer keeps stays worth opening.
626
+ 'payment', jsonb_build_object(
627
+ 'state', v_header.payment_state,
628
+ 'amount_paid', v_header.amount_paid::text,
629
+ 'amount_residual', v_header.amount_residual::text,
630
+ 'last_payment_date', v_last_payment
631
+ ),
632
+ 'legal_mentions', coalesce((
633
+ select jsonb_agg(jsonb_build_object(
634
+ 'code', lm.code,
635
+ 'text', lm.text)
636
+ order by lm.sequence, lm.code)
637
+ from document_legal_mentions lm
638
+ where lm.document_id = v_doc.id), '[]'::jsonb)
639
+ );
640
+
641
+ return v_payload;
642
+ end;
643
+ $$;
644
+
645
+ comment on function shared_document(text) is
646
+ 'One document, read by whoever holds its link: the header, the lines with the price as it was keyed and whether that price holds the tax, the tax breakdown, the totals, the legal mentions in the language the document was written in, and what is still owed today. Returns null — the same null, in the same shape — for a token that is unknown, withdrawn, expired, or onto a document that may no longer be shared.';
647
+
648
+ revoke execute on function shared_document(text) from public;
649
+ grant execute on function shared_document(text) to anon, authenticated, service_role;