@fayz-ai/plugin-financial 0.25.9 → 0.26.0
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.
- package/dist/CashRegistersView-2G7DTIAA.js +4 -0
- package/dist/CashRegistersView-2G7DTIAA.js.map +1 -0
- package/dist/CommissionRulesView-2RW4ZCDT.js +878 -0
- package/dist/CommissionRulesView-2RW4ZCDT.js.map +1 -0
- package/dist/FinancialGeneralSettings-WXJOCJPZ.js +3 -0
- package/dist/FinancialGeneralSettings-WXJOCJPZ.js.map +1 -0
- package/dist/FinancialPage-PGODULVH.js +6484 -0
- package/dist/FinancialPage-PGODULVH.js.map +1 -0
- package/dist/PersonStatementWidget-ULXXXYZ4.js +214 -0
- package/dist/PersonStatementWidget-ULXXXYZ4.js.map +1 -0
- package/dist/chunk-7VKJLBYY.js +1736 -0
- package/dist/chunk-7VKJLBYY.js.map +1 -0
- package/dist/chunk-CNXAVGSW.js +13 -0
- package/dist/chunk-CNXAVGSW.js.map +1 -0
- package/dist/chunk-K37Q7SKX.js +4340 -0
- package/dist/chunk-K37Q7SKX.js.map +1 -0
- package/dist/chunk-MGDAJLUQ.js +823 -0
- package/dist/chunk-MGDAJLUQ.js.map +1 -0
- package/dist/chunk-POYZFEAN.js +64 -0
- package/dist/chunk-POYZFEAN.js.map +1 -0
- package/dist/components/BatchClassify.d.ts +26 -0
- package/dist/components/BatchClassify.d.ts.map +1 -0
- package/dist/components/CashSessionHeaderControl.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4944 -45345
- package/dist/index.js.map +1 -1
- package/dist/migrations/main.browser.js +143 -0
- package/dist/migrations/main.browser.js.map +1 -0
- package/dist/migrations/main.js +26230 -0
- package/dist/migrations/main.js.map +1 -0
- package/dist/payable-agent-TCKHUAQG.js +3 -0
- package/dist/payable-agent-TCKHUAQG.js.map +1 -0
- package/dist/views/ReconciliationView.d.ts.map +1 -1
- package/package.json +11 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/migrations/index.ts"],"names":[],"mappings":";AAIO,IAAM,sBAAA,GAAyB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAq/H/B,IAAM,6DAAA,GAAgE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AA6ItE,IAAM,0DAAA,GAA6D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AA+GnE,IAAM,8CAAA,GAAiD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAmJvD,IAAM,wCAAA,GAA2C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqLjD,IAAM,qDAAA,GAAwD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmF9D,IAAM,gEAAA,GAAmE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAmUzE,IAAM,8DAAA,GAAiE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6QvE,IAAM,+DAAA,GAAkE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuXxE,IAAM,uDAAA,GAA0D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AA2dhE,IAAM,+DAAA,GAAkE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmRxE,IAAM,0DAAA,GAA6D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AA+JnE,IAAM,wDAAA,GAA2D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAyKjE,IAAM,gEAAA,GAAmE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AA6YzE,IAAM,kDAAA,GAAqD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AA2N3D,IAAM,yCAAA,GAA4C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAkJlD,IAAM,4CAAA,GAA+C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AA0JrD,IAAM,iDAAA,GAAoD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AA0H1D,IAAM,iDAAA,GAAoD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAqE1D,IAAM,yCAAA,GAA4C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuGlD,IAAM,yDAAA,GAA4D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBlE,IAAM,yCAAA,GAA4C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAsIlD,IAAM,iDAAA,GAAoD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AA4H1D,IAAM,6CAAA,GAAgD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAkItD,IAAM,2CAAA,GAA8C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAiIpD,IAAM,4CAAA,GAA+C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAiMrD,IAAM,mDAAA,GAAsD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AA8H5D,IAAM,0DAAA,GAA6D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAySnE,IAAM,8BAAA,GAAiC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAuBvC,IAAM,0CAAA,GAA6C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiHnD,IAAM,0CAAA,GAA6C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAmDnD,IAAM,4DAAA,GAA+D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAiFrE,IAAM,8CAAA,GAAiD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoevD,IAAM,+CAAA,GAAkD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAqNxD,IAAM,uDAAA,GAA0D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwNhE,IAAM,oDAAA,GAAuD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgS7D,IAAM,sDAAA,GAAyD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkL/D,IAAM,yCAAA,GAA4C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyLlD,IAAM,6DAAA,GAAgE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AA8ItE,IAAM,6DAAA,GAAgE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AA2FtE,IAAM,uCAAA,GAA0C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgHhD,IAAM,kDAAA,GAAqD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0F3D,IAAM,mDAAA,GAAsD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4M5D,IAAM,mDAAA,GAAsD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAkN5D,IAAM,6CAAA,GAAgD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyLtD,IAAM,sDAAA,GAAyD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwH/D,IAAM,oDAAA,GAAuD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkJ7D,IAAM,+DAAA,GAAkE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAgLxE,IAAM,8DAAA,GAAiE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2UvE,IAAM,8CAAA,GAAiD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAkOvD,IAAM,kDAAA,GAAqD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAiM3D,IAAM,8DAAA,GAAiE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAuLvE,IAAM,+CAAA,GAAkD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AA0KxD,IAAM,oDAAA,GAAuD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AA4K7D,IAAM,4CAAA,GAA+C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAgLrD,IAAM,8CAAA,GAAiD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAmHvD,IAAM,qCAAA,GAAwC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4I9C,IAAM,wCAAA,GAA2C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAkGjD,IAAM,kCAAA,GAAqC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAoG3C,IAAM,iCAAA,GAAoC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AA6F1C,IAAM,mDAAA,GAAsD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAwD5D,IAAM,8CAAA,GAAiD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8evD,IAAM,+CAAA,GAAkD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmOxD,IAAM,uDAAA,GAA0D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkOhE,IAAM,oDAAA,GAAuD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4S7D,IAAM,sDAAA,GAAyD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4L/D,IAAM,yCAAA,GAA4C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmMlD,IAAM,yCAAA,GAA4C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAiMlD,IAAM,yDAAA,GAA4D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4MlE,IAAM,iDAAA,GAAoD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0K1D,IAAM,gDAAA,GAAmD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAqMzD,IAAM,6CAAA,GAAgD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4RtD,IAAM,+CAAA,GAAkD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyQxD,IAAM,uDAAA,GAA0D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgJhE,IAAM,sDAAA,GAAyD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6D/D,IAAM,qDAAA,GAAwD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6T9D,IAAM,4DAAA,GAA+D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DrE,IAAM,kDAAA,GAAqD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AA4G3D,IAAM,4DAAA,GAA+D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAyHrE,IAAM,yDAAA,GAA4D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBlE,IAAM,2DAAA,GAA8D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAwSpE,IAAM,+CAAA,GAAkD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAkKxD,IAAM,oDAAA,GAAuD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAoF7D,IAAM,uEAAA,GAA0E,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AA4DhF,IAAM,qDAAA,GAAwD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwP9D,IAAM,uEAAA,GAA0E,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuEhF,IAAM,iEAAA,GAAoE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AA6M1E,IAAM,4DAAA,GAA+D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkGrE,IAAM,4DAAA,GAA+D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuHrE,IAAM,sDAAA,GAAyD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAwD/D,IAAM,0CAAA,GAA6C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AA2HnD,IAAM,2CAAA,GAA8C,CAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAqFpD,IAAM,kDAAA,GAAqD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAmK3D,IAAM,mDAAA,GAAsD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqV5D,IAAM,4DAAA,GAA+D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;AAAA;AAAA;AAAA;AAmSrE,IAAM,qDAAA,GAAwD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAmE9D,IAAM,qDAAA,GAAwD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmF9D,IAAM,2CAAA,GAA8C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AA2MpD,IAAM,wEAAA,GAA2E,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AA4KjF,IAAM,qEAAA,GAAwE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAuF9E,IAAM,gDAAA,GAAmD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuFzD,IAAM,wDAAA,GAA2D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAiDjE,IAAM,2DAAA,GAA8D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAyFpE,IAAM,sDAAA,GAAyD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAqC/D,IAAM,0CAAA,GAA6C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAyEnD,IAAM,sEAAA,GAAyE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAkL/E,IAAM,kDAAA,GAAqD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAoG3D,IAAM,mDAAA,GAAsD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyI5D,IAAM,2CAAA,GAA8C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6IpD,IAAM,uDAAA,GAA0D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAqRhE,IAAM,yDAAA,GAA4D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AA2ElE,IAAM,6CAAA,GAAgD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AA4ItD,IAAM,0DAAA,GAA6D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAyLnE,IAAM,yDAAA,GAA4D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAsIlE,IAAM,uDAAA,GAA0D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAkGhE,IAAM,sDAAA,GAAyD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AA4I/D,IAAM,uCAAA,GAA0C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAuEhD,IAAM,+CAAA,GAAkD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAoBxD,IAAM,2CAAA,GAA8C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAwFpD,IAAM,sDAAA,GAAyD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAgI/D,IAAM,sDAAA,GAAyD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAszB/D,IAAM,2CAAA,GAA8C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8BpD,IAAM,sDAAA,GAAyD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqE/D,IAAM,yDAAA,GAA4D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyHlE,IAAM,sCAAA,GAAyC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqN/C,IAAM,qDAAA,GAAwD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqL9D,IAAM,yCAAA,GAA4C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuIlD,IAAM,2DAAA,GAA8D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmLpE,IAAM,iDAAA,GAAoD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuD1D,IAAM,uDAAA,GAA0D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqChE,IAAM,kCAAA,GAAqC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAmB3C,IAAM,oDAAA,GAAuD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAiX7D,IAAM,6DAAA,GAAgE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCtE,IAAM,wDAAA,GAA2D,CAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAuEjE,IAAM,uCAAA,GAA0C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBhD,IAAM,uDAAA,GAA0D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqDhE,IAAM,uDAAA,GAA0D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgQhE,IAAM,UAAA,GAAiD;AAAA,EAC5D,EAAE,EAAA,EAAI,cAAA,EAAgB,GAAA,EAAK,sBAAA,EAAuB;AAAA,EAClD,EAAE,EAAA,EAAI,qDAAA,EAAuD,GAAA,EAAK,6DAAA,EAA8D;AAAA,EAChI,EAAE,EAAA,EAAI,kDAAA,EAAoD,GAAA,EAAK,0DAAA,EAA2D;AAAA,EAC1H,EAAE,EAAA,EAAI,sCAAA,EAAwC,GAAA,EAAK,8CAAA,EAA+C;AAAA,EAClG,EAAE,EAAA,EAAI,gCAAA,EAAkC,GAAA,EAAK,wCAAA,EAAyC;AAAA,EACtF,EAAE,EAAA,EAAI,6CAAA,EAA+C,GAAA,EAAK,qDAAA,EAAsD;AAAA,EAChH,EAAE,EAAA,EAAI,wDAAA,EAA0D,GAAA,EAAK,gEAAA,EAAiE;AAAA,EACtI,EAAE,EAAA,EAAI,sDAAA,EAAwD,GAAA,EAAK,8DAAA,EAA+D;AAAA,EAClI,EAAE,EAAA,EAAI,uDAAA,EAAyD,GAAA,EAAK,+DAAA,EAAgE;AAAA,EACpI,EAAE,EAAA,EAAI,+CAAA,EAAiD,GAAA,EAAK,uDAAA,EAAwD;AAAA,EACpH,EAAE,EAAA,EAAI,uDAAA,EAAyD,GAAA,EAAK,+DAAA,EAAgE;AAAA,EACpI,EAAE,EAAA,EAAI,kDAAA,EAAoD,GAAA,EAAK,0DAAA,EAA2D;AAAA,EAC1H,EAAE,EAAA,EAAI,gDAAA,EAAkD,GAAA,EAAK,wDAAA,EAAyD;AAAA,EACtH,EAAE,EAAA,EAAI,wDAAA,EAA0D,GAAA,EAAK,gEAAA,EAAiE;AAAA,EACtI,EAAE,EAAA,EAAI,0CAAA,EAA4C,GAAA,EAAK,kDAAA,EAAmD;AAAA,EAC1G,EAAE,EAAA,EAAI,iCAAA,EAAmC,GAAA,EAAK,yCAAA,EAA0C;AAAA,EACxF,EAAE,EAAA,EAAI,oCAAA,EAAsC,GAAA,EAAK,4CAAA,EAA6C;AAAA,EAC9F,EAAE,EAAA,EAAI,yCAAA,EAA2C,GAAA,EAAK,iDAAA,EAAkD;AAAA,EACxG,EAAE,EAAA,EAAI,yCAAA,EAA2C,GAAA,EAAK,iDAAA,EAAkD;AAAA,EACxG,EAAE,EAAA,EAAI,iCAAA,EAAmC,GAAA,EAAK,yCAAA,EAA0C;AAAA,EACxF,EAAE,EAAA,EAAI,iDAAA,EAAmD,GAAA,EAAK,yDAAA,EAA0D;AAAA,EACxH,EAAE,EAAA,EAAI,iCAAA,EAAmC,GAAA,EAAK,yCAAA,EAA0C;AAAA,EACxF,EAAE,EAAA,EAAI,yCAAA,EAA2C,GAAA,EAAK,iDAAA,EAAkD;AAAA,EACxG,EAAE,EAAA,EAAI,qCAAA,EAAuC,GAAA,EAAK,6CAAA,EAA8C;AAAA,EAChG,EAAE,EAAA,EAAI,mCAAA,EAAqC,GAAA,EAAK,2CAAA,EAA4C;AAAA,EAC5F,EAAE,EAAA,EAAI,oCAAA,EAAsC,GAAA,EAAK,4CAAA,EAA6C;AAAA,EAC9F,EAAE,EAAA,EAAI,2CAAA,EAA6C,GAAA,EAAK,mDAAA,EAAoD;AAAA,EAC5G,EAAE,EAAA,EAAI,kDAAA,EAAoD,GAAA,EAAK,0DAAA,EAA2D;AAAA,EAC1H,EAAE,EAAA,EAAI,sBAAA,EAAwB,GAAA,EAAK,8BAAA,EAA+B;AAAA,EAClE,EAAE,EAAA,EAAI,kCAAA,EAAoC,GAAA,EAAK,0CAAA,EAA2C;AAAA,EAC1F,EAAE,EAAA,EAAI,kCAAA,EAAoC,GAAA,EAAK,0CAAA,EAA2C;AAAA,EAC1F,EAAE,EAAA,EAAI,oDAAA,EAAsD,GAAA,EAAK,4DAAA,EAA6D;AAAA,EAC9H,EAAE,EAAA,EAAI,sCAAA,EAAwC,GAAA,EAAK,8CAAA,EAA+C;AAAA,EAClG,EAAE,EAAA,EAAI,uCAAA,EAAyC,GAAA,EAAK,+CAAA,EAAgD;AAAA,EACpG,EAAE,EAAA,EAAI,+CAAA,EAAiD,GAAA,EAAK,uDAAA,EAAwD;AAAA,EACpH,EAAE,EAAA,EAAI,4CAAA,EAA8C,GAAA,EAAK,oDAAA,EAAqD;AAAA,EAC9G,EAAE,EAAA,EAAI,8CAAA,EAAgD,GAAA,EAAK,sDAAA,EAAuD;AAAA,EAClH,EAAE,EAAA,EAAI,iCAAA,EAAmC,GAAA,EAAK,yCAAA,EAA0C;AAAA,EACxF,EAAE,EAAA,EAAI,qDAAA,EAAuD,GAAA,EAAK,6DAAA,EAA8D;AAAA,EAChI,EAAE,EAAA,EAAI,qDAAA,EAAuD,GAAA,EAAK,6DAAA,EAA8D;AAAA,EAChI,EAAE,EAAA,EAAI,+BAAA,EAAiC,GAAA,EAAK,uCAAA,EAAwC;AAAA,EACpF,EAAE,EAAA,EAAI,0CAAA,EAA4C,GAAA,EAAK,kDAAA,EAAmD;AAAA,EAC1G,EAAE,EAAA,EAAI,2CAAA,EAA6C,GAAA,EAAK,mDAAA,EAAoD;AAAA,EAC5G,EAAE,EAAA,EAAI,2CAAA,EAA6C,GAAA,EAAK,mDAAA,EAAoD;AAAA,EAC5G,EAAE,EAAA,EAAI,qCAAA,EAAuC,GAAA,EAAK,6CAAA,EAA8C;AAAA,EAChG,EAAE,EAAA,EAAI,8CAAA,EAAgD,GAAA,EAAK,sDAAA,EAAuD;AAAA,EAClH,EAAE,EAAA,EAAI,4CAAA,EAA8C,GAAA,EAAK,oDAAA,EAAqD;AAAA,EAC9G,EAAE,EAAA,EAAI,uDAAA,EAAyD,GAAA,EAAK,+DAAA,EAAgE;AAAA,EACpI,EAAE,EAAA,EAAI,sDAAA,EAAwD,GAAA,EAAK,8DAAA,EAA+D;AAAA,EAClI,EAAE,EAAA,EAAI,sCAAA,EAAwC,GAAA,EAAK,8CAAA,EAA+C;AAAA,EAClG,EAAE,EAAA,EAAI,0CAAA,EAA4C,GAAA,EAAK,kDAAA,EAAmD;AAAA,EAC1G,EAAE,EAAA,EAAI,sDAAA,EAAwD,GAAA,EAAK,8DAAA,EAA+D;AAAA,EAClI,EAAE,EAAA,EAAI,uCAAA,EAAyC,GAAA,EAAK,+CAAA,EAAgD;AAAA,EACpG,EAAE,EAAA,EAAI,4CAAA,EAA8C,GAAA,EAAK,oDAAA,EAAqD;AAAA,EAC9G,EAAE,EAAA,EAAI,oCAAA,EAAsC,GAAA,EAAK,4CAAA,EAA6C;AAAA,EAC9F,EAAE,EAAA,EAAI,sCAAA,EAAwC,GAAA,EAAK,8CAAA,EAA+C;AAAA,EAClG,EAAE,EAAA,EAAI,6BAAA,EAA+B,GAAA,EAAK,qCAAA,EAAsC;AAAA,EAChF,EAAE,EAAA,EAAI,gCAAA,EAAkC,GAAA,EAAK,wCAAA,EAAyC;AAAA,EACtF,EAAE,EAAA,EAAI,0BAAA,EAA4B,GAAA,EAAK,kCAAA,EAAmC;AAAA,EAC1E,EAAE,EAAA,EAAI,yBAAA,EAA2B,GAAA,EAAK,iCAAA,EAAkC;AAAA,EACxE,EAAE,EAAA,EAAI,2CAAA,EAA6C,GAAA,EAAK,mDAAA,EAAoD;AAAA,EAC5G,EAAE,EAAA,EAAI,sCAAA,EAAwC,GAAA,EAAK,8CAAA,EAA+C;AAAA,EAClG,EAAE,EAAA,EAAI,uCAAA,EAAyC,GAAA,EAAK,+CAAA,EAAgD;AAAA,EACpG,EAAE,EAAA,EAAI,+CAAA,EAAiD,GAAA,EAAK,uDAAA,EAAwD;AAAA,EACpH,EAAE,EAAA,EAAI,4CAAA,EAA8C,GAAA,EAAK,oDAAA,EAAqD;AAAA,EAC9G,EAAE,EAAA,EAAI,8CAAA,EAAgD,GAAA,EAAK,sDAAA,EAAuD;AAAA,EAClH,EAAE,EAAA,EAAI,iCAAA,EAAmC,GAAA,EAAK,yCAAA,EAA0C;AAAA,EACxF,EAAE,EAAA,EAAI,iCAAA,EAAmC,GAAA,EAAK,yCAAA,EAA0C;AAAA,EACxF,EAAE,EAAA,EAAI,iDAAA,EAAmD,GAAA,EAAK,yDAAA,EAA0D;AAAA,EACxH,EAAE,EAAA,EAAI,yCAAA,EAA2C,GAAA,EAAK,iDAAA,EAAkD;AAAA,EACxG,EAAE,EAAA,EAAI,wCAAA,EAA0C,GAAA,EAAK,gDAAA,EAAiD;AAAA,EACtG,EAAE,EAAA,EAAI,qCAAA,EAAuC,GAAA,EAAK,6CAAA,EAA8C;AAAA,EAChG,EAAE,EAAA,EAAI,uCAAA,EAAyC,GAAA,EAAK,+CAAA,EAAgD;AAAA,EACpG,EAAE,EAAA,EAAI,+CAAA,EAAiD,GAAA,EAAK,uDAAA,EAAwD;AAAA,EACpH,EAAE,EAAA,EAAI,8CAAA,EAAgD,GAAA,EAAK,sDAAA,EAAuD;AAAA,EAClH,EAAE,EAAA,EAAI,6CAAA,EAA+C,GAAA,EAAK,qDAAA,EAAsD;AAAA,EAChH,EAAE,EAAA,EAAI,oDAAA,EAAsD,GAAA,EAAK,4DAAA,EAA6D;AAAA,EAC9H,EAAE,EAAA,EAAI,0CAAA,EAA4C,GAAA,EAAK,kDAAA,EAAmD;AAAA,EAC1G,EAAE,EAAA,EAAI,oDAAA,EAAsD,GAAA,EAAK,4DAAA,EAA6D;AAAA,EAC9H,EAAE,EAAA,EAAI,iDAAA,EAAmD,GAAA,EAAK,yDAAA,EAA0D;AAAA,EACxH,EAAE,EAAA,EAAI,mDAAA,EAAqD,GAAA,EAAK,2DAAA,EAA4D;AAAA,EAC5H,EAAE,EAAA,EAAI,uCAAA,EAAyC,GAAA,EAAK,+CAAA,EAAgD;AAAA,EACpG,EAAE,EAAA,EAAI,4CAAA,EAA8C,GAAA,EAAK,oDAAA,EAAqD;AAAA,EAC9G,EAAE,EAAA,EAAI,+DAAA,EAAiE,GAAA,EAAK,uEAAA,EAAwE;AAAA,EACpJ,EAAE,EAAA,EAAI,6CAAA,EAA+C,GAAA,EAAK,qDAAA,EAAsD;AAAA,EAChH,EAAE,EAAA,EAAI,+DAAA,EAAiE,GAAA,EAAK,uEAAA,EAAwE;AAAA,EACpJ,EAAE,EAAA,EAAI,yDAAA,EAA2D,GAAA,EAAK,iEAAA,EAAkE;AAAA,EACxI,EAAE,EAAA,EAAI,oDAAA,EAAsD,GAAA,EAAK,4DAAA,EAA6D;AAAA,EAC9H,EAAE,EAAA,EAAI,oDAAA,EAAsD,GAAA,EAAK,4DAAA,EAA6D;AAAA,EAC9H,EAAE,EAAA,EAAI,8CAAA,EAAgD,GAAA,EAAK,sDAAA,EAAuD;AAAA,EAClH,EAAE,EAAA,EAAI,kCAAA,EAAoC,GAAA,EAAK,0CAAA,EAA2C;AAAA,EAC1F,EAAE,EAAA,EAAI,mCAAA,EAAqC,GAAA,EAAK,2CAAA,EAA4C;AAAA,EAC5F,EAAE,EAAA,EAAI,0CAAA,EAA4C,GAAA,EAAK,kDAAA,EAAmD;AAAA,EAC1G,EAAE,EAAA,EAAI,2CAAA,EAA6C,GAAA,EAAK,mDAAA,EAAoD;AAAA,EAC5G,EAAE,EAAA,EAAI,oDAAA,EAAsD,GAAA,EAAK,4DAAA,EAA6D;AAAA,EAC9H,EAAE,EAAA,EAAI,6CAAA,EAA+C,GAAA,EAAK,qDAAA,EAAsD;AAAA,EAChH,EAAE,EAAA,EAAI,6CAAA,EAA+C,GAAA,EAAK,qDAAA,EAAsD;AAAA,EAChH,EAAE,EAAA,EAAI,mCAAA,EAAqC,GAAA,EAAK,2CAAA,EAA4C;AAAA,EAC5F,EAAE,EAAA,EAAI,gEAAA,EAAkE,GAAA,EAAK,wEAAA,EAAyE;AAAA,EACtJ,EAAE,EAAA,EAAI,6DAAA,EAA+D,GAAA,EAAK,qEAAA,EAAsE;AAAA,EAChJ,EAAE,EAAA,EAAI,wCAAA,EAA0C,GAAA,EAAK,gDAAA,EAAiD;AAAA,EACtG,EAAE,EAAA,EAAI,gDAAA,EAAkD,GAAA,EAAK,wDAAA,EAAyD;AAAA,EACtH,EAAE,EAAA,EAAI,mDAAA,EAAqD,GAAA,EAAK,2DAAA,EAA4D;AAAA,EAC5H,EAAE,EAAA,EAAI,8CAAA,EAAgD,GAAA,EAAK,sDAAA,EAAuD;AAAA,EAClH,EAAE,EAAA,EAAI,kCAAA,EAAoC,GAAA,EAAK,0CAAA,EAA2C;AAAA,EAC1F,EAAE,EAAA,EAAI,8DAAA,EAAgE,GAAA,EAAK,sEAAA,EAAuE;AAAA,EAClJ,EAAE,EAAA,EAAI,0CAAA,EAA4C,GAAA,EAAK,kDAAA,EAAmD;AAAA,EAC1G,EAAE,EAAA,EAAI,2CAAA,EAA6C,GAAA,EAAK,mDAAA,EAAoD;AAAA,EAC5G,EAAE,EAAA,EAAI,mCAAA,EAAqC,GAAA,EAAK,2CAAA,EAA4C;AAAA,EAC5F,EAAE,EAAA,EAAI,+CAAA,EAAiD,GAAA,EAAK,uDAAA,EAAwD;AAAA,EACpH,EAAE,EAAA,EAAI,iDAAA,EAAmD,GAAA,EAAK,yDAAA,EAA0D;AAAA,EACxH,EAAE,EAAA,EAAI,qCAAA,EAAuC,GAAA,EAAK,6CAAA,EAA8C;AAAA,EAChG,EAAE,EAAA,EAAI,kDAAA,EAAoD,GAAA,EAAK,0DAAA,EAA2D;AAAA,EAC1H,EAAE,EAAA,EAAI,iDAAA,EAAmD,GAAA,EAAK,yDAAA,EAA0D;AAAA,EACxH,EAAE,EAAA,EAAI,+CAAA,EAAiD,GAAA,EAAK,uDAAA,EAAwD;AAAA,EACpH,EAAE,EAAA,EAAI,8CAAA,EAAgD,GAAA,EAAK,sDAAA,EAAuD;AAAA,EAClH,EAAE,EAAA,EAAI,+BAAA,EAAiC,GAAA,EAAK,uCAAA,EAAwC;AAAA,EACpF,EAAE,EAAA,EAAI,uCAAA,EAAyC,GAAA,EAAK,+CAAA,EAAgD;AAAA,EACpG,EAAE,EAAA,EAAI,mCAAA,EAAqC,GAAA,EAAK,2CAAA,EAA4C;AAAA,EAC5F,EAAE,EAAA,EAAI,8CAAA,EAAgD,GAAA,EAAK,sDAAA,EAAuD;AAAA,EAClH,EAAE,EAAA,EAAI,8CAAA,EAAgD,GAAA,EAAK,sDAAA,EAAuD;AAAA,EAClH,EAAE,EAAA,EAAI,mCAAA,EAAqC,GAAA,EAAK,2CAAA,EAA4C;AAAA,EAC5F,EAAE,EAAA,EAAI,8CAAA,EAAgD,GAAA,EAAK,sDAAA,EAAuD;AAAA,EAClH,EAAE,EAAA,EAAI,iDAAA,EAAmD,GAAA,EAAK,yDAAA,EAA0D;AAAA,EACxH,EAAE,EAAA,EAAI,8BAAA,EAAgC,GAAA,EAAK,sCAAA,EAAuC;AAAA,EAClF,EAAE,EAAA,EAAI,6CAAA,EAA+C,GAAA,EAAK,qDAAA,EAAsD;AAAA,EAChH,EAAE,EAAA,EAAI,iCAAA,EAAmC,GAAA,EAAK,yCAAA,EAA0C;AAAA,EACxF,EAAE,EAAA,EAAI,mDAAA,EAAqD,GAAA,EAAK,2DAAA,EAA4D;AAAA,EAC5H,EAAE,EAAA,EAAI,yCAAA,EAA2C,GAAA,EAAK,iDAAA,EAAkD;AAAA,EACxG,EAAE,EAAA,EAAI,+CAAA,EAAiD,GAAA,EAAK,uDAAA,EAAwD;AAAA,EACpH,EAAE,EAAA,EAAI,0BAAA,EAA4B,GAAA,EAAK,kCAAA,EAAmC;AAAA,EAC1E,EAAE,EAAA,EAAI,4CAAA,EAA8C,GAAA,EAAK,oDAAA,EAAqD;AAAA,EAC9G,EAAE,EAAA,EAAI,qDAAA,EAAuD,GAAA,EAAK,6DAAA,EAA8D;AAAA,EAChI,EAAE,EAAA,EAAI,gDAAA,EAAkD,GAAA,EAAK,wDAAA,EAAyD;AAAA,EACtH,EAAE,EAAA,EAAI,+BAAA,EAAiC,GAAA,EAAK,uCAAA,EAAwC;AAAA,EACpF,EAAE,EAAA,EAAI,+CAAA,EAAiD,GAAA,EAAK,uDAAA,EAAwD;AAAA,EACpH,EAAE,EAAA,EAAI,+CAAA,EAAiD,GAAA,EAAK,uDAAA;AAC9D","file":"main.js","sourcesContent":["// AUTO-GENERATED from 000_baseline.sql, 001_the_comanda_becomes_a_receivable_when_it_closes.sql, 002_payment_types_are_the_country_not_the_tenant.sql, 003_the_commission_rule_gets_a_table.sql, 004_one_bridge_for_three_doors.sql, 005_the_appointment_closes_like_the_comanda.sql, 006_the_month_can_be_closed_and_the_book_can_be_proven.sql, 007_the_invoice_knows_who_issued_it_and_what_it_owes.sql, 008_the_commission_rule_learns_to_choose_and_to_split.sql, 009_the_commission_is_calculated_line_by_line.sql, 010_the_invoice_raised_from_a_sale_gets_its_own_lines.sql, 011_a_new_tenant_can_take_money_on_the_first_day.sql, 012_the_commissions_become_one_bill_per_person.sql, 013_a_discount_has_a_reason_and_someone_who_allowed_it.sql, 014_the_bill_that_comes_back_every_month.sql, 015_the_sale_knows_what_it_cost.sql, 016_the_overdue_bill_gets_a_letter.sql, 017_the_receipt_is_a_query_not_a_layout.sql, 018_the_caixa_shows_what_it_should_have.sql, 019_who_did_it_is_already_known.sql, 020_a_rule_with_nobody_to_pay_pays_the_executor.sql, 021_the_most_specific_rule_wins.sql, 022_an_unknown_attribute_does_not_match.sql, 023_reapurar_nao_cria_um_lote_orfao.sql, 024_o_bordero_paga_o_que_ele_soma.sql, 025_a_meta_do_mes_tem_tres_degraus.sql, 026_quem_fecha_o_caixa_nao_e_quem_confere.sql, 027_o_vale_presente_nasce_em_lote_e_e_um_passivo.sql, 028_o_cartao_tem_cor.sql, 029_the_kiosk_sale_is_a_sale_too.sql, 030_the_census_can_see_the_kiosk.sql, 031_the_bank_line_is_evidence_not_a_second_payment.sql, 032_contas_a_pagar_ganham_onde_morar.sql, 033_o_vencimento_aponta_para_o_titulo.sql, 034_a_unidade_tem_conta_e_a_pessoa_tem_uma_so.sql, 035_a_classificacao_contabil_chega_inteira.sql, 036_a_fusao_para_de_dobrar_a_propria_memoria.sql, 037_a_venda_de_balcao_tem_conta.sql, 038_a_linha_da_fatura_alcanca_quem_veio_pelo_pedido.sql, 039_o_titulo_de_uma_venda_cancelada_nasce_cancelado.sql, 040_a_dre_le_a_linha_da_venda.sql, 041_o_caixa_do_v1_nao_segura_o_movimento.sql, 042_o_cancelado_do_v1_e_o_cancelled_do_v2.sql, 043_o_movimento_sabe_de_que_unidade_ele_e.sql, 044_a_dre_nao_paga_pedagio_por_item.sql, 045_a_conta_so_classifica_se_a_natureza_casa.sql, 046_o_demonstrativo_mostra_o_plano_inteiro.sql, 047_a_conta_reconhece_a_contraparte_na_segunda_oferta.sql, 048_a_parcela_e_a_conta_do_v1_e_a_baixa_e_a_alocacao.sql, 049_a_parcela_carrega_o_cancelamento.sql, 050_o_saldo_do_titulo_se_calcula_uma_vez.sql, 051_o_saldo_se_calcula_so_na_linha_que_a_tela_mostra.sql, 052_a_lista_de_contas_fala_vencimento.sql, 053_um_titulo_parcelado_vence_varias_vezes.sql, 054_as_datas_em_que_o_titulo_vence.sql, 055_a_lista_de_contas_lista_parcelas.sql, 056_what_the_port_left_open.sql, 057_a_linha_diz_que_tem_recibo.sql, 058_o_clipe_usa_o_indice.sql, 059_o_clipe_sai_da_view.sql, 060_o_recebivel_volta_da_arvore_derrubada.sql, 061_contas_a_pagar_ganham_onde_morar.sql, 062_o_vencimento_aponta_para_o_titulo.sql, 063_a_unidade_tem_conta_e_a_pessoa_tem_uma_so.sql, 064_a_classificacao_contabil_chega_inteira.sql, 065_a_fusao_para_de_dobrar_a_propria_memoria.sql, 066_a_venda_de_balcao_tem_conta.sql, 067_o_recibo_da_conta_atravessa.sql, 068_a_unidade_entra_com_a_linha_nao_depois_dela.sql, 069_o_titulo_que_ja_existe_esta_migrado.sql, 070_a_parcela_nasce_antes_do_movimento.sql, 071_a_parcela_tem_uma_assinatura_so.sql, 072_o_caixa_do_v1_guarda_quem_o_abriu.sql, 073_o_caixa_pertence_a_conta_que_o_v1_lhe_deu.sql, 074_o_movimento_volta_a_apontar_para_o_caixa.sql, 075_a_venda_cancelada_cancela_o_titulo_dela.sql, 076_o_plano_de_contas_do_cliente_vence_o_do_modelo.sql, 077_o_resumo_financeiro_se_soma_no_banco.sql, 078_o_saldo_da_conta_se_calcula_so_na_conta_pedida.sql, 079_as_portas_de_porte_novas_nao_nascem_abertas.sql, 080_a_remessa_e_a_comissao_a_pagar_tem_onde_morar.sql, 081_a_remessa_e_a_comissao_atravessam.sql, 082_o_beneficiario_da_comissao_e_uma_conta.sql, 083_a_conta_do_banco_e_uma_conta_e_a_fatura_tem_dois_destinos.sql, 084_a_nota_de_entrada_tem_um_titulo_a_pagar.sql, 085_o_resumo_le_cinco_mil_linhas_e_nao_duzentas_e_oitenta_mil.sql, 086_a_cerca_se_pergunta_uma_vez_e_nao_uma_vez_por_linha.sql, 087_o_saldo_de_uma_fatura_nao_soma_o_razao_inteiro.sql, 088_a_conta_sem_pessoa_e_do_sistema_nao_de_ninguem.sql, 089_o_fornecedor_aponta_para_o_plano_migrado.sql, 090_o_dre_recorta_antes_de_somar.sql, 091_a_recorrencia_repete_os_itens.sql, 092_a_conta_paga_no_cartao_sai_do_cartao.sql, 093_o_pagamento_diz_qual_conta_ele_quitou.sql, 094_o_titulo_da_nota_de_entrada_sabe_de_quem_ele_e.sql, 095_o_caixa_do_v1_e_registro_nao_referencia.sql, 096_o_caixa_do_v1_ja_tem_dono_e_pode_entrar.sql, 097_o_caixa_conta_linha_por_linha.sql, 098_a_conta_que_ninguem_pagou_ainda_muda_de_data_e_de_parcelas.sql, 099_os_itens_de_uma_conta_sem_recebimento_ainda_se_corrigem.sql, 100_a_conta_bancaria_diz_de_quem_ela_e.sql, 101_o_cartao_se_reconhece_pelos_quatro_ultimos.sql, 102_a_bandeira_do_cartao_e_do_pais_nao_do_lojista.sql, 103_o_rail_da_ficha_e_igual_em_todo_cadastro.sql, 104_servico_e_o_que_a_casa_vende.sql, 105_a_ocorrencia_provisionada_vira_conta_quando_alguem_manda.sql, 106_a_recorrencia_nova_tambem_provisiona.sql, 107_a_previsao_comeca_onde_a_regua_comeca.sql, 108_mudou_a_regua_muda_a_previsao.sql, 109_a_regua_grava_camelCase_e_o_sql_lia_snake.sql, 110_trocar_de_fornecedor_enquanto_ninguem_pagou.sql, 111_reparcelar_sem_apagar_a_parcela.sql, 112_a_linha_do_extrato_nao_e_um_lancamento_ainda.sql, 113_so_a_conta_integrada_aparece_na_conciliacao.sql, 114_a_parcela_diz_por_qual_conta_ela_foi_paga.sql, 115_a_remessa_aponta_para_a_conta_que_existe.sql, 116_um_estorno_nao_se_estorna.sql, 117_copying_lines_is_an_internal_step.sql, 118_gift_card_found_by_exact_code.sql, 119_an_unpaid_legacy_bill_can_be_rescheduled.sql, 120_cancelar_e_por_item_com_motivo_e_destino.sql, 121_o_motivo_e_do_estabelecimento.sql, 122_o_cancelamento_fecha_as_portas_que_abriu.sql, 123_the_mirror_does_not_bill_a_recurrence_twice.sql, 124_money_knows_its_currency.sql, 125_the_summary_adds_one_currency_at_a_time.sql, 126_a_bill_line_can_enter_stock.sql, 127_a_bill_line_enters_stock_as_it_really_arrived.sql, 128_a_stock_receipt_shows_what_was_paid.sql, 129_a_bill_can_be_written_in_another_currency.sql, 130_a_bill_can_be_tagged.sql, 131_the_bill_follows_the_price_of_the_sale.sql, 132_the_statement_source_knows_its_provider_account.sql, 133_reversing_a_payment_releases_its_bank_line.sql, 134_a_bill_knows_who_filed_it.sql, 135_a_bill_and_its_lines_are_written_together.sql, 136_a_bank_statement_arrives_from_the_service.sql — regenerate with scripts/embed-migrations.mjs\n// SQL files are the source of truth; this inline copy lets the manifest declare\n// migrations as data. Do not edit by hand — run the embed script instead.\n\nexport const MIGRATION_000_BASELINE = `-- ============================================================================\n-- plugins/plugin-financial/src/migrations/000_baseline.sql — what installing this provisions, as one file.\n--\n-- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:\n-- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs\n-- prove the result still matches.\n--\n-- This replaced 48 migration files. They are kept, unapplied, in\n-- migrations.archive/ — a year of decisions is worth reading even when it is no\n-- longer worth replaying.\n--\n-- Object order is fixed rather than dependency-sorted: schemas, types,\n-- sequences, tables, defaults, functions, constraints, views, indexes, foreign\n-- keys, triggers, row security, policies, grants. Dependencies BETWEEN units\n-- are carried by the order packages/db/chain.json declares.\n-- ============================================================================\n\n-- A LANGUAGE sql function binds its body at CREATE time, and 570 functions\n-- sorted by name are not sorted by who calls whom. This is what lets them load\n-- in any order; every body is still compiled on first call, so a genuinely\n-- broken reference surfaces then rather than never. Session-scoped.\nSET check_function_bodies = false;\n\n\n\n-- ── default privileges: reset ───────────────────────────────────────────\n-- Cleared BEFORE anything is created, so every object below gets the ACL the\n-- dump describes rather than the image's defaults on top of it. Restored at\n-- the end of this file.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\n\n-- ── table ───────────────────────────────────────────────────────────────\n\nCREATE TABLE public.plg_financial_allocations (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n payment_movement_id uuid NOT NULL,\n installment_id uuid NOT NULL,\n amount numeric(14,2) NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_by uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n owner_id uuid,\n CONSTRAINT plg_financial_allocations_amount_check CHECK ((amount > (0)::numeric))\n);\n\nALTER TABLE ONLY public.plg_financial_allocations FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_invoice_installments (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n invoice_id uuid NOT NULL,\n number integer NOT NULL,\n due_date date NOT NULL,\n amount numeric(14,2) NOT NULL,\n cancelled_at timestamp with time zone,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n owner_id uuid,\n CONSTRAINT plg_financial_invoice_installments_amount_check CHECK ((amount >= (0)::numeric)),\n CONSTRAINT plg_financial_invoice_installments_number_check CHECK ((number >= 1))\n);\n\nALTER TABLE ONLY public.plg_financial_invoice_installments FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_invoices (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n direction text NOT NULL,\n counterparty_person_id uuid,\n order_id uuid,\n number text,\n competence_date date DEFAULT CURRENT_DATE NOT NULL,\n issue_date date DEFAULT CURRENT_DATE NOT NULL,\n total numeric(14,2) NOT NULL,\n currency text DEFAULT 'BRL'::text NOT NULL,\n description text,\n notes text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n idempotency_key text,\n cancelled_at timestamp with time zone,\n cancel_reason text,\n created_by uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n owner_id uuid,\n CONSTRAINT plg_financial_invoices_direction_check CHECK ((direction = ANY (ARRAY['receivable'::text, 'payable'::text]))),\n CONSTRAINT plg_financial_invoices_total_check CHECK ((total >= (0)::numeric))\n);\n\nALTER TABLE ONLY public.plg_financial_invoices FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_movements (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n invoice_id uuid,\n direction text NOT NULL,\n movement_kind text NOT NULL,\n amount numeric(14,2) NOT NULL,\n paid_amount numeric(14,2) DEFAULT 0,\n status text DEFAULT 'pending'::text NOT NULL,\n due_date date NOT NULL,\n payment_date date,\n installment_number integer,\n payment_method_id uuid,\n bank_account_id uuid,\n cash_session_id uuid,\n card_brand text,\n card_installments integer,\n notes text,\n metadata jsonb DEFAULT '{}'::jsonb,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n fee_amount numeric(14,2) DEFAULT 0 NOT NULL,\n external_id text,\n external_source text,\n reconciled_at timestamp with time zone,\n matched_movement_id uuid,\n payment_method_type_id uuid,\n settles_movement_id uuid,\n unit_id uuid,\n owner_id uuid,\n financial_invoice_id uuid,\n financial_invoice_installment_id uuid,\n debit_account_id uuid,\n credit_account_id uuid,\n method text,\n nsu text,\n reversed_by_id uuid,\n reverses_id uuid,\n reason text,\n idempotency_key text,\n created_by uuid,\n CONSTRAINT plg_financial_movements_kind_check CHECK ((movement_kind = ANY (ARRAY['bill'::text, 'payment'::text, 'transfer'::text, 'commission'::text]))),\n CONSTRAINT plg_financial_movements_legs_check CHECK (((movement_kind = ANY (ARRAY['bill'::text, 'commission'::text])) OR (metadata ? 'legacy_mirror'::text) OR ((debit_account_id IS NOT NULL) AND (credit_account_id IS NOT NULL) AND (debit_account_id <> credit_account_id))))\n);\n\nALTER TABLE ONLY public.plg_financial_movements FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_accounts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n kind text DEFAULT 'bank'::text NOT NULL,\n code text,\n name text NOT NULL,\n person_id uuid,\n account_type text,\n bank_name text,\n agency_number text,\n account_number text,\n credit_limit numeric(14,2),\n due_day integer,\n closing_day integer,\n best_purchase_day integer,\n settlement_days integer,\n is_active boolean DEFAULT true NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_by uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n owner_id uuid,\n CONSTRAINT plg_financial_accounts_check CHECK (((kind = 'person'::text) = (person_id IS NOT NULL))),\n CONSTRAINT plg_financial_accounts_check1 CHECK (((kind <> 'system'::text) OR (code IS NOT NULL))),\n CONSTRAINT plg_financial_accounts_kind_check CHECK ((kind = ANY (ARRAY['bank'::text, 'cash_physical'::text, 'cash_session'::text, 'card_receivable_credit'::text, 'card_receivable_debit'::text, 'card_payable'::text, 'person'::text, 'system'::text])))\n);\n\nALTER TABLE ONLY public.plg_financial_accounts FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_bank_accounts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n name text NOT NULL,\n account_type text DEFAULT 'bank_account'::text NOT NULL,\n bank_name text,\n account_number text,\n agency_number text,\n current_balance numeric(14,2) DEFAULT 0 NOT NULL,\n initial_balance numeric(14,2) DEFAULT 0 NOT NULL,\n credit_limit numeric(14,2),\n due_day integer,\n closing_day integer,\n is_active boolean DEFAULT true NOT NULL,\n unit_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n owner_id uuid\n);\n\nALTER TABLE ONLY public.plg_financial_bank_accounts FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_card_brands (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n name text NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n unit_id uuid,\n owner_id uuid\n);\n\nALTER TABLE ONLY public.plg_financial_card_brands FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_card_fee_configs (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n account_id uuid NOT NULL,\n card_brand_id uuid,\n brand text,\n min_installments integer DEFAULT 1 NOT NULL,\n max_installments integer DEFAULT 1 NOT NULL,\n fee_rate numeric(7,4) NOT NULL,\n settlement_days integer,\n is_active boolean DEFAULT true NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_by uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n owner_id uuid,\n CONSTRAINT plg_financial_card_fee_configs_check CHECK ((min_installments <= max_installments)),\n CONSTRAINT plg_financial_card_fee_configs_fee_rate_check CHECK (((fee_rate >= (0)::numeric) AND (fee_rate <= (100)::numeric))),\n CONSTRAINT plg_financial_card_fee_configs_max_installments_check CHECK ((max_installments >= 1)),\n CONSTRAINT plg_financial_card_fee_configs_min_installments_check CHECK ((min_installments >= 1))\n);\n\nALTER TABLE ONLY public.plg_financial_card_fee_configs FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_cash_register_sessions (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n bank_account_id uuid NOT NULL,\n status text DEFAULT 'open'::text NOT NULL,\n opened_at timestamp with time zone DEFAULT now() NOT NULL,\n opened_by_user_id uuid,\n opened_by_name text,\n opening_balance numeric(14,2) NOT NULL,\n closed_at timestamp with time zone,\n closed_by_user_id uuid,\n closed_by_name text,\n closing_balance numeric(14,2),\n expected_balance numeric(14,2),\n difference numeric(14,2),\n notes text,\n unit_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n owner_id uuid,\n account_id uuid,\n user_id uuid NOT NULL,\n expected_cash numeric(14,2),\n counted_cash numeric(14,2),\n difference_cash numeric(14,2),\n expected_card numeric(14,2),\n counted_card numeric(14,2),\n difference_card numeric(14,2),\n opening_movement_id uuid,\n closing_movement_id uuid,\n difference_movement_id uuid,\n open_idempotency_key text,\n close_idempotency_key text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n CONSTRAINT plg_financial_cash_register_sessions_status_check CHECK ((status = ANY (ARRAY['open'::text, 'closed'::text])))\n);\n\nALTER TABLE ONLY public.plg_financial_cash_register_sessions FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_cash_session_reviewers (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n session_id uuid NOT NULL,\n reviewer_id uuid NOT NULL,\n verdict text,\n notes text,\n reviewed_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n owner_id uuid,\n CONSTRAINT plg_financial_cash_session_reviewers_verdict_check CHECK (((verdict IS NULL) OR (verdict = ANY (ARRAY['approved'::text, 'rejected'::text]))))\n);\n\nALTER TABLE ONLY public.plg_financial_cash_session_reviewers FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_chart_of_accounts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n code text NOT NULL,\n name text NOT NULL,\n node_type text DEFAULT 'leaf'::text NOT NULL,\n parent_id uuid,\n is_active boolean DEFAULT true NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n account_type text,\n unit_id uuid,\n owner_id uuid\n);\n\nALTER TABLE ONLY public.plg_financial_chart_of_accounts FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_cost_centers (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n code text NOT NULL,\n name text NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n unit_id uuid,\n owner_id uuid\n);\n\nALTER TABLE ONLY public.plg_financial_cost_centers FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_invoice_items (\n item_id uuid NOT NULL,\n parent_kind text DEFAULT 'invoice'::text NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n chart_account_id uuid,\n cost_center_id uuid,\n tax_amount numeric(14,2) DEFAULT 0 NOT NULL,\n cfop text,\n ncm text,\n owner_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_financial_invoice_items_parent_kind_check CHECK ((parent_kind = 'invoice'::text))\n);\n\nALTER TABLE ONLY public.plg_financial_invoice_items FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_nfe (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n model text DEFAULT 'nfe'::text NOT NULL,\n status text DEFAULT 'draft'::text NOT NULL,\n number text,\n series text,\n order_id uuid,\n contact_id uuid,\n contact_name text,\n total_amount numeric(14,2) DEFAULT 0 NOT NULL,\n issued_at timestamp with time zone,\n access_key text,\n xml_url text,\n danfe_url text,\n emissor text,\n error_message text,\n observations text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n unit_id uuid,\n owner_id uuid\n);\n\nALTER TABLE ONLY public.plg_financial_nfe FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_payment_method_types (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n name text NOT NULL,\n transaction_type text NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n allowed_account_types jsonb DEFAULT '[]'::jsonb,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n unit_id uuid,\n owner_id uuid\n);\n\nALTER TABLE ONLY public.plg_financial_payment_method_types FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_financial_payment_methods (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n name text NOT NULL,\n payment_method_type_id uuid,\n is_active boolean DEFAULT true NOT NULL,\n discount_mode text,\n discount_value numeric(12,2) DEFAULT 0,\n interest_mode text,\n interest_value numeric(12,2) DEFAULT 0,\n min_installments integer DEFAULT 1,\n max_installments integer DEFAULT 1,\n service_filter_mode text DEFAULT 'all'::text,\n service_filter_ids jsonb DEFAULT '[]'::jsonb,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n unit_id uuid,\n owner_id uuid\n);\n\nALTER TABLE ONLY public.plg_financial_payment_methods FORCE ROW LEVEL SECURITY;\n\n\n-- ── function ────────────────────────────────────────────────────────────\n\nCREATE FUNCTION public.agent_financial_mark_payment_received(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_order_id uuid := nullif(p_payload ->> 'order_id', '')::uuid;\n v_method text := coalesce(nullif(p_payload ->> 'method', ''), 'cash');\n v_installments int := greatest(coalesce((p_payload ->> 'installments')::int, 1), 1);\n v_denial jsonb;\n v_order record;\n v_inst record;\n v_open numeric;\n v_paid numeric := 0;\n v_n int := 0;\nBEGIN\n IF v_order_id IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'error', 'order_id is required');\n END IF;\n\n SELECT o.id, o.status, o.total INTO v_order\n FROM public.orders o WHERE o.id = v_order_id AND o.tenant_id = p_tenant_id;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('ok', false, 'error', 'unknown order for this tenant');\n END IF;\n IF v_order.status = 'cancelled' THEN\n RETURN jsonb_build_object('ok', false, 'error', 'cannot receive payment on a cancelled order');\n END IF;\n\n v_denial := public.agent_guard(p_tenant_id, p_actor_user_id, 'financial.receivables', 'update');\n IF v_denial IS NOT NULL THEN\n RETURN jsonb_build_object('ok', false, 'denial', v_denial);\n END IF;\n\n -- Already fully settled? Asked of the balances, which are derived from the\n -- allocations — the one place the answer can come from.\n IF EXISTS (SELECT 1 FROM public.v_financial_installment_balances b WHERE b.order_id = v_order_id)\n AND NOT EXISTS (SELECT 1 FROM public.v_financial_installment_balances b\n WHERE b.order_id = v_order_id AND b.status NOT IN ('paid', 'cancelled')) THEN\n RETURN jsonb_build_object('ok', false, 'error', 'order is already fully paid');\n END IF;\n\n PERFORM set_config('request.jwt.claims',\n jsonb_build_object('sub', p_actor_user_id, 'role', 'authenticated')::text, true);\n PERFORM public.fn_invoice_from_order(v_order_id, current_date, NULL, v_installments, 'credit');\n\n FOR v_inst IN\n SELECT b.installment_id, b.balance\n FROM public.v_financial_installment_balances b\n WHERE b.order_id = v_order_id AND b.tenant_id = p_tenant_id AND b.balance > 0\n ORDER BY b.number NULLS FIRST\n LOOP\n v_open := round(v_inst.balance, 2);\n CONTINUE WHEN v_open <= 0;\n PERFORM public.financial_receive_payment(\n p_installment_id => v_inst.installment_id,\n p_amount => v_open,\n p_method => v_method,\n p_payment_date => current_date,\n p_notes => 'agent',\n p_metadata => jsonb_build_object('paidVia', 'agent', 'method', v_method));\n v_paid := v_paid + v_open;\n v_n := v_n + 1;\n END LOOP;\n\n RETURN jsonb_build_object('ok', true, 'order_id', v_order_id,\n 'installments_settled', v_n, 'amount', v_paid, 'method', v_method);\nEND $$;\n\nCREATE FUNCTION public.financial_cancel_invoice(p_invoice_id uuid, p_reason text, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_invoice public.plg_financial_invoices%ROWTYPE;\n v_paid numeric;\nBEGIN\n IF nullif(btrim(coalesce(p_reason, '')), '') IS NULL THEN\n RAISE EXCEPTION 'financial_cancel_invoice: a reason is required' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_invoice FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_cancel_invoice: invoice % not found', p_invoice_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_invoice.unit_id);\n\n -- idempotent by STATE (cancelled_at) rather than by a key lookup, which is\n -- right for a cancellation — and still racing without the claim: two callers\n -- both read cancelled_at IS NULL and both cancel.\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n\n IF v_invoice.cancelled_at IS NOT NULL THEN\n RETURN jsonb_build_object('invoice_id', v_invoice.id, 'status', 'cancelled', 'replayed', true);\n END IF;\n\n SELECT coalesce(sum(paid), 0) INTO v_paid FROM public.v_financial_installment_balances WHERE invoice_id = p_invoice_id;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'financial_cancel_invoice: invoice % has % already received; reverse the payments or cancel the paid items into credit first', v_invoice.number, v_paid\n USING ERRCODE = '55000';\n END IF;\n\n UPDATE public.plg_financial_invoices\n SET cancelled_at = now(), cancel_reason = btrim(p_reason),\n metadata = metadata || jsonb_build_object('cancel_idempotency_key', p_idempotency_key)\n WHERE id = p_invoice_id;\n UPDATE public.plg_financial_invoice_installments SET cancelled_at = now() WHERE invoice_id = p_invoice_id AND cancelled_at IS NULL;\n -- legacy mirror rows follow the installment\n UPDATE public.plg_financial_movements SET status = 'cancelled', updated_at = now()\n WHERE financial_invoice_id = p_invoice_id AND movement_kind = 'bill' AND status <> 'cancelled';\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.cancel', 'plg_financial_invoices', p_invoice_id::text,\n jsonb_build_object('reason', btrim(p_reason), 'idempotency_key', p_idempotency_key));\n RETURN jsonb_build_object('invoice_id', v_invoice.id, 'status', 'cancelled', 'replayed', false);\nEND $$;\n\nCREATE FUNCTION public.financial_cancel_paid_item(p_order_item_id uuid, p_reason text, p_idempotency_key text DEFAULT NULL::text, p_amount numeric DEFAULT NULL::numeric) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_item public.items%ROWTYPE;\n v_order public.orders%ROWTYPE;\n v_amount numeric;\n v_paid numeric; v_credited numeric;\n v_person_acc uuid; v_cancel_acc uuid; v_mov_id uuid; v_credit numeric;\nBEGIN\n IF nullif(btrim(coalesce(p_reason, '')), '') IS NULL THEN\n RAISE EXCEPTION 'financial_cancel_paid_item: a reason is required' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_item FROM public.items WHERE id = p_order_item_id AND tenant_id = v_tenant AND order_id IS NOT NULL;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_cancel_paid_item: order item % not found', p_order_item_id USING ERRCODE = 'P0002';\n END IF;\n SELECT * INTO v_order FROM public.orders WHERE id = v_item.order_id;\n PERFORM public.plg_financial_require_permission('financial.edit', v_order.unit_id);\n IF v_order.party_id IS NULL THEN\n RAISE EXCEPTION 'financial_cancel_paid_item: the sale has no customer to credit' USING ERRCODE = '55000';\n END IF;\n v_amount := round(coalesce(p_amount, v_item.total, 0), 2);\n IF v_amount <= 0 THEN\n RAISE EXCEPTION 'financial_cancel_paid_item: nothing to credit' USING ERRCODE = '22023';\n END IF;\n\n IF p_idempotency_key IS NOT NULL THEN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT m.id INTO v_mov_id FROM public.plg_financial_movements m WHERE m.tenant_id = v_tenant AND m.idempotency_key = p_idempotency_key;\n IF v_mov_id IS NOT NULL THEN\n v_person_acc := public.plg_financial_person_account(v_tenant, v_order.party_id);\n SELECT credit INTO v_credit FROM public.v_financial_customer_credit WHERE account_id = v_person_acc;\n RETURN jsonb_build_object('movement_id', v_mov_id, 'person_account_id', v_person_acc, 'credit_amount', v_amount, 'credit', coalesce(v_credit, 0), 'replayed', true);\n END IF;\n END IF;\n IF v_item.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'financial_cancel_paid_item: item is already cancelled' USING ERRCODE = '55000';\n END IF;\n\n -- \"paid\" = the sale's invoices received at least this much beyond what was already credited back\n SELECT coalesce(sum(t.paid), 0) INTO v_paid FROM public.v_financial_invoice_balances t WHERE t.order_id = v_order.id;\n SELECT coalesce(sum(m.amount), 0) INTO v_credited FROM public.plg_financial_movements m\n WHERE m.tenant_id = v_tenant AND m.status <> 'cancelled' AND m.reversed_by_id IS NULL\n AND m.metadata ->> 'cancelled_order_id' = v_order.id::text;\n IF v_paid - v_credited + 0.000001 < v_amount THEN\n RAISE EXCEPTION 'financial_cancel_paid_item: the sale has % received (% already credited back); the item is not paid for %', v_paid, v_credited, v_amount\n USING ERRCODE = '55000';\n END IF;\n\n v_person_acc := public.plg_financial_person_account(v_tenant, v_order.party_id);\n v_cancel_acc := public.plg_financial_system_account(v_tenant, 'cancellations');\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, invoice_id, direction, movement_kind, amount, paid_amount, status, due_date, payment_date, method,\n reason, notes, metadata, idempotency_key, created_by, debit_account_id, credit_account_id)\n VALUES\n (v_tenant, v_order.unit_id, v_order.id, 'debit', 'transfer', v_amount, v_amount, 'paid', current_date, current_date, 'credit',\n btrim(p_reason), NULL,\n jsonb_build_object('source', 'financial_cancel_paid_item', 'credit_source', 'cancellation',\n 'cancelled_order_item_id', v_item.id, 'cancelled_order_id', v_order.id, 'reason', btrim(p_reason)),\n p_idempotency_key, auth.uid(), v_cancel_acc, v_person_acc)\n RETURNING id INTO v_mov_id;\n\n UPDATE public.items\n SET cancelled_at = now(),\n cancel_reason = btrim(p_reason),\n metadata = coalesce(metadata, '{}'::jsonb) || jsonb_build_object('creditMovementId', v_mov_id)\n WHERE id = v_item.id;\n\n SELECT credit INTO v_credit FROM public.v_financial_customer_credit WHERE account_id = v_person_acc;\n PERFORM public.plg_financial_audit(v_tenant, 'financial.item.cancel_to_credit', 'items', v_item.id::text,\n jsonb_build_object('order_id', v_order.id, 'amount', v_amount, 'reason', btrim(p_reason), 'movement_id', v_mov_id, 'idempotency_key', p_idempotency_key));\n RETURN jsonb_build_object('movement_id', v_mov_id, 'person_account_id', v_person_acc, 'credit_amount', v_amount, 'credit', coalesce(v_credit, 0), 'replayed', false);\nEND $$;\n\nCREATE FUNCTION public.financial_cash_session_close(p_counted_cash numeric, p_counted_card numeric, p_idempotency_key text DEFAULT NULL::text, p_notes text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_actor uuid := auth.uid();\n v_s public.plg_financial_cash_register_sessions%ROWTYPE;\n v_exp_cash numeric; v_exp_card numeric; v_diff_cash numeric; v_diff_card numeric;\n v_counted_cash numeric := round(coalesce(p_counted_cash, 0), 2);\n v_counted_card numeric := round(coalesce(p_counted_card, 0), 2);\n v_close_mov uuid; v_diff_mov uuid; v_name text;\nBEGIN\n IF v_actor IS NULL THEN\n RAISE EXCEPTION 'financial_cash_session_close: a signed-in user is required' USING ERRCODE = '42501';\n END IF;\n -- replay?\n IF p_idempotency_key IS NOT NULL THEN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT * INTO v_s FROM public.plg_financial_cash_register_sessions s\n WHERE s.tenant_id = v_tenant AND s.close_idempotency_key = p_idempotency_key;\n IF FOUND THEN\n RETURN jsonb_build_object('session_id', v_s.id, 'expected_cash', v_s.expected_cash, 'counted_cash', v_s.counted_cash,\n 'difference_cash', v_s.difference_cash, 'expected_card', v_s.expected_card, 'counted_card', v_s.counted_card,\n 'difference_card', v_s.difference_card, 'replayed', true);\n END IF;\n END IF;\n\n SELECT * INTO v_s FROM public.plg_financial_cash_register_sessions s\n WHERE s.tenant_id = v_tenant AND s.user_id = v_actor AND s.status = 'open'\n ORDER BY s.opened_at DESC LIMIT 1;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_cash_session_close: you have no open cash session' USING ERRCODE = '55000';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.cash_session', v_s.unit_id);\n\n SELECT balance INTO v_exp_cash FROM public.v_financial_account_balances WHERE account_id = v_s.account_id;\n v_exp_cash := round(coalesce(v_exp_cash, 0), 2);\n SELECT coalesce(sum(m.amount), 0) INTO v_exp_card FROM public.plg_financial_movements m\n WHERE m.cash_session_id = v_s.id AND m.movement_kind = 'payment' AND m.status <> 'cancelled' AND m.reversed_by_id IS NULL\n AND m.method IN ('credit_card', 'debit_card');\n v_exp_card := round(coalesce(v_exp_card, 0), 2);\n v_diff_cash := round(v_counted_cash - v_exp_cash, 2);\n v_diff_card := round(v_counted_card - v_exp_card, 2);\n\n SELECT coalesce(nullif(u.raw_user_meta_data ->> 'full_name', ''), u.email) INTO v_name FROM auth.users u WHERE u.id = v_actor;\n\n -- counted cash returns to the cofre\n IF v_counted_cash > 0 THEN\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, direction, movement_kind, amount, paid_amount, status, due_date, payment_date, method, cash_session_id,\n bank_account_id, notes, metadata, created_by, debit_account_id, credit_account_id)\n VALUES\n (v_tenant, v_s.unit_id, 'debit', 'transfer', v_counted_cash, v_counted_cash, 'paid', current_date, current_date, 'cash', v_s.id,\n v_s.bank_account_id, 'Fechamento de caixa', jsonb_build_object('source', 'financial_cash_session_close', 'cash_session_id', v_s.id, 'role', 'closing'),\n v_actor, v_s.bank_account_id, v_s.account_id)\n RETURNING id INTO v_close_mov;\n END IF;\n -- the difference is a shortage (counted < expected: drawer credited from the\n -- shortage account, i.e. we lost it) or an overage (drawer had more: booked out to overage)\n IF v_diff_cash <> 0 THEN\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, direction, movement_kind, amount, paid_amount, status, due_date, payment_date, method, cash_session_id,\n reason, notes, metadata, created_by, debit_account_id, credit_account_id)\n VALUES\n (v_tenant, v_s.unit_id, CASE WHEN v_diff_cash < 0 THEN 'debit' ELSE 'credit' END, 'transfer', abs(v_diff_cash), abs(v_diff_cash), 'paid',\n current_date, current_date, 'cash', v_s.id,\n CASE WHEN v_diff_cash < 0 THEN 'Quebra de caixa (falta)' ELSE 'Quebra de caixa (sobra)' END, p_notes,\n jsonb_build_object('source', 'financial_cash_session_close', 'cash_session_id', v_s.id,\n 'role', CASE WHEN v_diff_cash < 0 THEN 'shortage' ELSE 'overage' END, 'difference', v_diff_cash),\n v_actor,\n CASE WHEN v_diff_cash < 0 THEN public.plg_financial_system_account(v_tenant, 'cash_shortage') ELSE v_s.account_id END,\n CASE WHEN v_diff_cash < 0 THEN v_s.account_id ELSE public.plg_financial_system_account(v_tenant, 'cash_overage') END)\n RETURNING id INTO v_diff_mov;\n END IF;\n\n UPDATE public.plg_financial_cash_register_sessions\n SET status = 'closed', closed_at = now(), closed_by_user_id = v_actor, closed_by_name = v_name,\n expected_cash = v_exp_cash, counted_cash = v_counted_cash, difference_cash = v_diff_cash,\n expected_card = v_exp_card, counted_card = v_counted_card, difference_card = v_diff_card,\n expected_balance = v_exp_cash, closing_balance = v_counted_cash, difference = v_diff_cash,\n closing_movement_id = v_close_mov, difference_movement_id = v_diff_mov,\n close_idempotency_key = p_idempotency_key,\n notes = coalesce(p_notes, notes)\n WHERE id = v_s.id;\n UPDATE public.plg_financial_accounts SET is_active = false WHERE id = v_s.account_id;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.cash_session.close', 'plg_financial_cash_register_sessions', v_s.id::text,\n jsonb_build_object('expected_cash', v_exp_cash, 'counted_cash', v_counted_cash, 'difference_cash', v_diff_cash,\n 'expected_card', v_exp_card, 'counted_card', v_counted_card, 'difference_card', v_diff_card,\n 'idempotency_key', p_idempotency_key));\n RETURN jsonb_build_object('session_id', v_s.id, 'expected_cash', v_exp_cash, 'counted_cash', v_counted_cash,\n 'difference_cash', v_diff_cash, 'expected_card', v_exp_card, 'counted_card', v_counted_card,\n 'difference_card', v_diff_card, 'closing_movement_id', v_close_mov, 'difference_movement_id', v_diff_mov, 'replayed', false);\nEND $$;\n\nCREATE FUNCTION public.financial_cash_session_open(p_cofre_account_id uuid, p_opening_amount numeric, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_actor uuid := auth.uid();\n v_cofre record;\n v_amount numeric := round(coalesce(p_opening_amount, 0), 2);\n v_sess uuid; v_acc uuid; v_mov uuid; v_name text;\nBEGIN\n IF v_actor IS NULL THEN\n RAISE EXCEPTION 'financial_cash_session_open: a signed-in user is required' USING ERRCODE = '42501';\n END IF;\n IF v_amount < 0 THEN\n RAISE EXCEPTION 'financial_cash_session_open: opening amount must be >= 0' USING ERRCODE = '22023';\n END IF;\n SELECT a.id, a.unit_id, a.kind, a.name INTO v_cofre FROM public.plg_financial_accounts a\n WHERE a.id = p_cofre_account_id AND a.tenant_id = v_tenant AND a.is_active;\n IF v_cofre.id IS NULL THEN\n RAISE EXCEPTION 'financial_cash_session_open: cofre account % not found', p_cofre_account_id USING ERRCODE = 'P0002';\n END IF;\n IF v_cofre.kind <> 'cash_physical' THEN\n RAISE EXCEPTION 'financial_cash_session_open: account % is not a cofre (cash_physical)', p_cofre_account_id USING ERRCODE = '23514';\n END IF;\n IF v_cofre.unit_id IS NULL THEN\n RAISE EXCEPTION 'financial_cash_session_open: the cofre has no unit' USING ERRCODE = '55000';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.cash_session', v_cofre.unit_id);\n\n IF p_idempotency_key IS NOT NULL THEN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT s.id, s.account_id, s.opening_movement_id INTO v_sess, v_acc, v_mov FROM public.plg_financial_cash_register_sessions s\n WHERE s.tenant_id = v_tenant AND s.open_idempotency_key = p_idempotency_key;\n IF v_sess IS NOT NULL THEN\n RETURN jsonb_build_object('session_id', v_sess, 'account_id', v_acc, 'opening_movement_id', v_mov, 'replayed', true);\n END IF;\n END IF;\n SELECT s.id INTO v_sess FROM public.plg_financial_cash_register_sessions s\n WHERE s.tenant_id = v_tenant AND s.user_id = v_actor AND s.unit_id = v_cofre.unit_id AND s.status = 'open';\n IF v_sess IS NOT NULL THEN\n RAISE EXCEPTION 'financial_cash_session_open: you already have an open cash session in this unit' USING ERRCODE = '55000';\n END IF;\n\n SELECT coalesce(nullif(u.raw_user_meta_data ->> 'full_name', ''), u.email, v_actor::text) INTO v_name FROM auth.users u WHERE u.id = v_actor;\n INSERT INTO public.plg_financial_accounts (tenant_id, unit_id, kind, name, metadata, created_by)\n VALUES (v_tenant, v_cofre.unit_id, 'cash_session', 'Caixinha · ' || coalesce(v_name, '') || ' · ' || to_char(now(), 'YYYY-MM-DD HH24:MI'),\n jsonb_build_object('cash_session', true), v_actor)\n RETURNING id INTO v_acc;\n\n INSERT INTO public.plg_financial_cash_register_sessions\n (tenant_id, unit_id, user_id, bank_account_id, account_id, status, opened_at, opened_by_user_id, opened_by_name,\n opening_balance, open_idempotency_key, metadata)\n VALUES\n (v_tenant, v_cofre.unit_id, v_actor, v_cofre.id, v_acc, 'open', now(), v_actor, v_name,\n v_amount, p_idempotency_key, jsonb_build_object('cofre_name', v_cofre.name))\n RETURNING id INTO v_sess;\n UPDATE public.plg_financial_accounts SET metadata = metadata || jsonb_build_object('cash_session_id', v_sess) WHERE id = v_acc;\n\n IF v_amount > 0 THEN\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, direction, movement_kind, amount, paid_amount, status, due_date, payment_date, method, cash_session_id,\n bank_account_id, notes, metadata, created_by, debit_account_id, credit_account_id)\n VALUES\n (v_tenant, v_cofre.unit_id, 'credit', 'transfer', v_amount, v_amount, 'paid', current_date, current_date, 'cash', v_sess,\n v_acc, 'Abertura de caixa', jsonb_build_object('source', 'financial_cash_session_open', 'cash_session_id', v_sess, 'role', 'opening'),\n v_actor, v_acc, v_cofre.id)\n RETURNING id INTO v_mov;\n UPDATE public.plg_financial_cash_register_sessions SET opening_movement_id = v_mov WHERE id = v_sess;\n END IF;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.cash_session.open', 'plg_financial_cash_register_sessions', v_sess::text,\n jsonb_build_object('cofre_account_id', v_cofre.id, 'unit_id', v_cofre.unit_id, 'opening_amount', v_amount, 'idempotency_key', p_idempotency_key));\n RETURN jsonb_build_object('session_id', v_sess, 'account_id', v_acc, 'opening_movement_id', v_mov, 'replayed', false);\nEND $$;\n\nCREATE FUNCTION public.financial_create_invoice(p_direction text, p_total numeric, p_counterparty_person_id uuid DEFAULT NULL::uuid, p_order_id uuid DEFAULT NULL::uuid, p_unit_id uuid DEFAULT NULL::uuid, p_installments jsonb DEFAULT NULL::jsonb, p_competence_date date DEFAULT NULL::date, p_description text DEFAULT NULL::text, p_notes text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_unit uuid := p_unit_id;\n v_order public.orders%ROWTYPE;\n v_invoice_id uuid;\n v_number text;\n v_total numeric := round(coalesce(p_total, 0), 2);\n v_insts jsonb;\n v_sum numeric := 0;\n v_i jsonb;\n v_n int := 0;\n v_out jsonb := '[]'::jsonb;\n v_inst_id uuid;\nBEGIN\n IF p_direction NOT IN ('receivable', 'payable') THEN\n RAISE EXCEPTION 'financial_create_invoice: direction must be receivable|payable' USING ERRCODE = '22023';\n END IF;\n IF v_total < 0 THEN\n RAISE EXCEPTION 'financial_create_invoice: total must be >= 0' USING ERRCODE = '22023';\n END IF;\n\n IF p_order_id IS NOT NULL THEN\n SELECT * INTO v_order FROM public.orders WHERE id = p_order_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_create_invoice: order % not found in this tenant', p_order_id USING ERRCODE = 'P0002';\n END IF;\n IF v_order.kind IN ('invoice_receivable', 'invoice_payable') THEN\n RAISE EXCEPTION 'financial_create_invoice: order % is a retired invoice kind', p_order_id USING ERRCODE = '23514';\n END IF;\n v_unit := coalesce(v_unit, v_order.unit_id);\n END IF;\n\n PERFORM public.plg_financial_require_permission('financial.create', v_unit);\n\n -- Idempotency: same key in the tenant → the same invoice, no second effect.\n IF p_idempotency_key IS NOT NULL THEN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT id, number INTO v_invoice_id, v_number FROM public.plg_financial_invoices\n WHERE tenant_id = v_tenant AND idempotency_key = p_idempotency_key;\n IF v_invoice_id IS NOT NULL THEN\n SELECT coalesce(jsonb_agg(jsonb_build_object('id', i.id, 'number', i.number, 'due_date', i.due_date, 'amount', i.amount) ORDER BY i.number), '[]'::jsonb)\n INTO v_out FROM public.plg_financial_invoice_installments i WHERE i.invoice_id = v_invoice_id;\n RETURN jsonb_build_object('invoice_id', v_invoice_id, 'number', v_number, 'installments', v_out, 'replayed', true);\n END IF;\n END IF;\n\n -- installments: given, or one for the total\n v_insts := coalesce(p_installments, jsonb_build_array(jsonb_build_object(\n 'number', 1, 'due_date', coalesce(p_competence_date, current_date), 'amount', v_total)));\n IF jsonb_typeof(v_insts) <> 'array' OR jsonb_array_length(v_insts) = 0 THEN\n RAISE EXCEPTION 'financial_create_invoice: installments must be a non-empty array' USING ERRCODE = '22023';\n END IF;\n FOR v_i IN SELECT * FROM jsonb_array_elements(v_insts) LOOP\n v_sum := v_sum + round(coalesce((v_i ->> 'amount')::numeric, 0), 2);\n END LOOP;\n IF v_sum <> v_total THEN\n RAISE EXCEPTION 'financial_create_invoice: installments sum to % but the invoice total is %', v_sum, v_total USING ERRCODE = '23514';\n END IF;\n\n v_number := public.plg_financial_next_invoice_number(v_tenant, p_direction);\n\n INSERT INTO public.plg_financial_invoices\n (tenant_id, unit_id, direction, counterparty_person_id, order_id, number, competence_date, issue_date,\n total, description, notes, metadata, idempotency_key, created_by)\n VALUES\n (v_tenant, v_unit, p_direction, coalesce(p_counterparty_person_id, v_order.party_id), p_order_id, v_number,\n coalesce(p_competence_date, current_date), current_date, v_total, p_description, p_notes,\n coalesce(p_metadata, '{}'::jsonb) || jsonb_build_object('source', 'financial_create_invoice'),\n p_idempotency_key, auth.uid())\n RETURNING id INTO v_invoice_id;\n\n FOR v_i IN SELECT * FROM jsonb_array_elements(v_insts) LOOP\n v_n := v_n + 1;\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount, metadata)\n VALUES (v_tenant, v_unit, v_invoice_id, coalesce((v_i ->> 'number')::int, v_n),\n coalesce((v_i ->> 'due_date')::date, coalesce(p_competence_date, current_date)),\n round(coalesce((v_i ->> 'amount')::numeric, 0), 2),\n -- what came attached to THIS installment (today: receiptUrl). 015 dropped\n -- it on the floor, which is the whole point of this migration.\n CASE WHEN jsonb_typeof(v_i -> 'metadata') = 'object' THEN v_i -> 'metadata' ELSE '{}'::jsonb END)\n RETURNING id INTO v_inst_id;\n v_out := v_out || jsonb_build_object('id', v_inst_id, 'number', coalesce((v_i ->> 'number')::int, v_n),\n 'due_date', coalesce((v_i ->> 'due_date')::date, coalesce(p_competence_date, current_date)),\n 'amount', round(coalesce((v_i ->> 'amount')::numeric, 0), 2));\n END LOOP;\n\n -- The Venda records that it was invoiced (same keys the legacy readers use).\n IF p_order_id IS NOT NULL THEN\n UPDATE public.orders SET\n stage = 'invoiced',\n direction = CASE WHEN p_direction = 'payable' THEN 'debit' ELSE 'credit' END,\n metadata = coalesce(metadata, '{}'::jsonb) || jsonb_build_object('invoiceNumber', v_number, 'installmentCount', v_n),\n updated_at = now()\n WHERE id = p_order_id;\n END IF;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.create', 'plg_financial_invoices', v_invoice_id::text,\n jsonb_build_object('number', v_number, 'direction', p_direction, 'total', v_total, 'order_id', p_order_id,\n 'installments', v_n, 'idempotency_key', p_idempotency_key));\n\n RETURN jsonb_build_object('invoice_id', v_invoice_id, 'number', v_number, 'installments', v_out, 'replayed', false);\nEND $$;\n\nCREATE FUNCTION public.financial_create_payable(p_description text, p_total numeric, p_due_date date DEFAULT NULL::date, p_person_id uuid DEFAULT NULL::uuid, p_unit_id uuid DEFAULT NULL::uuid, p_chart_account_id uuid DEFAULT NULL::uuid, p_cost_center_id uuid DEFAULT NULL::uuid, p_installments jsonb DEFAULT NULL::jsonb, p_competence_date date DEFAULT NULL::date, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_res jsonb;\n v_invoice uuid;\n v_item uuid;\nBEGIN\n IF coalesce(btrim(p_description), '') = '' THEN\n RAISE EXCEPTION 'financial_create_payable: a payable with no product needs a description' USING ERRCODE = '22023';\n END IF;\n\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n\n v_res := public.financial_create_invoice(\n 'payable', round(coalesce(p_total, 0), 2), p_person_id, NULL, p_unit_id,\n coalesce(p_installments, jsonb_build_array(jsonb_build_object(\n 'number', 1, 'due_date', coalesce(p_due_date, current_date), 'amount', round(coalesce(p_total, 0), 2)))),\n p_competence_date, p_description, NULL,\n jsonb_build_object('source', 'financial_create_payable'), p_idempotency_key);\n\n v_invoice := (v_res ->> 'invoice_id')::uuid;\n IF coalesce((v_res ->> 'replayed')::boolean, false) THEN RETURN v_res; END IF;\n\n INSERT INTO public.items (tenant_id, unit_id, invoice_id, description, quantity, unit_price, total, metadata)\n VALUES (v_tenant, p_unit_id, v_invoice, p_description, 1,\n round(coalesce(p_total, 0), 2), round(coalesce(p_total, 0), 2),\n jsonb_build_object('source', 'financial_create_payable'))\n RETURNING id INTO v_item;\n\n INSERT INTO public.plg_financial_invoice_items (item_id, tenant_id, unit_id, chart_account_id, cost_center_id)\n VALUES (v_item, v_tenant, p_unit_id, p_chart_account_id, p_cost_center_id);\n\n RETURN v_res || jsonb_build_object('item_id', v_item);\nEND $$;\n\nCREATE FUNCTION public.financial_credit_customer(p_person_id uuid, p_amount numeric, p_to_account_id uuid, p_method text DEFAULT 'pix'::text, p_reason text DEFAULT NULL::text, p_notes text DEFAULT NULL::text, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_person_acc uuid;\n v_res jsonb; v_credit numeric;\nBEGIN\n IF p_person_id IS NULL THEN\n RAISE EXCEPTION 'financial_credit_customer: person required' USING ERRCODE = '22023';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.people p WHERE p.id = p_person_id AND p.tenant_id = v_tenant) THEN\n RAISE EXCEPTION 'financial_credit_customer: person % not found in this tenant', p_person_id USING ERRCODE = 'P0002';\n END IF;\n v_person_acc := public.plg_financial_person_account(v_tenant, p_person_id);\n v_res := public.financial_transfer(v_person_acc, p_to_account_id, p_amount, current_date, p_method, p_notes, p_idempotency_key,\n jsonb_build_object('credit_source', 'deposit', 'reason', p_reason, 'person_id', p_person_id));\n SELECT credit INTO v_credit FROM public.v_financial_customer_credit WHERE account_id = v_person_acc;\n IF NOT coalesce((v_res ->> 'replayed')::boolean, false) THEN\n PERFORM public.plg_financial_audit(v_tenant, 'financial.credit.deposit', 'plg_financial_accounts', v_person_acc::text,\n jsonb_build_object('person_id', p_person_id, 'amount', round(p_amount, 2), 'movement_id', v_res ->> 'movement_id', 'reason', p_reason));\n END IF;\n RETURN v_res || jsonb_build_object('person_account_id', v_person_acc, 'credit', coalesce(v_credit, 0));\nEND $$;\n\nCREATE FUNCTION public.financial_invoice_from_lines(p_order_id uuid, p_lines jsonb, p_direction text DEFAULT 'receivable'::text, p_person_id uuid DEFAULT NULL::uuid, p_installments jsonb DEFAULT NULL::jsonb, p_competence_date date DEFAULT NULL::date, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_order public.orders%ROWTYPE;\n v_line jsonb;\n v_src public.items%ROWTYPE;\n v_qty numeric;\n v_total numeric := 0;\n v_res jsonb;\n v_invoice uuid;\n v_new uuid;\nBEGIN\n SELECT * INTO v_order FROM public.orders o WHERE o.id = p_order_id AND o.tenant_id = v_tenant;\n IF v_order.id IS NULL THEN\n RAISE EXCEPTION 'financial_invoice_from_lines: order % not found in this tenant', p_order_id USING ERRCODE = 'P0002';\n END IF;\n IF jsonb_typeof(p_lines) <> 'array' OR jsonb_array_length(p_lines) = 0 THEN\n RAISE EXCEPTION 'financial_invoice_from_lines: at least one line is required' USING ERRCODE = '22023';\n END IF;\n\n -- Same discipline as every other money RPC (031): two concurrent calls with\n -- the same key serialise, so the second finds the first one's invoice instead\n -- of raising a duplicate.\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n\n -- The total is the sum of what is actually being billed, computed here rather\n -- than trusted from the caller: a split bill whose parts do not add up to the\n -- order is the failure this whole shape exists to prevent.\n FOR v_line IN SELECT * FROM jsonb_array_elements(p_lines) LOOP\n SELECT * INTO v_src FROM public.items oi\n WHERE oi.id = (v_line ->> 'order_item_id')::uuid AND oi.order_id = p_order_id;\n IF v_src.id IS NULL THEN\n RAISE EXCEPTION 'financial_invoice_from_lines: line % is not a line of order %', v_line ->> 'order_item_id', p_order_id\n USING ERRCODE = '23503';\n END IF;\n v_qty := coalesce((v_line ->> 'quantity')::numeric, v_src.quantity);\n v_total := v_total + round(v_qty * coalesce(v_src.unit_price, 0) - coalesce(v_src.discount, 0) * (v_qty / nullif(v_src.quantity, 0)), 2);\n END LOOP;\n\n v_res := public.financial_create_invoice(\n p_direction, round(v_total, 2), coalesce(p_person_id, v_order.party_id), p_order_id, v_order.unit_id,\n p_installments, p_competence_date, NULL, NULL,\n jsonb_build_object('source', 'financial_invoice_from_lines'), p_idempotency_key);\n v_invoice := (v_res ->> 'invoice_id')::uuid;\n\n IF coalesce((v_res ->> 'replayed')::boolean, false) THEN RETURN v_res; END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(p_lines) LOOP\n SELECT * INTO v_src FROM public.items oi WHERE oi.id = (v_line ->> 'order_item_id')::uuid AND oi.order_id = p_order_id;\n v_qty := coalesce((v_line ->> 'quantity')::numeric, v_src.quantity);\n\n INSERT INTO public.items (tenant_id, unit_id, invoice_id, product_id, variant_id,\n name, description, quantity, unit_price, discount, total, sort_order, metadata)\n VALUES (v_src.tenant_id, v_src.unit_id, v_invoice, v_src.product_id, v_src.variant_id,\n v_src.name, v_src.description, v_qty, coalesce(v_src.unit_price, 0), 0,\n round(v_qty * coalesce(v_src.unit_price, 0), 2), coalesce(v_src.sort_order, 0),\n jsonb_build_object('billed_from', v_src.id))\n RETURNING id INTO v_new;\n\n INSERT INTO public.item_associations (tenant_id, from_item_id, to_item_id, quantity)\n VALUES (v_src.tenant_id, v_src.id, v_new, v_qty);\n\n IF (v_line ? 'chart_account_id') OR (v_line ? 'cost_center_id') OR (v_line ? 'cfop') THEN\n INSERT INTO public.plg_financial_invoice_items (item_id, tenant_id, unit_id, chart_account_id, cost_center_id, cfop)\n VALUES (v_new, v_src.tenant_id, v_src.unit_id,\n nullif(v_line ->> 'chart_account_id', '')::uuid,\n nullif(v_line ->> 'cost_center_id', '')::uuid,\n nullif(v_line ->> 'cfop', ''));\n END IF;\n END LOOP;\n\n RETURN v_res || jsonb_build_object('lines', jsonb_array_length(p_lines));\nEND $$;\n\nCREATE FUNCTION public.financial_receive_payment(p_installment_id uuid, p_amount numeric, p_account_id uuid DEFAULT NULL::uuid, p_method text DEFAULT NULL::text, p_cash_session_id uuid DEFAULT NULL::uuid, p_card_brand text DEFAULT NULL::text, p_card_installments integer DEFAULT NULL::integer, p_nsu text DEFAULT NULL::text, p_payment_date date DEFAULT NULL::date, p_fee_amount numeric DEFAULT NULL::numeric, p_notes text DEFAULT NULL::text, p_idempotency_key text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_actor uuid := auth.uid();\n v_inst_id uuid := p_installment_id;\n v_inst record;\n v_invoice public.plg_financial_invoices%ROWTYPE;\n v_amount numeric := round(coalesce(p_amount, 0), 2);\n v_open numeric;\n v_method text := lower(coalesce(nullif(btrim(p_method), ''), 'other'));\n v_holder uuid;\n v_holder_k text;\n v_session uuid := p_cash_session_id;\n v_counter uuid;\n v_credit numeric;\n v_mov_id uuid;\n v_alloc_id uuid;\n v_dir text;\nBEGIN\n IF v_amount <= 0 THEN\n RAISE EXCEPTION 'financial_receive_payment: amount must be > 0' USING ERRCODE = '22023';\n END IF;\n IF v_method NOT IN ('cash', 'pix', 'credit_card', 'debit_card', 'transfer', 'boleto', 'check', 'credit', 'other') THEN\n RAISE EXCEPTION 'financial_receive_payment: unknown method %', v_method USING ERRCODE = '22023';\n END IF;\n\n -- accept a legacy per-order 'bill' movement id in place of the installment\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_invoice_installments i WHERE i.id = v_inst_id) THEN\n SELECT m.financial_invoice_installment_id INTO v_inst_id FROM public.plg_financial_movements m\n WHERE m.id = p_installment_id AND m.movement_kind = 'bill' AND m.tenant_id = v_tenant;\n END IF;\n SELECT i.* INTO v_inst FROM public.plg_financial_invoice_installments i WHERE i.id = v_inst_id AND i.tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_receive_payment: installment % not found', p_installment_id USING ERRCODE = 'P0002';\n END IF;\n SELECT * INTO v_invoice FROM public.plg_financial_invoices WHERE id = v_inst.invoice_id;\n PERFORM public.plg_financial_require_permission('financial.create', v_inst.unit_id);\n\n -- idempotency\n IF p_idempotency_key IS NOT NULL THEN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT m.id INTO v_mov_id FROM public.plg_financial_movements m WHERE m.tenant_id = v_tenant AND m.idempotency_key = p_idempotency_key;\n IF v_mov_id IS NOT NULL THEN\n SELECT a.id INTO v_alloc_id FROM public.plg_financial_allocations a WHERE a.payment_movement_id = v_mov_id LIMIT 1;\n RETURN jsonb_build_object('movement_id', v_mov_id, 'allocation_id', v_alloc_id, 'installment_id', v_inst.id,\n 'invoice_id', v_inst.invoice_id, 'amount', v_amount, 'replayed', true);\n END IF;\n END IF;\n\n IF v_inst.cancelled_at IS NOT NULL OR v_invoice.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: installment is cancelled' USING ERRCODE = '55000';\n END IF;\n v_open := round(v_inst.amount - public.plg_financial_installment_paid(v_inst.id), 2);\n IF v_amount > v_open + 0.000001 THEN\n RAISE EXCEPTION 'financial_receive_payment: % exceeds the open balance % of installment %/% of %', v_amount, v_open, v_inst.number, v_invoice.number, v_invoice.number\n USING ERRCODE = '23514';\n END IF;\n\n -- the money holder\n IF v_method = 'credit' THEN\n IF v_invoice.counterparty_person_id IS NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: the invoice has no counterparty to consume credit from' USING ERRCODE = '55000';\n END IF;\n v_holder := public.plg_financial_person_account(v_tenant, v_invoice.counterparty_person_id);\n SELECT balance INTO v_credit FROM public.v_financial_account_balances WHERE account_id = v_holder;\n IF coalesce(v_credit, 0) + 0.000001 < v_amount THEN\n RAISE EXCEPTION 'financial_receive_payment: customer credit % is below %', coalesce(v_credit, 0), v_amount USING ERRCODE = '23514';\n END IF;\n ELSIF v_method = 'cash' THEN\n IF v_session IS NULL THEN\n v_session := public.plg_financial_open_session_of(v_tenant, v_actor, v_inst.unit_id);\n END IF;\n IF v_session IS NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: no open cash session (caixinha) for cash in this unit' USING ERRCODE = '55000',\n HINT = 'Open your cash session first (financial_cash_session_open).';\n END IF;\n SELECT s.account_id INTO v_holder FROM public.plg_financial_cash_register_sessions s\n WHERE s.id = v_session AND s.tenant_id = v_tenant AND s.status = 'open';\n IF v_holder IS NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: cash session % is not open', v_session USING ERRCODE = '55000';\n END IF;\n ELSE\n v_holder := p_account_id;\n IF v_holder IS NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: an account is required for method %', v_method USING ERRCODE = '22023';\n END IF;\n END IF;\n SELECT a.kind INTO v_holder_k FROM public.plg_financial_accounts a WHERE a.id = v_holder AND a.tenant_id = v_tenant AND a.is_active;\n IF v_holder_k IS NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: account % not found or inactive', v_holder USING ERRCODE = 'P0002';\n END IF;\n IF (v_method = 'credit_card' AND v_holder_k <> 'card_receivable_credit')\n OR (v_method = 'debit_card' AND v_holder_k <> 'card_receivable_debit') THEN\n RAISE EXCEPTION 'financial_receive_payment: method % must settle in a % account', v_method,\n CASE WHEN v_method = 'credit_card' THEN 'card_receivable_credit' ELSE 'card_receivable_debit' END USING ERRCODE = '23514';\n END IF;\n IF v_method NOT IN ('credit_card', 'debit_card') AND v_holder_k IN ('card_receivable_credit', 'card_receivable_debit') THEN\n RAISE EXCEPTION 'financial_receive_payment: a card receivable account only takes card methods' USING ERRCODE = '23514';\n END IF;\n\n IF v_invoice.direction = 'payable' THEN\n v_dir := 'debit';\n v_counter := public.plg_financial_system_account(v_tenant, 'payables');\n ELSE\n v_dir := 'credit';\n v_counter := public.plg_financial_system_account(v_tenant, 'receivables');\n END IF;\n\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, invoice_id, financial_invoice_id, financial_invoice_installment_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, method, card_brand, card_installments, nsu, cash_session_id,\n bank_account_id, fee_amount, notes, metadata, idempotency_key, created_by, debit_account_id, credit_account_id)\n VALUES\n (v_tenant, v_inst.unit_id, v_invoice.order_id, v_invoice.id, v_inst.id, v_dir, 'payment', v_amount, v_amount, 'paid',\n v_inst.due_date, coalesce(p_payment_date, current_date), v_inst.number, v_method, p_card_brand, p_card_installments, p_nsu, v_session,\n CASE WHEN v_holder_k IN ('person', 'system') THEN NULL ELSE v_holder END,\n round(coalesce(p_fee_amount, 0), 2), p_notes,\n coalesce(p_metadata, '{}'::jsonb) || jsonb_build_object('source', 'financial_receive_payment'),\n p_idempotency_key, v_actor,\n CASE WHEN v_dir = 'credit' THEN v_holder ELSE v_counter END,\n CASE WHEN v_dir = 'credit' THEN v_counter ELSE v_holder END)\n RETURNING id INTO v_mov_id;\n\n INSERT INTO public.plg_financial_allocations (tenant_id, unit_id, payment_movement_id, installment_id, amount, created_by)\n VALUES (v_tenant, v_inst.unit_id, v_mov_id, v_inst.id, v_amount, v_actor)\n RETURNING id INTO v_alloc_id;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.payment.receive', 'plg_financial_movements', v_mov_id::text,\n jsonb_build_object('installment_id', v_inst.id, 'invoice_id', v_invoice.id, 'invoice_number', v_invoice.number, 'amount', v_amount,\n 'method', v_method, 'account_id', v_holder, 'cash_session_id', v_session, 'idempotency_key', p_idempotency_key));\n\n RETURN jsonb_build_object('movement_id', v_mov_id, 'allocation_id', v_alloc_id, 'installment_id', v_inst.id,\n 'invoice_id', v_inst.invoice_id, 'amount', v_amount, 'replayed', false);\nEND $$;\n\nCREATE FUNCTION public.financial_retire_bill_mirror(p_execute boolean DEFAULT false) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_writers text[];\n v_rows bigint;\n v_nonmirror bigint;\n v_deleted bigint := 0;\nBEGIN\n SELECT coalesce(array_agg(writer ORDER BY writer), '{}') INTO v_writers FROM public.v_bill_mirror_writers;\n SELECT count(*) INTO v_rows FROM public.plg_financial_movements WHERE movement_kind = 'bill';\n -- A \\`bill\\` row that is NOT a mirror would be real data, and there is none:\n -- 018 stamped every one it wrote. Counting them is what makes the DELETE safe\n -- rather than merely intended.\n SELECT count(*) INTO v_nonmirror FROM public.plg_financial_movements\n WHERE movement_kind = 'bill' AND coalesce((metadata ->> 'legacy_mirror')::boolean, false) IS NOT true;\n\n IF array_length(v_writers, 1) > 0 THEN\n RETURN jsonb_build_object('dropped', false, 'reason', 'writers remain', 'writers', to_jsonb(v_writers), 'rows', v_rows);\n END IF;\n IF v_nonmirror > 0 THEN\n RETURN jsonb_build_object('dropped', false, 'reason', 'some bill rows are not legacy mirrors', 'non_mirror_rows', v_nonmirror, 'rows', v_rows);\n END IF;\n IF NOT p_execute THEN\n RETURN jsonb_build_object('dropped', false, 'reason', 'dry run', 'rows', v_rows, 'would_delete', v_rows);\n END IF;\n\n DELETE FROM public.plg_financial_movements WHERE movement_kind = 'bill';\n GET DIAGNOSTICS v_deleted = ROW_COUNT;\n RETURN jsonb_build_object('dropped', true, 'rows_deleted', v_deleted);\nEND $$;\n\nCREATE FUNCTION public.financial_reverse_movement(p_movement_id uuid, p_reason text, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_m public.plg_financial_movements%ROWTYPE;\n v_rev uuid;\nBEGIN\n IF nullif(btrim(coalesce(p_reason, '')), '') IS NULL THEN\n RAISE EXCEPTION 'financial_reverse_movement: a reason is required' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_m FROM public.plg_financial_movements WHERE id = p_movement_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_reverse_movement: movement % not found', p_movement_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_m.unit_id);\n IF v_m.movement_kind NOT IN ('payment', 'transfer') OR v_m.debit_account_id IS NULL THEN\n RAISE EXCEPTION 'financial_reverse_movement: % is not a money event', p_movement_id USING ERRCODE = '55000';\n END IF;\n IF v_m.reversed_by_id IS NOT NULL THEN\n RETURN jsonb_build_object('reversal_id', v_m.reversed_by_id, 'movement_id', v_m.id, 'replayed', true);\n END IF;\n IF p_idempotency_key IS NOT NULL THEN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT m.id INTO v_rev FROM public.plg_financial_movements m WHERE m.tenant_id = v_tenant AND m.idempotency_key = p_idempotency_key;\n IF v_rev IS NOT NULL THEN\n RETURN jsonb_build_object('reversal_id', v_rev, 'movement_id', v_m.id, 'replayed', true);\n END IF;\n END IF;\n\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, invoice_id, financial_invoice_id, financial_invoice_installment_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, method, cash_session_id, bank_account_id, reason, notes, metadata,\n idempotency_key, created_by, reverses_id, debit_account_id, credit_account_id)\n VALUES\n (v_m.tenant_id, v_m.unit_id, v_m.invoice_id, v_m.financial_invoice_id, v_m.financial_invoice_installment_id,\n CASE WHEN v_m.direction = 'credit' THEN 'debit' ELSE 'credit' END, v_m.movement_kind, v_m.amount, v_m.amount, 'paid',\n current_date, current_date, v_m.installment_number, v_m.method, v_m.cash_session_id, v_m.bank_account_id, btrim(p_reason),\n 'Estorno de ' || v_m.id::text,\n jsonb_build_object('source', 'financial_reverse_movement', 'reversal_of', v_m.id, 'reason', btrim(p_reason)),\n p_idempotency_key, auth.uid(), v_m.id, v_m.credit_account_id, v_m.debit_account_id)\n RETURNING id INTO v_rev;\n\n UPDATE public.plg_financial_movements SET reversed_by_id = v_rev, reason = coalesce(reason, btrim(p_reason)), updated_at = now()\n WHERE id = v_m.id;\n IF v_m.financial_invoice_installment_id IS NOT NULL THEN PERFORM public.plg_financial_sync_mirror(v_m.financial_invoice_installment_id); END IF;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.movement.reverse', 'plg_financial_movements', v_m.id::text,\n jsonb_build_object('reversal_id', v_rev, 'amount', v_m.amount, 'reason', btrim(p_reason), 'idempotency_key', p_idempotency_key));\n RETURN jsonb_build_object('reversal_id', v_rev, 'movement_id', v_m.id, 'replayed', false);\nEND $$;\n\nCREATE FUNCTION public.financial_set_opening_balance(p_account_id uuid, p_amount numeric, p_date date DEFAULT NULL::date, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_acc record; v_existing record; v_amount numeric := round(coalesce(p_amount, 0), 2); v_mov uuid; v_open uuid;\nBEGIN\n SELECT a.id, a.unit_id, a.kind INTO v_acc FROM public.plg_financial_accounts a WHERE a.id = p_account_id AND a.tenant_id = v_tenant;\n IF v_acc.id IS NULL THEN\n RAISE EXCEPTION 'financial_set_opening_balance: account % not found', p_account_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.create', v_acc.unit_id);\n -- idempotent by ACCOUNT here rather than by key (an account has one opening\n -- balance), which is right — and still racing without the claim.\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT m.id, m.amount, m.debit_account_id INTO v_existing FROM public.plg_financial_movements m\n WHERE m.tenant_id = v_tenant AND m.metadata ->> 'opening_balance_for' = p_account_id::text;\n IF v_existing.id IS NOT NULL THEN\n IF (CASE WHEN v_existing.debit_account_id = p_account_id THEN v_existing.amount ELSE -v_existing.amount END) = v_amount THEN\n RETURN jsonb_build_object('movement_id', v_existing.id, 'amount', v_amount, 'replayed', true);\n END IF;\n RAISE EXCEPTION 'financial_set_opening_balance: account % already has an opening balance; post an adjustment transfer' , p_account_id USING ERRCODE = '55000';\n END IF;\n IF v_amount = 0 THEN\n RETURN jsonb_build_object('movement_id', NULL, 'amount', 0, 'replayed', false);\n END IF;\n v_open := public.plg_financial_system_account(v_tenant, 'opening_balances');\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, direction, movement_kind, amount, paid_amount, status, due_date, payment_date, notes, metadata,\n idempotency_key, created_by, debit_account_id, credit_account_id)\n VALUES\n (v_tenant, v_acc.unit_id, CASE WHEN v_amount > 0 THEN 'credit' ELSE 'debit' END, 'transfer', abs(v_amount), abs(v_amount), 'paid',\n coalesce(p_date, current_date), coalesce(p_date, current_date), 'Saldo de abertura',\n jsonb_build_object('opening_balance_for', p_account_id, 'source', 'financial_set_opening_balance'),\n p_idempotency_key, auth.uid(),\n CASE WHEN v_amount > 0 THEN p_account_id ELSE v_open END,\n CASE WHEN v_amount > 0 THEN v_open ELSE p_account_id END)\n RETURNING id INTO v_mov;\n PERFORM public.plg_financial_audit(v_tenant, 'financial.account.opening_balance', 'plg_financial_accounts', p_account_id::text,\n jsonb_build_object('amount', v_amount, 'movement_id', v_mov));\n RETURN jsonb_build_object('movement_id', v_mov, 'amount', v_amount, 'replayed', false);\nEND $$;\n\nCREATE FUNCTION public.financial_transfer(p_from_account_id uuid, p_to_account_id uuid, p_amount numeric, p_date date DEFAULT NULL::date, p_method text DEFAULT NULL::text, p_notes text DEFAULT NULL::text, p_idempotency_key text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_from record; v_to record;\n v_amount numeric := round(coalesce(p_amount, 0), 2);\n v_mov_id uuid;\nBEGIN\n IF v_amount <= 0 THEN\n RAISE EXCEPTION 'financial_transfer: amount must be > 0' USING ERRCODE = '22023';\n END IF;\n IF p_from_account_id IS NULL OR p_to_account_id IS NULL OR p_from_account_id = p_to_account_id THEN\n RAISE EXCEPTION 'financial_transfer: source and destination must be two different accounts' USING ERRCODE = '22023';\n END IF;\n SELECT a.id, a.unit_id, a.kind INTO v_from FROM public.plg_financial_accounts a WHERE a.id = p_from_account_id AND a.tenant_id = v_tenant AND a.is_active;\n SELECT a.id, a.unit_id, a.kind INTO v_to FROM public.plg_financial_accounts a WHERE a.id = p_to_account_id AND a.tenant_id = v_tenant AND a.is_active;\n IF v_from.id IS NULL OR v_to.id IS NULL THEN\n RAISE EXCEPTION 'financial_transfer: account not found or inactive' USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.create', coalesce(v_to.unit_id, v_from.unit_id));\n IF v_from.unit_id IS NOT NULL AND v_to.unit_id IS NOT NULL AND v_from.unit_id <> v_to.unit_id THEN\n PERFORM public.plg_financial_require_permission('financial.create', v_from.unit_id);\n END IF;\n\n IF p_idempotency_key IS NOT NULL THEN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT m.id INTO v_mov_id FROM public.plg_financial_movements m WHERE m.tenant_id = v_tenant AND m.idempotency_key = p_idempotency_key;\n IF v_mov_id IS NOT NULL THEN\n RETURN jsonb_build_object('movement_id', v_mov_id, 'amount', v_amount, 'replayed', true);\n END IF;\n END IF;\n\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, direction, movement_kind, amount, paid_amount, status, due_date, payment_date, method,\n bank_account_id, notes, metadata, idempotency_key, created_by, debit_account_id, credit_account_id)\n VALUES\n (v_tenant, coalesce(v_to.unit_id, v_from.unit_id),\n CASE WHEN v_to.kind IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit', 'card_receivable_debit') THEN 'credit' ELSE 'debit' END,\n 'transfer', v_amount, v_amount, 'paid', coalesce(p_date, current_date), coalesce(p_date, current_date), lower(nullif(btrim(p_method), '')),\n CASE WHEN v_to.kind IN ('person', 'system') THEN CASE WHEN v_from.kind IN ('person', 'system') THEN NULL ELSE v_from.id END ELSE v_to.id END,\n p_notes, coalesce(p_metadata, '{}'::jsonb) || jsonb_build_object('source', 'financial_transfer', 'from_account_id', v_from.id, 'to_account_id', v_to.id),\n p_idempotency_key, auth.uid(), v_to.id, v_from.id)\n RETURNING id INTO v_mov_id;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.transfer', 'plg_financial_movements', v_mov_id::text,\n jsonb_build_object('from_account_id', v_from.id, 'to_account_id', v_to.id, 'amount', v_amount, 'idempotency_key', p_idempotency_key));\n RETURN jsonb_build_object('movement_id', v_mov_id, 'amount', v_amount, 'replayed', false);\nEND $$;\n\nCREATE FUNCTION public.fn_invoice_from_order(p_order_id uuid, p_due_date date DEFAULT NULL::date, p_status text DEFAULT NULL::text, p_installments integer DEFAULT 1, p_direction text DEFAULT 'credit'::text) RETURNS uuid\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_tenant uuid; v_unit uuid;\nBEGIN\n SELECT tenant_id, unit_id INTO v_tenant, v_unit FROM public.orders WHERE id = p_order_id;\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'fn_invoice_from_order: order % not found', p_order_id;\n END IF;\n IF NOT (v_tenant = app.current_tenant_id() AND app.has_permission('financial.create', v_unit)) THEN\n RAISE EXCEPTION 'fn_invoice_from_order: forbidden' USING ERRCODE = '42501';\n END IF;\n\n RETURN public.fn_invoice_from_order_internal(\n p_order_id, p_due_date, p_status, p_installments, p_direction);\nEND;\n$$;\n\nCREATE FUNCTION public.fn_invoice_from_order_internal(p_order_id uuid, p_due_date date DEFAULT NULL::date, p_status text DEFAULT NULL::text, p_installments integer DEFAULT 1, p_direction text DEFAULT 'credit'::text) RETURNS uuid\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_order public.orders%ROWTYPE;\n v_invoice_id uuid;\n v_dir text;\n v_number text;\n v_summary text;\n v_contact text;\n v_n int := GREATEST(COALESCE(p_installments, 1), 1);\n v_total numeric;\n v_base numeric;\n v_amt numeric;\n v_alloc numeric := 0;\n v_due date;\n i int;\nBEGIN\n SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;\n IF NOT FOUND THEN RAISE EXCEPTION 'fn_invoice_from_order: order % not found', p_order_id; END IF;\n\n SELECT id INTO v_invoice_id FROM public.plg_financial_invoices\n WHERE order_id = p_order_id AND cancelled_at IS NULL LIMIT 1;\n IF v_invoice_id IS NOT NULL THEN RETURN p_order_id; END IF;\n -- A pool promoted before 016 carries legacy bill rows and no invoice; it is\n -- already invoiced and must not be invoiced twice.\n IF EXISTS (SELECT 1 FROM public.plg_financial_movements\n WHERE invoice_id = p_order_id AND movement_kind = 'bill'\n AND status <> 'cancelled') THEN\n RETURN p_order_id;\n END IF;\n\n v_dir := CASE WHEN p_direction = 'debit' THEN 'payable' ELSE 'receivable' END;\n v_number := public.plg_financial_next_invoice_number(v_order.tenant_id, v_dir);\n\n SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary\n FROM public.items WHERE order_id = p_order_id;\n SELECT name INTO v_contact FROM public.people WHERE id = v_order.party_id;\n\n UPDATE public.orders SET\n status = COALESCE(p_status, status),\n stage = 'invoiced',\n direction = p_direction,\n reference_number = v_number,\n metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object(\n 'itemsSummary', COALESCE(v_summary, ''),\n 'contactName', v_contact,\n 'installmentCount', v_n,\n 'quoteNumber', v_order.reference_number,\n 'direction', p_direction,\n 'invoiceNumber', v_number\n ),\n updated_at = now()\n WHERE id = p_order_id;\n\n v_total := round(COALESCE(v_order.total, 0), 2);\n v_due := COALESCE(p_due_date, current_date);\n\n INSERT INTO public.plg_financial_invoices\n (tenant_id, unit_id, direction, counterparty_person_id, order_id, number, competence_date, issue_date,\n total, description, metadata, created_by)\n VALUES\n (v_order.tenant_id, v_order.unit_id, v_dir, v_order.party_id, p_order_id, v_number,\n COALESCE(v_order.created_at::date, current_date), current_date,\n v_total, NULLIF(v_summary, ''),\n jsonb_build_object('source', 'fn_invoice_from_order', 'orderKind', v_order.kind),\n auth.uid())\n RETURNING id INTO v_invoice_id;\n\n v_base := round(v_total / v_n, 2);\n FOR i IN 1..v_n LOOP\n IF i = v_n THEN v_amt := round(v_total - v_alloc, 2);\n ELSE v_amt := v_base; v_alloc := v_alloc + v_base; END IF;\n\n INSERT INTO public.plg_financial_invoice_installments (invoice_id, number, due_date, amount)\n VALUES (v_invoice_id, i, (v_due + ((i - 1) || ' months')::interval)::date, v_amt);\n -- No mirror. v_financial_bills gives the legacy shape to whoever still\n -- wants it, derived from exactly this row.\n END LOOP;\n\n RETURN p_order_id;\nEND;\n$$;\n\nCREATE FUNCTION public.fn_order_to_cash(p_kind text, p_lines jsonb, p_party_id uuid DEFAULT NULL::uuid, p_unit_id uuid DEFAULT NULL::uuid, p_status text DEFAULT NULL::text, p_channel text DEFAULT NULL::text, p_assignee_id uuid DEFAULT NULL::uuid, p_installments integer DEFAULT 1, p_due_date date DEFAULT NULL::date, p_direction text DEFAULT 'credit'::text, p_metadata jsonb DEFAULT '{}'::jsonb, p_appointment_id uuid DEFAULT NULL::uuid, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_order uuid;\n v_invoice uuid;\nBEGIN\n PERFORM public.plg_financial_require_permission('financial.create', p_unit_id);\n\n -- One transaction. This is the whole point of the function existing: the two\n -- statements the browser used to make separately cannot half-happen here.\n v_order := public.order_create(\n p_kind => p_kind,\n p_lines => p_lines,\n p_party_id => p_party_id,\n p_unit_id => p_unit_id,\n p_status => p_status,\n p_channel => p_channel,\n p_assignee_id => p_assignee_id,\n p_metadata => coalesce(p_metadata, '{}'::jsonb) || jsonb_build_object('source', 'fn_order_to_cash'),\n p_idempotency_key => p_idempotency_key);\n\n PERFORM public.fn_invoice_from_order_internal(v_order, p_due_date, p_status, greatest(coalesce(p_installments, 1), 1), p_direction);\n\n SELECT f.id INTO v_invoice FROM public.plg_financial_invoices f\n WHERE f.order_id = v_order AND f.cancelled_at IS NULL\n ORDER BY f.created_at DESC LIMIT 1;\n\n -- An appointment that bills carries its Venda. Filling it here rather than in\n -- a second browser call is what stops the 5% from staying 5%.\n IF p_appointment_id IS NOT NULL THEN\n UPDATE public.appointments SET order_id = v_order\n WHERE id = p_appointment_id AND tenant_id = v_tenant AND order_id IS NULL;\n END IF;\n\n RETURN jsonb_build_object('order_id', v_order, 'invoice_id', v_invoice);\nEND $$;\n\nCREATE FUNCTION public.next_sequence_internal(p_tenant_id uuid, p_kind text) RETURNS bigint\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_next bigint;\nBEGIN\n INSERT INTO public.sequences (tenant_id, kind, current_value)\n VALUES (p_tenant_id, p_kind, 1)\n ON CONFLICT (tenant_id, kind)\n DO UPDATE SET current_value = public.sequences.current_value + 1\n RETURNING current_value INTO v_next;\n RETURN v_next;\nEND $$;\n\nCREATE FUNCTION public.plg_financial_account_kind_of(p_account_type text) RETURNS text\n LANGUAGE sql IMMUTABLE\n AS $$\n SELECT CASE lower(coalesce(p_account_type, ''))\n WHEN 'cash_register' THEN 'cash_physical'\n WHEN 'caixa_fisico' THEN 'cash_physical'\n WHEN 'cash' THEN 'cash_physical'\n WHEN 'credit_card' THEN 'card_payable'\n WHEN 'cartao_credito_pagamento' THEN 'card_payable'\n WHEN 'cartao_credito_recebimento' THEN 'card_receivable_credit'\n WHEN 'card_receivable_credit' THEN 'card_receivable_credit'\n WHEN 'cartao_debito_recebimento' THEN 'card_receivable_debit'\n WHEN 'card_receivable_debit' THEN 'card_receivable_debit'\n ELSE 'bank'\n END\n$$;\n\nCREATE FUNCTION public.plg_financial_audit(p_tenant uuid, p_action text, p_entity_type text, p_entity_id text, p_metadata jsonb DEFAULT '{}'::jsonb) RETURNS void\n LANGUAGE sql SECURITY DEFINER\n SET search_path TO ''\n AS $$\n INSERT INTO public.audit_logs (tenant_id, user_id, action, entity_type, entity_id, metadata, reason)\n VALUES (p_tenant, auth.uid(), p_action, p_entity_type, p_entity_id, coalesce(p_metadata, '{}'::jsonb),\n -- the destructive-action gate (086) reads the COLUMN. Callers have\n -- always put the reason in the blob; lift it out rather than making\n -- every RPC learn a new argument.\n nullif(btrim(coalesce(p_metadata ->> 'reason', '')), ''));\n$$;\n\nCREATE FUNCTION public.plg_financial_backfill_ledger() RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n r record; v_legs record; v_open numeric; v_alloc numeric; v_paid numeric; v_id uuid;\n v_legs_n int := 0; v_alloc_n int := 0; v_synth_n int := 0; v_open_n int := 0;\nBEGIN\n -- 1. legs on every legless payment/transfer; mirrors marked\n FOR r IN\n SELECT m.* FROM public.plg_financial_movements m\n WHERE m.debit_account_id IS NULL AND m.credit_account_id IS NULL\n AND NOT (m.metadata ? 'legacy_mirror')\n ORDER BY m.created_at, m.id\n LOOP\n SELECT * INTO v_legs FROM public.plg_financial_legacy_legs(\n r.tenant_id, r.movement_kind, r.direction, r.bank_account_id, r.cash_session_id, r.financial_invoice_id, r.metadata);\n IF v_legs.o_mirror THEN\n UPDATE public.plg_financial_movements SET metadata = coalesce(metadata, '{}'::jsonb) || jsonb_build_object('legacy_mirror', true) WHERE id = r.id;\n ELSE\n UPDATE public.plg_financial_movements\n SET debit_account_id = v_legs.o_debit, credit_account_id = v_legs.o_credit,\n metadata = coalesce(metadata, '{}'::jsonb) || jsonb_build_object('legacy_shape', true, 'legs_backfilled', true)\n WHERE id = r.id;\n v_legs_n := v_legs_n + 1;\n END IF;\n END LOOP;\n\n -- 2. allocations from the links 016 made\n FOR r IN\n SELECT m.* FROM public.plg_financial_movements m\n WHERE m.movement_kind = 'payment' AND m.status <> 'cancelled' AND m.financial_invoice_installment_id IS NOT NULL\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations a WHERE a.payment_movement_id = m.id)\n ORDER BY m.payment_date NULLS LAST, m.created_at, m.id\n LOOP\n SELECT i.amount - public.plg_financial_installment_paid(i.id) INTO v_open\n FROM public.plg_financial_invoice_installments i WHERE i.id = r.financial_invoice_installment_id AND i.cancelled_at IS NULL;\n v_alloc := LEAST(coalesce(v_open, 0), r.amount);\n IF v_alloc > 0 THEN\n INSERT INTO public.plg_financial_allocations (tenant_id, unit_id, payment_movement_id, installment_id, amount, metadata)\n VALUES (r.tenant_id, r.unit_id, r.id, r.financial_invoice_installment_id, v_alloc, jsonb_build_object('backfilled', true))\n ON CONFLICT (payment_movement_id, installment_id) DO NOTHING;\n v_alloc_n := v_alloc_n + 1;\n END IF;\n END LOOP;\n\n -- 3. R14 — a bill that says paid without the receipt: the synthetic \"Crédito Legado\"\n FOR r IN\n SELECT b.*, i.amount AS inst_amount, t.direction AS invoice_dir\n FROM public.plg_financial_movements b\n JOIN public.plg_financial_invoice_installments i ON i.id = b.financial_invoice_installment_id\n JOIN public.plg_financial_invoices t ON t.id = i.invoice_id\n WHERE b.movement_kind = 'bill' AND b.status <> 'cancelled' AND i.cancelled_at IS NULL\n AND coalesce(b.paid_amount, 0) > public.plg_financial_installment_paid(i.id) + 0.005\n AND (b.metadata ? 'legacy_mirror')\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_movements s WHERE s.metadata ->> 'legacy_credit_for_bill' = b.id::text)\n ORDER BY b.created_at, b.id\n LOOP\n v_paid := public.plg_financial_installment_paid(r.financial_invoice_installment_id);\n v_alloc := LEAST(round(coalesce(r.paid_amount, 0) - v_paid, 2), round(r.inst_amount - v_paid, 2));\n CONTINUE WHEN v_alloc <= 0;\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, invoice_id, financial_invoice_id, financial_invoice_installment_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, notes, metadata,\n debit_account_id, credit_account_id)\n VALUES\n (r.tenant_id, r.unit_id, r.invoice_id, r.financial_invoice_id, r.financial_invoice_installment_id,\n CASE WHEN r.invoice_dir = 'payable' THEN 'debit' ELSE 'credit' END, 'payment', v_alloc, v_alloc, 'paid',\n r.due_date, coalesce(r.payment_date, r.updated_at::date, current_date), r.installment_number,\n 'Crédito Legado', jsonb_build_object('synthetic', 'legacy_credit', 'legacy_credit_for_bill', r.id, 'source', 'plg_financial_backfill_ledger'),\n CASE WHEN r.invoice_dir = 'payable' THEN public.plg_financial_system_account(r.tenant_id, 'payables')\n ELSE public.plg_financial_system_account(r.tenant_id, 'legacy_credit') END,\n CASE WHEN r.invoice_dir = 'payable' THEN public.plg_financial_system_account(r.tenant_id, 'legacy_credit')\n ELSE public.plg_financial_system_account(r.tenant_id, 'receivables') END)\n RETURNING id INTO v_id;\n INSERT INTO public.plg_financial_allocations (tenant_id, unit_id, payment_movement_id, installment_id, amount, metadata)\n VALUES (r.tenant_id, r.unit_id, v_id, r.financial_invoice_installment_id, v_alloc, jsonb_build_object('synthetic', 'legacy_credit'))\n ON CONFLICT DO NOTHING;\n v_synth_n := v_synth_n + 1;\n END LOOP;\n\n -- 4. legacy initial balances become opening movements\n FOR r IN\n SELECT a.* FROM public.plg_financial_accounts a\n WHERE coalesce((a.metadata ->> 'legacy_initial_balance')::numeric, 0) <> 0\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_movements m WHERE m.metadata ->> 'opening_balance_for' = a.id::text)\n LOOP\n v_alloc := (r.metadata ->> 'legacy_initial_balance')::numeric;\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, direction, movement_kind, amount, paid_amount, status, due_date, payment_date, notes, metadata,\n debit_account_id, credit_account_id)\n VALUES\n (r.tenant_id, r.unit_id, CASE WHEN v_alloc > 0 THEN 'credit' ELSE 'debit' END, 'transfer', abs(round(v_alloc, 2)), abs(round(v_alloc, 2)), 'paid',\n r.created_at::date, r.created_at::date, 'Saldo de abertura (migrado)',\n jsonb_build_object('opening_balance_for', r.id, 'source', 'plg_financial_backfill_ledger'),\n CASE WHEN v_alloc > 0 THEN r.id ELSE public.plg_financial_system_account(r.tenant_id, 'opening_balances') END,\n CASE WHEN v_alloc > 0 THEN public.plg_financial_system_account(r.tenant_id, 'opening_balances') ELSE r.id END);\n v_open_n := v_open_n + 1;\n END LOOP;\n\n RETURN jsonb_build_object('legs', v_legs_n, 'allocations', v_alloc_n, 'legacy_credit_receipts', v_synth_n, 'opening_balances', v_open_n);\nEND $$;\n\nCREATE FUNCTION public.plg_financial_claim_idempotency(p_tenant uuid, p_key text) RETURNS void\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nBEGIN\n IF p_tenant IS NULL OR p_key IS NULL THEN RETURN; END IF;\n -- Serialise this (tenant, key) for the rest of the transaction. The caller that\n -- gets here second waits, then finds the first one's row and replays it.\n PERFORM pg_advisory_xact_lock(hashtextextended(p_tenant::text || ':' || p_key, 0));\nEND $$;\n\nCREATE FUNCTION public.plg_financial_installment_paid(p_installment uuid) RETURNS numeric\n LANGUAGE sql STABLE SECURITY DEFINER\n SET search_path TO ''\n AS $$\n SELECT coalesce(sum(a.amount), 0)\n FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = p_installment AND m.status <> 'cancelled' AND m.reversed_by_id IS NULL;\n$$;\n\nCREATE FUNCTION public.plg_financial_legacy_legs(p_tenant uuid, p_kind text, p_direction text, p_bank_account_id uuid, p_cash_session_id uuid, p_invoice_id uuid, p_metadata jsonb, OUT o_debit uuid, OUT o_credit uuid, OUT o_mirror boolean) RETURNS record\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_holder uuid; v_counter uuid; v_invoice_dir text; v_other uuid;\nBEGIN\n o_mirror := false;\n IF p_kind IN ('bill', 'commission') THEN\n o_mirror := true; RETURN;\n END IF;\n\n -- money holder\n IF p_bank_account_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.plg_financial_accounts a WHERE a.id = p_bank_account_id AND a.tenant_id = p_tenant) THEN\n v_holder := p_bank_account_id;\n ELSIF p_cash_session_id IS NOT NULL THEN\n SELECT s.account_id INTO v_holder FROM public.plg_financial_cash_register_sessions s WHERE s.id = p_cash_session_id AND s.tenant_id = p_tenant;\n END IF;\n -- \\`legacy_holder\\`, not \\`legacy\\`: the counter-leg below falls back to \\`legacy\\`,\n -- and a payment with no holder AND no invoice would otherwise post legacy →\n -- legacy, which legs_check (debit <> credit) rejects (#199).\n IF v_holder IS NULL THEN v_holder := public.plg_financial_system_account(p_tenant, 'legacy_holder'); END IF;\n\n IF p_kind = 'transfer' THEN\n -- the old two-row transfer: 'out' carries the whole transfer, 'in' is a mirror\n IF coalesce(p_metadata ->> 'transferRole', '') = 'in' THEN\n o_mirror := true; RETURN;\n END IF;\n v_other := NULL;\n BEGIN\n v_other := (p_metadata ->> 'counterAccountId')::uuid;\n EXCEPTION WHEN OTHERS THEN v_other := NULL; END;\n IF v_other IS NULL OR NOT EXISTS (SELECT 1 FROM public.plg_financial_accounts a WHERE a.id = v_other AND a.tenant_id = p_tenant) THEN\n v_other := public.plg_financial_system_account(p_tenant, 'legacy');\n END IF;\n IF p_direction = 'debit' THEN -- money leaves the holder\n o_debit := v_other; o_credit := v_holder;\n ELSE\n o_debit := v_holder; o_credit := v_other;\n END IF;\n RETURN;\n END IF;\n\n -- payment: counter-leg is the clearing account of the invoice's direction\n IF p_invoice_id IS NOT NULL THEN\n SELECT t.direction INTO v_invoice_dir FROM public.plg_financial_invoices t WHERE t.id = p_invoice_id;\n END IF;\n v_counter := CASE\n WHEN v_invoice_dir = 'receivable' THEN public.plg_financial_system_account(p_tenant, 'receivables')\n WHEN v_invoice_dir = 'payable' THEN public.plg_financial_system_account(p_tenant, 'payables')\n ELSE public.plg_financial_system_account(p_tenant, 'legacy')\n END;\n IF p_direction = 'credit' THEN -- money in\n o_debit := v_holder; o_credit := v_counter;\n ELSE -- money out\n o_debit := v_counter; o_credit := v_holder;\n END IF;\nEND $$;\n\nCREATE FUNCTION public.plg_financial_migrate_legacy_invoices() RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $_$\nDECLARE\n r record;\n b record;\n v_invoice_id uuid;\n v_inst_id uuid;\n v_dir text;\n v_number text;\n v_total numeric;\n v_n int;\n v_invoices int := 0;\n v_insts int := 0;\n v_links int := 0;\n v_rekinded int := 0;\nBEGIN\n FOR r IN\n SELECT o.*\n FROM public.orders o\n WHERE (o.kind IN ('invoice_receivable', 'invoice_payable')\n OR EXISTS (SELECT 1 FROM public.plg_financial_movements m\n WHERE m.invoice_id = o.id AND m.movement_kind = 'bill'))\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_invoices t WHERE t.order_id = o.id)\n ORDER BY o.created_at, o.id\n LOOP\n -- direction: the bills know it; else the order's kind/direction\n SELECT CASE WHEN min(m.direction) = 'debit' THEN 'payable' ELSE 'receivable' END INTO v_dir\n FROM public.plg_financial_movements m WHERE m.invoice_id = r.id AND m.movement_kind = 'bill';\n IF v_dir IS NULL THEN\n v_dir := CASE WHEN r.kind = 'invoice_payable' OR r.direction = 'debit' THEN 'payable' ELSE 'receivable' END;\n END IF;\n\n -- number: keep the legacy REC-/PAG- number when it is free in the tenant\n v_number := NULL;\n IF r.reference_number ~ '^(REC|PAG)-[0-9]+$'\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_invoices t WHERE t.tenant_id = r.tenant_id AND t.number = r.reference_number) THEN\n v_number := r.reference_number;\n END IF;\n IF v_number IS NULL THEN\n v_number := public.plg_financial_next_invoice_number(r.tenant_id, v_dir);\n END IF;\n\n SELECT coalesce(sum(m.amount), 0) INTO v_total\n FROM public.plg_financial_movements m WHERE m.invoice_id = r.id AND m.movement_kind = 'bill' AND m.status <> 'cancelled';\n IF v_total = 0 THEN v_total := round(coalesce(r.total, 0), 2); END IF;\n\n INSERT INTO public.plg_financial_invoices\n (tenant_id, unit_id, direction, counterparty_person_id, order_id, number, competence_date, issue_date,\n total, description, notes, metadata, cancelled_at, cancel_reason, created_by, created_at)\n VALUES\n (r.tenant_id, r.unit_id, v_dir, r.party_id, r.id, v_number,\n coalesce(r.created_at::date, current_date), coalesce(r.created_at::date, current_date),\n v_total, nullif(r.metadata ->> 'itemsSummary', ''), r.notes,\n jsonb_build_object('source', 'migrate_legacy_invoices', 'legacy_mirror', true,\n 'legacy_kind', r.kind, 'legacy_status', r.status, 'legacy_stage', r.stage,\n 'contactName', r.metadata ->> 'contactName'),\n CASE WHEN r.status = 'cancelled' OR r.stage = 'cancelled' THEN coalesce(r.updated_at, now()) END,\n CASE WHEN r.status = 'cancelled' OR r.stage = 'cancelled' THEN 'legacy: order was cancelled' END,\n r.created_by, r.created_at)\n RETURNING id INTO v_invoice_id;\n v_invoices := v_invoices + 1;\n\n -- installments: one per bill; the bill becomes that installment's mirror\n v_n := 0;\n FOR b IN\n SELECT m.* FROM public.plg_financial_movements m\n WHERE m.invoice_id = r.id AND m.movement_kind = 'bill'\n ORDER BY m.installment_number NULLS LAST, m.due_date, m.created_at\n LOOP\n v_n := v_n + 1;\n INSERT INTO public.plg_financial_invoice_installments (invoice_id, number, due_date, amount, cancelled_at, metadata, created_at)\n VALUES (v_invoice_id, v_n, b.due_date, round(b.amount, 2),\n CASE WHEN b.status = 'cancelled' THEN coalesce(b.updated_at, now()) END,\n jsonb_build_object('legacy_movement_id', b.id, 'legacy_installment_number', b.installment_number),\n b.created_at)\n RETURNING id INTO v_inst_id;\n v_insts := v_insts + 1;\n\n UPDATE public.plg_financial_movements\n SET financial_invoice_id = v_invoice_id, financial_invoice_installment_id = v_inst_id,\n metadata = coalesce(metadata, '{}'::jsonb) || jsonb_build_object('legacy_mirror', true)\n WHERE id = b.id;\n\n -- receipts of this installment (same installment number)\n UPDATE public.plg_financial_movements p\n SET financial_invoice_id = v_invoice_id, financial_invoice_installment_id = v_inst_id\n WHERE p.invoice_id = r.id AND p.movement_kind = 'payment' AND p.financial_invoice_installment_id IS NULL\n AND p.installment_number IS NOT DISTINCT FROM b.installment_number;\n GET DIAGNOSTICS v_links = ROW_COUNT;\n END LOOP;\n\n IF v_n = 0 THEN\n INSERT INTO public.plg_financial_invoice_installments (invoice_id, number, due_date, amount, created_at)\n VALUES (v_invoice_id, 1, coalesce(r.due_at::date, r.created_at::date, current_date), v_total, r.created_at)\n RETURNING id INTO v_inst_id;\n v_insts := v_insts + 1;\n END IF;\n\n -- receipts without an installment number: the first live installment\n UPDATE public.plg_financial_movements p\n SET financial_invoice_id = v_invoice_id,\n financial_invoice_installment_id = (SELECT i.id FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = v_invoice_id AND i.cancelled_at IS NULL\n ORDER BY i.due_date, i.number LIMIT 1)\n WHERE p.invoice_id = r.id AND p.movement_kind = 'payment' AND p.financial_invoice_installment_id IS NULL;\n -- anything else per-order (commission accruals) at least knows its invoice\n UPDATE public.plg_financial_movements p\n SET financial_invoice_id = v_invoice_id\n WHERE p.invoice_id = r.id AND p.financial_invoice_id IS NULL;\n\n -- the invoice kinds are retired: the receivable was a confirmed sale, the\n -- payable a purchase document behind its payable invoice\n IF r.kind = 'invoice_receivable' THEN\n UPDATE public.orders SET kind = 'sale',\n status = CASE WHEN r.status = 'cancelled' THEN 'cancelled' ELSE 'confirmed' END,\n metadata = coalesce(metadata, '{}'::jsonb) || jsonb_build_object('legacy_kind', r.kind, 'legacy_status', r.status, 'invoiceNumber', v_number)\n WHERE id = r.id;\n v_rekinded := v_rekinded + 1;\n ELSIF r.kind = 'invoice_payable' THEN\n UPDATE public.orders SET kind = 'purchase',\n metadata = coalesce(metadata, '{}'::jsonb) || jsonb_build_object('legacy_kind', r.kind, 'legacy_status', r.status, 'invoiceNumber', v_number)\n WHERE id = r.id;\n v_rekinded := v_rekinded + 1;\n END IF;\n END LOOP;\n\n RETURN jsonb_build_object('invoices', v_invoices, 'installments', v_insts, 'orders_rekinded', v_rekinded);\nEND $_$;\n\nCREATE FUNCTION public.plg_financial_next_invoice_number(p_tenant_id uuid, p_direction text) RETURNS text\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_seq bigint;\nBEGIN\n IF p_direction = 'payable' THEN\n v_seq := public.next_sequence_internal(p_tenant_id, 'invoice_payable');\n RETURN 'PAG-' || lpad(v_seq::text, 6, '0');\n END IF;\n v_seq := public.next_sequence_internal(p_tenant_id, 'invoice_receivable');\n RETURN 'REC-' || lpad(v_seq::text, 6, '0');\nEND $$;\n\nCREATE FUNCTION public.plg_financial_open_session_of(p_tenant uuid, p_user uuid, p_unit uuid) RETURNS uuid\n LANGUAGE sql STABLE SECURITY DEFINER\n SET search_path TO ''\n AS $$\n SELECT s.id FROM public.plg_financial_cash_register_sessions s\n WHERE s.tenant_id = p_tenant AND s.user_id = p_user AND s.status = 'open'\n AND (p_unit IS NULL OR s.unit_id = p_unit)\n ORDER BY s.opened_at DESC LIMIT 1;\n$$;\n\nCREATE FUNCTION public.plg_financial_person_account(p_tenant uuid, p_person uuid) RETURNS uuid\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_id uuid; v_name text;\nBEGIN\n IF p_person IS NULL THEN\n RAISE EXCEPTION 'plg_financial_person_account: person required' USING ERRCODE = '22023';\n END IF;\n SELECT id INTO v_id FROM public.plg_financial_accounts WHERE tenant_id = p_tenant AND person_id = p_person;\n IF v_id IS NOT NULL THEN RETURN v_id; END IF;\n SELECT name INTO v_name FROM public.people WHERE id = p_person AND tenant_id = p_tenant;\n IF v_name IS NULL THEN\n RAISE EXCEPTION 'plg_financial_person_account: person % not found in tenant', p_person USING ERRCODE = 'P0002';\n END IF;\n INSERT INTO public.plg_financial_accounts (tenant_id, kind, name, person_id, metadata)\n VALUES (p_tenant, 'person', v_name, p_person, jsonb_build_object('person', true))\n ON CONFLICT DO NOTHING;\n SELECT id INTO v_id FROM public.plg_financial_accounts WHERE tenant_id = p_tenant AND person_id = p_person;\n RETURN v_id;\nEND $$;\n\nCREATE FUNCTION public.plg_financial_require_permission(p_perm text, p_unit uuid DEFAULT NULL::uuid) RETURNS void\n LANGUAGE plpgsql STABLE SECURITY DEFINER\n SET search_path TO ''\n AS $$\nBEGIN\n IF NOT app.has_permission(p_perm, p_unit) THEN\n RAISE EXCEPTION 'financial: permission % required', p_perm USING ERRCODE = '42501';\n END IF;\n IF p_unit IS NOT NULL AND NOT app.has_unit(p_unit) THEN\n RAISE EXCEPTION 'financial: unit % is outside your scope', p_unit USING ERRCODE = '42501';\n END IF;\nEND $$;\n\nCREATE FUNCTION public.plg_financial_require_tenant() RETURNS uuid\n LANGUAGE plpgsql STABLE SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_tenant uuid := app.current_tenant_id();\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial: no active tenant for this session' USING ERRCODE = '42501';\n END IF;\n RETURN v_tenant;\nEND $$;\n\nCREATE FUNCTION public.plg_financial_sync_mirror(p_installment uuid) RETURNS void\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_paid numeric; v_amount numeric; v_cancelled boolean; v_last date;\nBEGIN\n SELECT i.amount, i.cancelled_at IS NOT NULL INTO v_amount, v_cancelled\n FROM public.plg_financial_invoice_installments i WHERE i.id = p_installment;\n IF NOT FOUND THEN RETURN; END IF;\n v_paid := public.plg_financial_installment_paid(p_installment);\n SELECT max(m.payment_date) INTO v_last\n FROM public.plg_financial_allocations a JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = p_installment AND m.status <> 'cancelled' AND m.reversed_by_id IS NULL;\n UPDATE public.plg_financial_movements b\n SET paid_amount = v_paid,\n status = CASE WHEN v_cancelled THEN 'cancelled'\n WHEN v_paid >= v_amount AND v_amount > 0 THEN 'paid'\n WHEN v_paid > 0 THEN 'partial'\n WHEN b.status = 'overdue' THEN 'overdue'\n ELSE 'pending' END,\n payment_date = CASE WHEN v_paid >= v_amount AND v_amount > 0 THEN coalesce(v_last, b.payment_date) ELSE b.payment_date END,\n updated_at = now()\n WHERE b.financial_invoice_installment_id = p_installment AND b.movement_kind = 'bill'\n AND (b.paid_amount IS DISTINCT FROM v_paid\n OR b.status IS DISTINCT FROM CASE WHEN v_cancelled THEN 'cancelled'\n WHEN v_paid >= v_amount AND v_amount > 0 THEN 'paid'\n WHEN v_paid > 0 THEN 'partial'\n WHEN b.status = 'overdue' THEN 'overdue'\n ELSE 'pending' END);\nEND $$;\n\nCREATE FUNCTION public.plg_financial_system_account(p_tenant uuid, p_code text) RETURNS uuid\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_id uuid; v_name text;\nBEGIN\n IF p_code NOT IN ('receivables', 'payables', 'legacy', 'legacy_holder', 'legacy_credit', 'cancellations',\n 'cash_shortage', 'cash_overage', 'opening_balances') THEN\n RAISE EXCEPTION 'plg_financial_system_account: unknown code %', p_code USING ERRCODE = '22023';\n END IF;\n SELECT id INTO v_id FROM public.plg_financial_accounts WHERE tenant_id = p_tenant AND code = p_code;\n IF v_id IS NOT NULL THEN RETURN v_id; END IF;\n v_name := CASE p_code\n WHEN 'receivables' THEN 'Contas a receber (compensação)'\n WHEN 'payables' THEN 'Contas a pagar (compensação)'\n WHEN 'legacy' THEN 'Legado (contrapartida desconhecida)'\n -- The OTHER unknown. \\`legacy\\` is the counterparty we cannot name; this is the\n -- drawer we cannot name. A legacy payment with no bank account, no cash\n -- session and no invoice used to resolve BOTH legs to \\`legacy\\`, and\n -- plg_financial_movements_legs_check (debit <> credit) then rejected the row\n -- the backfill had just built (#199).\n WHEN 'legacy_holder' THEN 'Legado (portador desconhecido)'\n WHEN 'legacy_credit' THEN 'Crédito Legado'\n WHEN 'cancellations' THEN 'Cancelamentos (crédito ao cliente)'\n WHEN 'cash_shortage' THEN 'Quebra de caixa (falta)'\n WHEN 'cash_overage' THEN 'Quebra de caixa (sobra)'\n WHEN 'opening_balances' THEN 'Saldos de abertura'\n END;\n INSERT INTO public.plg_financial_accounts (tenant_id, kind, code, name, metadata)\n VALUES (p_tenant, 'system', p_code, v_name, jsonb_build_object('system', true))\n ON CONFLICT DO NOTHING;\n SELECT id INTO v_id FROM public.plg_financial_accounts WHERE tenant_id = p_tenant AND code = p_code;\n RETURN v_id;\nEND $$;\n\nCREATE FUNCTION public.trg_invoice_line_immutable() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_issued date; v_cancelled timestamptz; v_invoice uuid;\nBEGIN\n v_invoice := coalesce(NEW.invoice_id, OLD.invoice_id);\n IF v_invoice IS NULL THEN RETURN coalesce(NEW, OLD); END IF;\n SELECT f.issue_date, f.cancelled_at INTO v_issued, v_cancelled\n FROM public.plg_financial_invoices f WHERE f.id = v_invoice;\n IF v_issued IS NOT NULL AND v_cancelled IS NULL THEN\n RAISE EXCEPTION 'items: the lines of issued invoice % cannot be altered', v_invoice\n USING ERRCODE = '23514',\n HINT = 'A closed invoice is immutable (PRD 04). Raise a new invoice instead.';\n END IF;\n RETURN coalesce(NEW, OLD);\nEND $$;\n\nCREATE FUNCTION public.trg_plg_financial_allocations_check() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_pay record; v_inst record; v_sum_pay numeric; v_sum_inst numeric;\nBEGIN\n SELECT m.tenant_id, m.unit_id, m.movement_kind, m.amount, m.status, m.reversed_by_id INTO v_pay\n FROM public.plg_financial_movements m WHERE m.id = NEW.payment_movement_id;\n IF NOT FOUND OR v_pay.movement_kind <> 'payment' THEN\n RAISE EXCEPTION 'plg_financial_allocations: % is not a payment movement', NEW.payment_movement_id USING ERRCODE = '23514';\n END IF;\n IF v_pay.status = 'cancelled' OR v_pay.reversed_by_id IS NOT NULL THEN\n RAISE EXCEPTION 'plg_financial_allocations: payment % is cancelled or reversed', NEW.payment_movement_id USING ERRCODE = '23514';\n END IF;\n SELECT i.tenant_id, i.unit_id, i.amount, i.cancelled_at INTO v_inst\n FROM public.plg_financial_invoice_installments i WHERE i.id = NEW.installment_id;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'plg_financial_allocations: installment % not found', NEW.installment_id USING ERRCODE = '23503';\n END IF;\n IF v_inst.tenant_id <> v_pay.tenant_id THEN\n RAISE EXCEPTION 'plg_financial_allocations: payment and installment belong to different tenants' USING ERRCODE = '23514';\n END IF;\n IF v_inst.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'plg_financial_allocations: installment % is cancelled', NEW.installment_id USING ERRCODE = '23514';\n END IF;\n NEW.tenant_id := v_pay.tenant_id;\n NEW.unit_id := coalesce(NEW.unit_id, v_inst.unit_id, v_pay.unit_id);\n NEW.created_by := coalesce(NEW.created_by, auth.uid());\n\n SELECT coalesce(sum(a.amount), 0) INTO v_sum_pay FROM public.plg_financial_allocations a\n WHERE a.payment_movement_id = NEW.payment_movement_id AND (TG_OP = 'INSERT' OR a.id <> NEW.id);\n IF v_sum_pay + NEW.amount > v_pay.amount + 0.000001 THEN\n RAISE EXCEPTION 'plg_financial_allocations: allocations (%) exceed the payment amount (%)', v_sum_pay + NEW.amount, v_pay.amount\n USING ERRCODE = '23514';\n END IF;\n SELECT coalesce(sum(a.amount), 0) INTO v_sum_inst FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = NEW.installment_id AND m.status <> 'cancelled' AND m.reversed_by_id IS NULL AND (TG_OP = 'INSERT' OR a.id <> NEW.id);\n IF v_sum_inst + NEW.amount > v_inst.amount + 0.000001 THEN\n RAISE EXCEPTION 'plg_financial_allocations: allocations (%) exceed the installment amount (%)', v_sum_inst + NEW.amount, v_inst.amount\n USING ERRCODE = '23514';\n END IF;\n RETURN NEW;\nEND $$;\n\nCREATE FUNCTION public.trg_plg_financial_allocations_sync() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nBEGIN\n IF TG_OP IN ('INSERT', 'UPDATE') THEN PERFORM public.plg_financial_sync_mirror(NEW.installment_id); END IF;\n IF TG_OP IN ('UPDATE', 'DELETE') AND (TG_OP = 'DELETE' OR OLD.installment_id <> NEW.installment_id) THEN\n PERFORM public.plg_financial_sync_mirror(OLD.installment_id);\n END IF;\n RETURN NULL;\nEND $$;\n\nCREATE FUNCTION public.trg_plg_financial_bank_accounts_forward() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nBEGIN\n INSERT INTO public.plg_financial_accounts\n (id, tenant_id, unit_id, kind, name, account_type, bank_name, agency_number, account_number,\n credit_limit, due_day, closing_day, is_active, metadata)\n VALUES\n (NEW.id, NEW.tenant_id,\n CASE WHEN EXISTS (SELECT 1 FROM app.units u WHERE u.tenant_id = NEW.tenant_id AND u.id = NEW.unit_id) THEN NEW.unit_id END,\n public.plg_financial_account_kind_of(NEW.account_type), NEW.name, NEW.account_type, NEW.bank_name, NEW.agency_number,\n NEW.account_number, NEW.credit_limit, NEW.due_day, NEW.closing_day, NEW.is_active,\n jsonb_build_object('migrated_from', 'plg_financial_bank_accounts', 'legacy_initial_balance', NEW.initial_balance))\n ON CONFLICT (id) DO UPDATE\n SET name = EXCLUDED.name, account_type = EXCLUDED.account_type, kind = EXCLUDED.kind,\n bank_name = EXCLUDED.bank_name, agency_number = EXCLUDED.agency_number, account_number = EXCLUDED.account_number,\n credit_limit = EXCLUDED.credit_limit, due_day = EXCLUDED.due_day, closing_day = EXCLUDED.closing_day,\n is_active = EXCLUDED.is_active, unit_id = coalesce(EXCLUDED.unit_id, public.plg_financial_accounts.unit_id),\n updated_at = now();\n RETURN NEW;\nEND $$;\n\nCREATE FUNCTION public.trg_plg_financial_card_fee_configs_check() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_kind text; v_tenant uuid; v_unit uuid;\nBEGIN\n SELECT a.kind, a.tenant_id, a.unit_id INTO v_kind, v_tenant, v_unit FROM public.plg_financial_accounts a WHERE a.id = NEW.account_id;\n IF v_kind IS NULL OR v_kind NOT IN ('card_receivable_credit', 'card_receivable_debit') THEN\n RAISE EXCEPTION 'plg_financial_card_fee_configs: account % is not a card receiving account', NEW.account_id USING ERRCODE = '23514';\n END IF;\n IF NEW.tenant_id IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'plg_financial_card_fee_configs: account belongs to another tenant' USING ERRCODE = '23514';\n END IF;\n NEW.unit_id := coalesce(NEW.unit_id, v_unit);\n NEW.brand := nullif(btrim(NEW.brand), '');\n IF NEW.brand IS NULL AND NEW.card_brand_id IS NOT NULL THEN\n SELECT b.name INTO NEW.brand FROM public.plg_financial_card_brands b WHERE b.id = NEW.card_brand_id;\n END IF;\n RETURN NEW;\nEND $$;\n\nCREATE FUNCTION public.trg_plg_financial_cash_sessions_owner() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nBEGIN\n IF NEW.user_id IS NULL THEN NEW.user_id := coalesce(NEW.opened_by_user_id, auth.uid()); END IF;\n NEW.opened_by_user_id := coalesce(NEW.opened_by_user_id, NEW.user_id);\n IF EXISTS (SELECT 1 FROM app.memberships m WHERE m.tenant_id = NEW.tenant_id AND m.user_id = NEW.user_id) THEN\n NEW.owner_id := NEW.user_id;\n END IF;\n RETURN NEW;\nEND $$;\n\nCREATE FUNCTION public.trg_plg_financial_installments_inherit() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_tenant uuid; v_unit uuid;\nBEGIN\n SELECT t.tenant_id, t.unit_id INTO v_tenant, v_unit FROM public.plg_financial_invoices t WHERE t.id = NEW.invoice_id;\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'plg_financial_invoice_installments: invoice % not found', NEW.invoice_id USING ERRCODE = '23503';\n END IF;\n IF NEW.tenant_id IS NOT NULL AND NEW.tenant_id <> v_tenant THEN\n RAISE EXCEPTION 'plg_financial_invoice_installments: tenant_id must be the invoice''s tenant' USING ERRCODE = '23514';\n END IF;\n NEW.tenant_id := v_tenant;\n IF NEW.unit_id IS NULL THEN NEW.unit_id := v_unit; END IF;\n RETURN NEW;\nEND $$;\n\nCREATE FUNCTION public.trg_plg_financial_invoice_items_inherit() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_tenant uuid; v_unit uuid;\nBEGIN\n SELECT i.tenant_id, i.unit_id INTO v_tenant, v_unit FROM public.items i WHERE i.id = NEW.item_id;\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'plg_financial_invoice_items: line % not found', NEW.item_id USING ERRCODE = '23503';\n END IF;\n NEW.tenant_id := v_tenant;\n IF NEW.unit_id IS NULL THEN NEW.unit_id := v_unit; END IF;\n RETURN NEW;\nEND $$;\n\nCREATE FUNCTION public.trg_plg_financial_invoices_immutable() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nBEGIN\n -- Not closed yet: a draft is a draft.\n IF OLD.issue_date IS NULL OR OLD.cancelled_at IS NOT NULL THEN RETURN NEW; END IF;\n\n -- The named exception: cancelling, and the bookkeeping that goes with it.\n IF NEW.cancelled_at IS NOT NULL AND OLD.cancelled_at IS NULL THEN RETURN NEW; END IF;\n\n IF NEW.total IS DISTINCT FROM OLD.total\n OR NEW.direction IS DISTINCT FROM OLD.direction\n OR NEW.counterparty_person_id IS DISTINCT FROM OLD.counterparty_person_id\n OR NEW.order_id IS DISTINCT FROM OLD.order_id\n OR NEW.number IS DISTINCT FROM OLD.number\n OR NEW.competence_date IS DISTINCT FROM OLD.competence_date\n OR NEW.issue_date IS DISTINCT FROM OLD.issue_date\n OR NEW.currency IS DISTINCT FROM OLD.currency THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % is issued and cannot be altered', OLD.number\n USING ERRCODE = '23514',\n HINT = 'A closed invoice is immutable (PRD 04). Correct it with a new invoice, or cancel this one through financial_cancel_invoice.';\n END IF;\n RETURN NEW;\nEND $$;\n\nCREATE FUNCTION public.trg_plg_financial_movements_after_insert() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_open numeric; v_alloc numeric;\nBEGIN\n IF NEW.movement_kind = 'payment' AND NEW.status <> 'cancelled'\n AND NEW.financial_invoice_installment_id IS NOT NULL\n AND coalesce(NEW.metadata ->> 'legacy_shape', '') = 'true' THEN\n SELECT i.amount - public.plg_financial_installment_paid(i.id) INTO v_open\n FROM public.plg_financial_invoice_installments i WHERE i.id = NEW.financial_invoice_installment_id AND i.cancelled_at IS NULL;\n v_alloc := LEAST(coalesce(v_open, 0), NEW.amount);\n IF v_alloc > 0 THEN\n INSERT INTO public.plg_financial_allocations (tenant_id, unit_id, payment_movement_id, installment_id, amount, created_by, metadata)\n VALUES (NEW.tenant_id, NEW.unit_id, NEW.id, NEW.financial_invoice_installment_id, v_alloc, NEW.created_by, jsonb_build_object('auto', true))\n ON CONFLICT (payment_movement_id, installment_id) DO NOTHING;\n END IF;\n END IF;\n RETURN NULL;\nEND $$;\n\nCREATE FUNCTION public.trg_plg_financial_movements_append_only() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nBEGIN\n IF TG_OP = 'DELETE' THEN\n IF pg_trigger_depth() > 1 THEN RETURN OLD; END IF; -- a cascade (tenant removal)\n RAISE EXCEPTION 'plg_financial_movements is append-only: a money event is never deleted — reverse it (financial_reverse_movement)'\n USING ERRCODE = '55000';\n END IF;\n\n -- immutable on every row\n IF NEW.id <> OLD.id OR NEW.tenant_id <> OLD.tenant_id OR NEW.movement_kind <> OLD.movement_kind\n OR NEW.amount <> OLD.amount OR NEW.direction IS DISTINCT FROM OLD.direction\n OR NEW.created_at <> OLD.created_at\n OR (OLD.invoice_id IS NOT NULL AND NEW.invoice_id IS DISTINCT FROM OLD.invoice_id)\n OR (OLD.financial_invoice_id IS NOT NULL AND NEW.financial_invoice_id IS DISTINCT FROM OLD.financial_invoice_id)\n OR (OLD.financial_invoice_installment_id IS NOT NULL AND NEW.financial_invoice_installment_id IS DISTINCT FROM OLD.financial_invoice_installment_id)\n OR (OLD.reversed_by_id IS NOT NULL AND NEW.reversed_by_id IS DISTINCT FROM OLD.reversed_by_id)\n OR NEW.reverses_id IS DISTINCT FROM OLD.reverses_id\n -- A leg may be FILLED exactly once and never rewritten — same shape as the\n -- link columns three lines up. Rows that predate the two-leg model carry\n -- NULL legs, and giving them their posting is what the backfill below\n -- exists to do; refusing that made this migration abort on every pool that\n -- actually had legacy movements, which is every real V1 cutover (#199).\n OR (OLD.debit_account_id IS NOT NULL AND NEW.debit_account_id IS DISTINCT FROM OLD.debit_account_id)\n OR (OLD.credit_account_id IS NOT NULL AND NEW.credit_account_id IS DISTINCT FROM OLD.credit_account_id) THEN\n RAISE EXCEPTION 'plg_financial_movements is append-only: amount, kind, accounts and links of movement % cannot change — post a reversal instead', OLD.id\n USING ERRCODE = '55000';\n END IF;\n\n IF OLD.movement_kind IN ('bill', 'commission') THEN\n RETURN NEW; -- obligation mirrors: derived columns may move\n END IF;\n\n -- money events: only reconciliation marks, notes/metadata, reversal stamp\n IF NEW.paid_amount IS DISTINCT FROM OLD.paid_amount\n OR NEW.due_date IS DISTINCT FROM OLD.due_date\n OR NEW.payment_date IS DISTINCT FROM OLD.payment_date\n OR NEW.installment_number IS DISTINCT FROM OLD.installment_number\n OR NEW.payment_method_id IS DISTINCT FROM OLD.payment_method_id\n OR NEW.payment_method_type_id IS DISTINCT FROM OLD.payment_method_type_id\n OR NEW.bank_account_id IS DISTINCT FROM OLD.bank_account_id\n OR NEW.cash_session_id IS DISTINCT FROM OLD.cash_session_id\n OR NEW.card_brand IS DISTINCT FROM OLD.card_brand\n OR NEW.card_installments IS DISTINCT FROM OLD.card_installments\n OR NEW.fee_amount IS DISTINCT FROM OLD.fee_amount\n OR NEW.method IS DISTINCT FROM OLD.method\n OR NEW.nsu IS DISTINCT FROM OLD.nsu\n OR NEW.external_id IS DISTINCT FROM OLD.external_id\n OR NEW.external_source IS DISTINCT FROM OLD.external_source\n OR NEW.idempotency_key IS DISTINCT FROM OLD.idempotency_key THEN\n RAISE EXCEPTION 'plg_financial_movements is append-only: money event % cannot be edited — post a reversal (financial_reverse_movement) or a new movement', OLD.id\n USING ERRCODE = '55000';\n END IF;\n IF NEW.status IS DISTINCT FROM OLD.status THEN\n RAISE EXCEPTION 'plg_financial_movements is append-only: the status of money event % never changes — a reversal is a new movement (financial_reverse_movement)', OLD.id\n USING ERRCODE = '55000';\n END IF;\n RETURN NEW;\nEND $$;\n\nCREATE FUNCTION public.trg_plg_financial_movements_before_insert() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_invoice uuid; v_inst uuid; v_legs record; v_dir text;\nBEGIN\n NEW.created_by := coalesce(NEW.created_by, auth.uid());\n NEW.metadata := coalesce(NEW.metadata, '{}'::jsonb);\n IF NEW.amount IS NULL OR NEW.amount < 0 THEN\n RAISE EXCEPTION 'plg_financial_movements: amount must be >= 0' USING ERRCODE = '23514';\n END IF;\n\n IF NEW.movement_kind IN ('bill', 'commission') THEN\n -- obligation mirrors carry no legs\n NEW.debit_account_id := NULL; NEW.credit_account_id := NULL;\n IF NEW.financial_invoice_installment_id IS NOT NULL THEN\n NEW.metadata := NEW.metadata || jsonb_build_object('legacy_mirror', true);\n IF NEW.financial_invoice_id IS NULL THEN\n SELECT i.invoice_id INTO NEW.financial_invoice_id FROM public.plg_financial_invoice_installments i WHERE i.id = NEW.financial_invoice_installment_id;\n END IF;\n ELSE\n NEW.metadata := NEW.metadata || jsonb_build_object('legacy_orphan', true);\n END IF;\n RETURN NEW;\n END IF;\n\n -- payment / transfer: resolve the installment for legacy per-order writers\n IF NEW.movement_kind = 'payment' AND NEW.financial_invoice_installment_id IS NULL AND NEW.invoice_id IS NOT NULL THEN\n SELECT t.id INTO v_invoice FROM public.plg_financial_invoices t\n WHERE t.order_id = NEW.invoice_id AND t.tenant_id = NEW.tenant_id AND t.cancelled_at IS NULL\n ORDER BY t.created_at DESC LIMIT 1;\n IF v_invoice IS NOT NULL THEN\n SELECT i.id INTO v_inst FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = v_invoice AND i.cancelled_at IS NULL\n AND (NEW.installment_number IS NULL OR i.number = NEW.installment_number)\n ORDER BY CASE WHEN public.plg_financial_installment_paid(i.id) < i.amount THEN 0 ELSE 1 END, i.due_date, i.number\n LIMIT 1;\n NEW.financial_invoice_id := coalesce(NEW.financial_invoice_id, v_invoice);\n NEW.financial_invoice_installment_id := v_inst;\n END IF;\n END IF;\n IF NEW.financial_invoice_installment_id IS NOT NULL AND NEW.financial_invoice_id IS NULL THEN\n SELECT i.invoice_id INTO NEW.financial_invoice_id FROM public.plg_financial_invoice_installments i WHERE i.id = NEW.financial_invoice_installment_id;\n END IF;\n\n IF NEW.debit_account_id IS NULL OR NEW.credit_account_id IS NULL THEN\n -- legacy shape: derive the legs\n SELECT * INTO v_legs FROM public.plg_financial_legacy_legs(\n NEW.tenant_id, NEW.movement_kind, NEW.direction, NEW.bank_account_id, NEW.cash_session_id, NEW.financial_invoice_id, NEW.metadata);\n IF v_legs.o_mirror THEN\n NEW.debit_account_id := NULL; NEW.credit_account_id := NULL;\n NEW.metadata := NEW.metadata || jsonb_build_object('legacy_mirror', true);\n ELSE\n NEW.debit_account_id := v_legs.o_debit; NEW.credit_account_id := v_legs.o_credit;\n NEW.metadata := NEW.metadata || jsonb_build_object('legacy_shape', true);\n END IF;\n ELSE\n IF NEW.debit_account_id = NEW.credit_account_id THEN\n RAISE EXCEPTION 'plg_financial_movements: debit and credit accounts must differ' USING ERRCODE = '23514';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_accounts a WHERE a.id = NEW.debit_account_id AND a.tenant_id = NEW.tenant_id)\n OR NOT EXISTS (SELECT 1 FROM public.plg_financial_accounts a WHERE a.id = NEW.credit_account_id AND a.tenant_id = NEW.tenant_id) THEN\n RAISE EXCEPTION 'plg_financial_movements: both accounts must belong to the tenant' USING ERRCODE = '23514';\n END IF;\n END IF;\n\n IF NEW.direction IS NULL THEN\n -- money entering an asset-kind debit account is 'credit' (money in) for the tenant\n SELECT CASE WHEN a.kind IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit', 'card_receivable_debit') THEN 'credit' ELSE 'debit' END\n INTO v_dir FROM public.plg_financial_accounts a WHERE a.id = NEW.debit_account_id;\n NEW.direction := coalesce(v_dir, 'credit');\n END IF;\n IF NEW.status IS NULL THEN NEW.status := 'paid'; END IF;\n IF NEW.paid_amount IS NULL AND NEW.status = 'paid' THEN NEW.paid_amount := NEW.amount; END IF;\n RETURN NEW;\nEND $$;\n\n\n-- ── constraint ──────────────────────────────────────────────────────────\n\nALTER TABLE ONLY public.plg_financial_accounts\n ADD CONSTRAINT plg_financial_accounts_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_accounts\n ADD CONSTRAINT plg_financial_accounts_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_allocations\n ADD CONSTRAINT plg_financial_allocations_payment_movement_id_installment_i_key UNIQUE (payment_movement_id, installment_id);\n\nALTER TABLE ONLY public.plg_financial_allocations\n ADD CONSTRAINT plg_financial_allocations_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_allocations\n ADD CONSTRAINT plg_financial_allocations_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_bank_accounts\n ADD CONSTRAINT plg_financial_bank_accounts_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_bank_accounts\n ADD CONSTRAINT plg_financial_bank_accounts_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_card_brands\n ADD CONSTRAINT plg_financial_card_brands_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_card_brands\n ADD CONSTRAINT plg_financial_card_brands_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_card_fee_configs\n ADD CONSTRAINT plg_financial_card_fee_configs_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_card_fee_configs\n ADD CONSTRAINT plg_financial_card_fee_configs_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_cash_register_sessions\n ADD CONSTRAINT plg_financial_cash_register_sessions_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_cash_register_sessions\n ADD CONSTRAINT plg_financial_cash_register_sessions_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_cash_session_reviewers\n ADD CONSTRAINT plg_financial_cash_session_reviewers_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_cash_session_reviewers\n ADD CONSTRAINT plg_financial_cash_session_reviewers_session_id_reviewer_id_key UNIQUE (session_id, reviewer_id);\n\nALTER TABLE ONLY public.plg_financial_cash_session_reviewers\n ADD CONSTRAINT plg_financial_cash_session_reviewers_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_chart_of_accounts\n ADD CONSTRAINT plg_financial_chart_of_accounts_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_chart_of_accounts\n ADD CONSTRAINT plg_financial_chart_of_accounts_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_cost_centers\n ADD CONSTRAINT plg_financial_cost_centers_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_cost_centers\n ADD CONSTRAINT plg_financial_cost_centers_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_invoice_installments\n ADD CONSTRAINT plg_financial_invoice_installments_invoice_id_number_key UNIQUE (invoice_id, number);\n\nALTER TABLE ONLY public.plg_financial_invoice_installments\n ADD CONSTRAINT plg_financial_invoice_installments_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_invoice_installments\n ADD CONSTRAINT plg_financial_invoice_installments_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_invoice_items\n ADD CONSTRAINT plg_financial_invoice_items_pkey PRIMARY KEY (item_id);\n\nALTER TABLE ONLY public.plg_financial_invoices\n ADD CONSTRAINT plg_financial_invoices_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_invoices\n ADD CONSTRAINT plg_financial_invoices_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_nfe\n ADD CONSTRAINT plg_financial_nfe_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_nfe\n ADD CONSTRAINT plg_financial_nfe_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_payment_method_types\n ADD CONSTRAINT plg_financial_payment_method_types_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_payment_method_types\n ADD CONSTRAINT plg_financial_payment_method_types_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_financial_payment_methods\n ADD CONSTRAINT plg_financial_payment_methods_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_financial_payment_methods\n ADD CONSTRAINT plg_financial_payment_methods_tenant_id_id_key UNIQUE (tenant_id, id);\n\n\n-- ── view ────────────────────────────────────────────────────────────────\n\nCREATE VIEW public.v_financial_installment_balances WITH (security_invoker='true') AS\n WITH paid AS (\n SELECT a.installment_id,\n sum(a.amount) AS paid\n FROM (public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON ((m.id = a.payment_movement_id)))\n WHERE ((m.status <> 'cancelled'::text) AND (m.reversed_by_id IS NULL))\n GROUP BY a.installment_id\n )\n SELECT i.id AS installment_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n t.number AS invoice_number,\n t.direction,\n t.counterparty_person_id,\n t.order_id,\n i.number,\n i.due_date,\n i.amount,\n (COALESCE(p.paid, (0)::numeric))::numeric(14,2) AS paid,\n (\n CASE\n WHEN ((i.cancelled_at IS NOT NULL) OR (t.cancelled_at IS NOT NULL)) THEN (0)::numeric\n ELSE GREATEST((i.amount - COALESCE(p.paid, (0)::numeric)), (0)::numeric)\n END)::numeric(14,2) AS balance,\n CASE\n WHEN ((i.cancelled_at IS NOT NULL) OR (t.cancelled_at IS NOT NULL)) THEN 'cancelled'::text\n WHEN (COALESCE(p.paid, (0)::numeric) >= i.amount) THEN 'paid'::text\n WHEN (COALESCE(p.paid, (0)::numeric) > (0)::numeric) THEN 'partial'::text\n WHEN (i.due_date < CURRENT_DATE) THEN 'overdue'::text\n ELSE 'pending'::text\n END AS status,\n i.cancelled_at,\n i.metadata,\n i.created_at,\n i.updated_at\n FROM ((public.plg_financial_invoice_installments i\n JOIN public.plg_financial_invoices t ON ((t.id = i.invoice_id)))\n LEFT JOIN paid p ON ((p.installment_id = i.id)));\n\nCREATE VIEW public.v_financial_invoice_balances WITH (security_invoker='true') AS\n WITH ib AS (\n SELECT v_financial_installment_balances.invoice_id,\n count(*) FILTER (WHERE (v_financial_installment_balances.status <> 'cancelled'::text)) AS installment_count,\n sum(v_financial_installment_balances.amount) FILTER (WHERE (v_financial_installment_balances.status <> 'cancelled'::text)) AS billed,\n sum(v_financial_installment_balances.paid) AS paid,\n bool_or((v_financial_installment_balances.status = 'overdue'::text)) AS has_overdue,\n min(v_financial_installment_balances.due_date) FILTER (WHERE (v_financial_installment_balances.status = ANY (ARRAY['pending'::text, 'partial'::text, 'overdue'::text]))) AS next_due_date,\n max(v_financial_installment_balances.due_date) FILTER (WHERE (v_financial_installment_balances.status <> 'cancelled'::text)) AS last_due_date\n FROM public.v_financial_installment_balances\n GROUP BY v_financial_installment_balances.invoice_id\n )\n SELECT t.id,\n t.tenant_id,\n t.unit_id,\n t.direction,\n t.number,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n t.order_id,\n t.competence_date,\n t.issue_date,\n t.total,\n (COALESCE(ib.billed, (0)::numeric))::numeric(14,2) AS billed,\n (COALESCE(ib.paid, (0)::numeric))::numeric(14,2) AS paid,\n (\n CASE\n WHEN (t.cancelled_at IS NOT NULL) THEN (0)::numeric\n ELSE GREATEST((COALESCE(ib.billed, t.total, (0)::numeric) - COALESCE(ib.paid, (0)::numeric)), (0)::numeric)\n END)::numeric(14,2) AS balance,\n CASE\n WHEN (t.cancelled_at IS NOT NULL) THEN 'cancelled'::text\n WHEN (COALESCE(ib.billed, t.total, (0)::numeric) = (0)::numeric) THEN 'paid'::text\n WHEN (COALESCE(ib.paid, (0)::numeric) >= COALESCE(ib.billed, t.total, (0)::numeric)) THEN 'paid'::text\n WHEN (COALESCE(ib.paid, (0)::numeric) > (0)::numeric) THEN 'partial'::text\n WHEN COALESCE(ib.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n (COALESCE(ib.installment_count, (0)::bigint))::integer AS installment_count,\n ib.next_due_date,\n ib.last_due_date,\n t.description,\n t.notes,\n t.metadata,\n t.idempotency_key,\n t.cancelled_at,\n t.cancel_reason,\n t.created_by,\n t.created_at,\n t.updated_at\n FROM ((public.plg_financial_invoices t\n LEFT JOIN ib ON ((ib.invoice_id = t.id)))\n LEFT JOIN public.people p ON ((p.id = t.counterparty_person_id)));\n\nCREATE VIEW public.v_invoice_balances WITH (security_invoker='true') AS\n WITH tb AS (\n SELECT t.order_id,\n min(t.number) AS reference_number,\n (min((t.counterparty_person_id)::text))::uuid AS party_id,\n min(t.direction) AS flow,\n sum(t.billed) AS amount,\n sum(t.paid) AS paid,\n sum(t.balance) AS balance,\n bool_and((t.status = 'cancelled'::text)) AS all_cancelled,\n bool_or((t.status = 'overdue'::text)) AS has_overdue,\n min(t.created_at) AS created_at,\n max(t.updated_at) AS updated_at\n FROM public.v_financial_invoice_balances t\n WHERE (t.order_id IS NOT NULL)\n GROUP BY t.order_id\n )\n SELECT o.id AS invoice_id,\n o.tenant_id,\n o.kind,\n tb.reference_number,\n tb.party_id,\n CASE\n WHEN (tb.flow = 'payable'::text) THEN 'debit'::text\n ELSE 'credit'::text\n END AS direction,\n tb.flow,\n COALESCE(tb.amount, (0)::numeric) AS amount,\n COALESCE(tb.paid, (0)::numeric) AS paid,\n CASE\n WHEN ((o.status = 'cancelled'::text) OR (o.stage = 'cancelled'::text) OR tb.all_cancelled) THEN (0)::numeric\n ELSE COALESCE(tb.balance, (0)::numeric)\n END AS balance,\n CASE\n WHEN ((o.status = 'cancelled'::text) OR (o.stage = 'cancelled'::text) OR tb.all_cancelled) THEN 'cancelled'::text\n WHEN (COALESCE(tb.amount, (0)::numeric) = (0)::numeric) THEN 'open'::text\n WHEN (COALESCE(tb.paid, (0)::numeric) >= COALESCE(tb.amount, (0)::numeric)) THEN 'paid'::text\n WHEN (COALESCE(tb.paid, (0)::numeric) > (0)::numeric) THEN 'partial'::text\n WHEN COALESCE(tb.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n tb.created_at,\n tb.updated_at\n FROM (tb\n JOIN public.orders o ON ((o.id = tb.order_id)));\n\nCREATE VIEW public.plg_financial_rep_receivables WITH (security_invoker='true') AS\n SELECT b.invoice_id,\n b.tenant_id,\n b.created_at,\n b.reference_number,\n b.kind,\n b.status,\n b.party_id,\n p.name AS party_name,\n b.amount,\n b.paid,\n b.balance,\n CASE\n WHEN (b.status = ANY (ARRAY['open'::text, 'overdue'::text])) THEN b.balance\n ELSE (0)::numeric\n END AS open_balance,\n CASE\n WHEN (b.status = 'overdue'::text) THEN b.balance\n ELSE (0)::numeric\n END AS overdue_balance,\n CASE\n WHEN (b.status = 'paid'::text) THEN b.amount\n ELSE (0)::numeric\n END AS settled_amount,\n CASE\n WHEN (b.status = 'overdue'::text) THEN 1\n ELSE 0\n END AS is_overdue,\n CASE\n WHEN (b.status = 'paid'::text) THEN 1\n ELSE 0\n END AS is_settled\n FROM (public.v_invoice_balances b\n LEFT JOIN public.people p ON ((p.id = b.party_id)))\n WHERE (b.flow = 'receivable'::text);\n\nCREATE VIEW public.v_bill_mirror_writers WITH (security_invoker='true') AS\n SELECT ((quote_ident((n.nspname)::text) || '.'::text) || (p.proname)::text) AS writer\n FROM (pg_proc p\n JOIN pg_namespace n ON ((n.oid = p.pronamespace)))\n WHERE ((n.nspname = ANY (ARRAY['public'::name, 'app'::name, 'migration'::name])) AND (p.prosrc ~ 'INSERT\\\\s+INTO\\\\s+public\\\\.plg_financial_movements'::text) AND (p.prosrc ~ '''bill'''::text));\n\nCREATE VIEW public.v_bill_mirror_pending WITH (security_invoker='true') AS\n SELECT ( SELECT count(*) AS count\n FROM public.plg_financial_movements\n WHERE (plg_financial_movements.movement_kind = 'bill'::text)) AS rows_remaining,\n ( SELECT COALESCE(array_agg(w.writer ORDER BY w.writer), '{}'::text[]) AS \"coalesce\"\n FROM public.v_bill_mirror_writers w) AS writers_remaining;\n\nCREATE VIEW public.v_bill_mirror_readers WITH (security_invoker='true') AS\n SELECT 'function'::text AS reader_kind,\n ((quote_ident((n.nspname)::text) || '.'::text) || (p.proname)::text) AS reader\n FROM (pg_proc p\n JOIN pg_namespace n ON ((n.oid = p.pronamespace)))\n WHERE ((n.nspname = ANY (ARRAY['public'::name, 'app'::name])) AND (p.prosrc ~ 'movement_kind\\\\s*=\\\\s*''bill'''::text) AND (p.proname <> 'financial_retire_bill_mirror'::name))\nUNION ALL\n SELECT 'view'::text AS reader_kind,\n ((quote_ident((n.nspname)::text) || '.'::text) || (c.relname)::text) AS reader\n FROM (pg_class c\n JOIN pg_namespace n ON ((n.oid = c.relnamespace)))\n WHERE ((c.relkind = ANY (ARRAY['v'::\"char\", 'm'::\"char\"])) AND (n.nspname = 'public'::name) AND (c.relname <> ALL (ARRAY['v_bill_mirror_readers'::name, 'v_bill_mirror_writers'::name, 'v_bill_mirror_pending'::name])) AND (pg_get_viewdef(c.oid, true) ~ 'movement_kind\\\\s*=\\\\s*''bill'''::text));\n\nCREATE VIEW public.v_financial_account_balances WITH (security_invoker='true') AS\n WITH d AS (\n SELECT m.debit_account_id AS account_id,\n sum(m.amount) AS total,\n count(*) AS n\n FROM public.plg_financial_movements m\n WHERE ((m.status <> 'cancelled'::text) AND (m.debit_account_id IS NOT NULL) AND (m.movement_kind = ANY (ARRAY['payment'::text, 'transfer'::text])))\n GROUP BY m.debit_account_id\n ), c AS (\n SELECT m.credit_account_id AS account_id,\n sum(m.amount) AS total,\n count(*) AS n\n FROM public.plg_financial_movements m\n WHERE ((m.status <> 'cancelled'::text) AND (m.credit_account_id IS NOT NULL) AND (m.movement_kind = ANY (ARRAY['payment'::text, 'transfer'::text])))\n GROUP BY m.credit_account_id\n )\n SELECT a.id AS account_id,\n a.tenant_id,\n a.unit_id,\n a.kind,\n a.code,\n a.name,\n a.person_id,\n a.is_active,\n a.account_type,\n a.bank_name,\n a.agency_number,\n a.account_number,\n a.metadata,\n (COALESCE(d.total, (0)::numeric))::numeric(14,2) AS debit_total,\n (COALESCE(c.total, (0)::numeric))::numeric(14,2) AS credit_total,\n ((COALESCE(c.total, (0)::numeric) - COALESCE(d.total, (0)::numeric)))::numeric(14,2) AS net_credit,\n (\n CASE\n WHEN (a.kind = ANY (ARRAY['bank'::text, 'cash_physical'::text, 'cash_session'::text, 'card_receivable_credit'::text, 'card_receivable_debit'::text])) THEN (COALESCE(d.total, (0)::numeric) - COALESCE(c.total, (0)::numeric))\n ELSE (COALESCE(c.total, (0)::numeric) - COALESCE(d.total, (0)::numeric))\n END)::numeric(14,2) AS balance,\n ((COALESCE(d.n, (0)::bigint) + COALESCE(c.n, (0)::bigint)))::integer AS movement_count,\n a.created_at,\n a.updated_at\n FROM ((public.plg_financial_accounts a\n LEFT JOIN d ON ((d.account_id = a.id)))\n LEFT JOIN c ON ((c.account_id = a.id)));\n\nCREATE VIEW public.v_financial_card_receivables WITH (security_invoker='true') AS\n WITH pay AS (\n SELECT m.id AS movement_id,\n m.tenant_id,\n m.unit_id,\n m.financial_invoice_id,\n m.financial_invoice_installment_id,\n m.cash_session_id,\n m.debit_account_id AS account_id,\n a.kind AS account_kind,\n a.name AS account_name,\n a.settlement_days AS account_settlement_days,\n m.amount AS gross,\n m.payment_date,\n m.card_brand,\n GREATEST(COALESCE(m.card_installments, 1), 1) AS card_installments,\n m.nsu,\n m.method\n FROM (public.plg_financial_movements m\n JOIN public.plg_financial_accounts a ON ((a.id = m.debit_account_id)))\n WHERE ((m.movement_kind = 'payment'::text) AND (m.status <> 'cancelled'::text) AND (m.reversed_by_id IS NULL) AND (a.kind = ANY (ARRAY['card_receivable_credit'::text, 'card_receivable_debit'::text])))\n ), cfg AS (\n SELECT p.movement_id,\n c_1.id AS fee_config_id,\n c_1.fee_rate,\n c_1.settlement_days,\n row_number() OVER (PARTITION BY p.movement_id ORDER BY (c_1.brand IS NOT NULL) DESC, (c_1.max_installments - c_1.min_installments), c_1.created_at DESC) AS rn\n FROM (pay p\n JOIN public.plg_financial_card_fee_configs c_1 ON (((c_1.account_id = p.account_id) AND c_1.is_active AND ((c_1.brand IS NULL) OR (lower(c_1.brand) = lower(COALESCE(p.card_brand, ''::text)))) AND ((p.card_installments >= c_1.min_installments) AND (p.card_installments <= c_1.max_installments)))))\n ), k AS (\n SELECT p.movement_id,\n p.tenant_id,\n p.unit_id,\n p.financial_invoice_id,\n p.financial_invoice_installment_id,\n p.cash_session_id,\n p.account_id,\n p.account_kind,\n p.account_name,\n p.account_settlement_days,\n p.gross,\n p.payment_date,\n p.card_brand,\n p.card_installments,\n p.nsu,\n p.method,\n generate_series(1, p.card_installments) AS k\n FROM pay p\n )\n SELECT k.movement_id,\n k.tenant_id,\n k.unit_id,\n k.financial_invoice_id,\n k.financial_invoice_installment_id,\n k.cash_session_id,\n k.account_id,\n k.account_kind,\n k.account_name,\n k.card_brand,\n k.card_installments,\n k.nsu,\n k.method,\n k.payment_date,\n k.k AS installment_k,\n k.gross AS gross_total,\n (\n CASE\n WHEN (k.k < k.card_installments) THEN round((k.gross / (k.card_installments)::numeric), 2)\n ELSE round((k.gross - (round((k.gross / (k.card_installments)::numeric), 2) * ((k.card_installments - 1))::numeric)), 2)\n END)::numeric(14,2) AS gross,\n c.fee_config_id,\n c.fee_rate,\n (round(((\n CASE\n WHEN (k.k < k.card_installments) THEN round((k.gross / (k.card_installments)::numeric), 2)\n ELSE round((k.gross - (round((k.gross / (k.card_installments)::numeric), 2) * ((k.card_installments - 1))::numeric)), 2)\n END * COALESCE(c.fee_rate, (0)::numeric)) / (100)::numeric), 2))::numeric(14,2) AS fee,\n (round((\n CASE\n WHEN (k.k < k.card_installments) THEN round((k.gross / (k.card_installments)::numeric), 2)\n ELSE round((k.gross - (round((k.gross / (k.card_installments)::numeric), 2) * ((k.card_installments - 1))::numeric)), 2)\n END * ((1)::numeric - (COALESCE(c.fee_rate, (0)::numeric) / (100)::numeric))), 2))::numeric(14,2) AS net,\n COALESCE(c.settlement_days, k.account_settlement_days,\n CASE\n WHEN (k.account_kind = 'card_receivable_debit'::text) THEN 1\n ELSE 30\n END) AS settlement_days,\n (k.payment_date + (COALESCE(c.settlement_days, k.account_settlement_days,\n CASE\n WHEN (k.account_kind = 'card_receivable_debit'::text) THEN 1\n ELSE 30\n END) + (30 * (k.k - 1)))) AS settlement_date,\n (c.fee_config_id IS NULL) AS missing_fee_config\n FROM (k\n LEFT JOIN cfg c ON (((c.movement_id = k.movement_id) AND (c.rn = 1))));\n\nCREATE VIEW public.v_financial_customer_credit WITH (security_invoker='true') AS\n SELECT b.tenant_id,\n b.person_id,\n b.account_id,\n p.name AS person_name,\n b.credit_total AS credited,\n b.debit_total AS consumed,\n b.balance AS credit,\n b.movement_count,\n b.updated_at\n FROM (public.v_financial_account_balances b\n LEFT JOIN public.people p ON ((p.id = b.person_id)))\n WHERE (b.kind = 'person'::text);\n\nCREATE VIEW public.v_financial_invoice_lines WITH (security_invoker='true') AS\n SELECT i.id AS item_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n f.number AS invoice_number,\n f.direction,\n f.issue_date,\n f.cancelled_at,\n i.product_id,\n i.name,\n i.description,\n i.quantity,\n i.unit_price,\n i.discount,\n i.total,\n x.chart_account_id,\n x.cost_center_id,\n x.tax_amount,\n x.cfop,\n x.ncm,\n a.from_item_id AS billed_from_order_item_id,\n a.quantity AS billed_quantity\n FROM (((public.items i\n JOIN public.plg_financial_invoices f ON ((f.id = i.invoice_id)))\n LEFT JOIN public.plg_financial_invoice_items x ON ((x.item_id = i.id)))\n LEFT JOIN public.item_associations a ON ((a.to_item_id = i.id)))\n WHERE (i.invoice_id IS NOT NULL);\n\nCREATE VIEW public.v_financial_ledger WITH (security_invoker='true') AS\n SELECT i.installment_id AS id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n i.installment_id AS invoice_installment_id,\n i.order_id,\n CASE\n WHEN (i.direction = 'payable'::text) THEN 'debit'::text\n ELSE 'credit'::text\n END AS direction,\n 'bill'::text AS movement_kind,\n i.amount,\n i.paid AS paid_amount,\n i.status,\n i.due_date,\n NULL::date AS payment_date,\n i.number AS installment_number,\n NULL::uuid AS payment_method_id,\n NULL::uuid AS payment_method_type_id,\n NULL::uuid AS bank_account_id,\n NULL::uuid AS cash_session_id,\n NULL::text AS card_brand,\n NULL::integer AS card_installments,\n (0)::numeric(14,2) AS fee_amount,\n NULL::uuid AS debit_account_id,\n NULL::uuid AS credit_account_id,\n NULL::text AS method,\n NULL::text AS nsu,\n NULL::text AS notes,\n i.metadata,\n NULL::text AS external_source,\n NULL::text AS external_id,\n NULL::timestamp with time zone AS reconciled_at,\n NULL::uuid AS matched_movement_id,\n NULL::uuid AS reversed_by_id,\n NULL::uuid AS reverses_id,\n NULL::text AS reason,\n 'installment'::text AS entry_kind,\n i.created_at,\n i.updated_at\n FROM public.v_financial_installment_balances i\nUNION ALL\n SELECT m.id,\n m.tenant_id,\n m.unit_id,\n m.financial_invoice_id AS invoice_id,\n m.financial_invoice_installment_id AS invoice_installment_id,\n m.invoice_id AS order_id,\n m.direction,\n m.movement_kind,\n m.amount,\n m.paid_amount,\n m.status,\n m.due_date,\n m.payment_date,\n m.installment_number,\n m.payment_method_id,\n m.payment_method_type_id,\n COALESCE(m.bank_account_id,\n CASE\n WHEN (m.direction = 'credit'::text) THEN m.debit_account_id\n ELSE m.credit_account_id\n END) AS bank_account_id,\n m.cash_session_id,\n m.card_brand,\n m.card_installments,\n (COALESCE(m.fee_amount, (0)::numeric))::numeric(14,2) AS fee_amount,\n m.debit_account_id,\n m.credit_account_id,\n m.method,\n m.nsu,\n m.notes,\n m.metadata,\n m.external_source,\n m.external_id,\n m.reconciled_at,\n m.matched_movement_id,\n m.reversed_by_id,\n m.reverses_id,\n m.reason,\n 'movement'::text AS entry_kind,\n m.created_at,\n m.updated_at\n FROM public.plg_financial_movements m\n WHERE ((NOT ((m.movement_kind = ANY (ARRAY['bill'::text, 'commission'::text])) AND (m.financial_invoice_installment_id IS NOT NULL))) AND (NOT ((m.movement_kind = 'transfer'::text) AND (m.metadata ? 'legacy_mirror'::text))));\n\nCREATE VIEW public.v_financial_person_payments WITH (security_invoker='true') AS\n SELECT l.id,\n l.tenant_id,\n l.unit_id,\n COALESCE(inv.counterparty_person_id, o.party_id) AS party_id,\n l.amount,\n COALESCE(inv.currency, 'BRL'::text) AS currency,\n COALESCE(pm.name, l.method) AS payment_method,\n l.status,\n COALESCE((l.payment_date)::timestamp with time zone, l.created_at) AS transacted_at,\n l.invoice_id,\n l.order_id,\n l.movement_kind,\n l.direction,\n l.created_at\n FROM (((public.v_financial_ledger l\n LEFT JOIN public.plg_financial_invoices inv ON ((inv.id = l.invoice_id)))\n LEFT JOIN public.orders o ON ((o.id = l.order_id)))\n LEFT JOIN public.plg_financial_payment_methods pm ON ((pm.id = l.payment_method_id)))\n WHERE ((l.entry_kind = 'movement'::text) AND (l.movement_kind <> 'bill'::text) AND (COALESCE(inv.counterparty_person_id, o.party_id) IS NOT NULL));\n\nCREATE VIEW public.v_order_billing WITH (security_invoker='true') AS\n SELECT o.id AS order_id,\n o.tenant_id,\n o.unit_id,\n o.total AS order_total,\n COALESCE(sum(f.total) FILTER (WHERE (f.cancelled_at IS NULL)), (0)::numeric) AS invoiced_total,\n (o.total - COALESCE(sum(f.total) FILTER (WHERE (f.cancelled_at IS NULL)), (0)::numeric)) AS uninvoiced_total,\n count(f.id) FILTER (WHERE (f.cancelled_at IS NULL)) AS invoices\n FROM (public.orders o\n LEFT JOIN public.plg_financial_invoices f ON ((f.order_id = o.id)))\n GROUP BY o.id, o.tenant_id, o.unit_id, o.total;\n\nCREATE VIEW public.v_order_payment_status WITH (security_invoker='true') AS\n SELECT o.id AS order_id,\n o.tenant_id,\n o.unit_id,\n o.status AS order_status,\n o.reference_number,\n (COALESCE(sum(b.amount), (0)::numeric))::numeric(14,2) AS total_amount,\n (COALESCE(sum(b.paid), (0)::numeric))::numeric(14,2) AS paid_amount,\n (COALESCE(sum(b.balance), (0)::numeric))::numeric(14,2) AS balance,\n CASE\n WHEN (count(b.installment_id) = 0) THEN 'none'::text\n WHEN bool_and((b.status = 'cancelled'::text)) THEN 'cancelled'::text\n WHEN bool_and((b.status = ANY (ARRAY['paid'::text, 'cancelled'::text]))) THEN 'paid'::text\n WHEN bool_or((b.status = 'overdue'::text)) THEN 'overdue'::text\n WHEN bool_or((b.status = ANY (ARRAY['paid'::text, 'partial'::text]))) THEN 'partial'::text\n ELSE 'pending'::text\n END AS status\n FROM (public.orders o\n LEFT JOIN public.v_financial_installment_balances b ON ((b.order_id = o.id)))\n GROUP BY o.id, o.tenant_id, o.unit_id, o.status, o.reference_number;\n\nCREATE VIEW public.v_orders_sem_invoice WITH (security_invoker='true') AS\n SELECT id AS order_id,\n tenant_id,\n unit_id,\n kind,\n channel,\n status,\n total,\n created_at\n FROM public.orders o\n WHERE ((kind = ANY (ARRAY['sale'::text, 'purchase'::text, 'course'::text])) AND (status = ANY (ARRAY['completed'::text, 'invoiced'::text, 'paid'::text])) AND (NOT (EXISTS ( SELECT 1\n FROM public.plg_financial_invoices f\n WHERE ((f.order_id = o.id) AND (f.cancelled_at IS NULL))))));\n\n\n-- ── index ───────────────────────────────────────────────────────────────\n\nCREATE INDEX idx_plg_financial_bank_accounts_tenant ON public.plg_financial_bank_accounts USING btree (tenant_id);\n\nCREATE INDEX idx_plg_financial_card_brands_tenant ON public.plg_financial_card_brands USING btree (tenant_id);\n\nCREATE INDEX idx_plg_financial_cash_register_sessions_tenant ON public.plg_financial_cash_register_sessions USING btree (tenant_id);\n\nCREATE INDEX idx_plg_financial_chart_of_accounts_parent ON public.plg_financial_chart_of_accounts USING btree (parent_id);\n\nCREATE INDEX idx_plg_financial_chart_of_accounts_tenant ON public.plg_financial_chart_of_accounts USING btree (tenant_id);\n\nCREATE INDEX idx_plg_financial_cost_centers_tenant ON public.plg_financial_cost_centers USING btree (tenant_id);\n\nCREATE INDEX idx_plg_financial_movements_bank ON public.plg_financial_movements USING btree (bank_account_id);\n\nCREATE INDEX idx_plg_financial_movements_due ON public.plg_financial_movements USING btree (tenant_id, due_date);\n\nCREATE INDEX idx_plg_financial_movements_invoice ON public.plg_financial_movements USING btree (invoice_id);\n\nCREATE INDEX idx_plg_financial_movements_settles ON public.plg_financial_movements USING btree (settles_movement_id) WHERE (settles_movement_id IS NOT NULL);\n\nCREATE INDEX idx_plg_financial_movements_statement ON public.plg_financial_movements USING btree (bank_account_id, payment_date) WHERE (status = ANY (ARRAY['paid'::text, 'partial'::text]));\n\nCREATE INDEX idx_plg_financial_movements_tenant ON public.plg_financial_movements USING btree (tenant_id);\n\nCREATE INDEX idx_plg_financial_movements_unreconciled ON public.plg_financial_movements USING btree (tenant_id, payment_date) WHERE ((external_source IS NOT NULL) AND (reconciled_at IS NULL));\n\nCREATE INDEX idx_plg_financial_nfe_order ON public.plg_financial_nfe USING btree (order_id);\n\nCREATE INDEX idx_plg_financial_nfe_tenant ON public.plg_financial_nfe USING btree (tenant_id);\n\nCREATE INDEX idx_plg_financial_payment_method_types_tenant ON public.plg_financial_payment_method_types USING btree (tenant_id);\n\nCREATE INDEX idx_plg_financial_payment_methods_tenant ON public.plg_financial_payment_methods USING btree (tenant_id);\n\nCREATE UNIQUE INDEX plg_financial_accounts_code_uq ON public.plg_financial_accounts USING btree (tenant_id, code) WHERE (code IS NOT NULL);\n\nCREATE INDEX plg_financial_accounts_kind_idx ON public.plg_financial_accounts USING btree (tenant_id, kind) WHERE is_active;\n\nCREATE UNIQUE INDEX plg_financial_accounts_person_uq ON public.plg_financial_accounts USING btree (tenant_id, person_id) WHERE (person_id IS NOT NULL);\n\nCREATE INDEX plg_financial_accounts_tenant_owner_idx ON public.plg_financial_accounts USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_accounts_tenant_unit_idx ON public.plg_financial_accounts USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_allocations_installment_idx ON public.plg_financial_allocations USING btree (installment_id);\n\nCREATE INDEX plg_financial_allocations_payment_idx ON public.plg_financial_allocations USING btree (payment_movement_id);\n\nCREATE INDEX plg_financial_allocations_tenant_owner_idx ON public.plg_financial_allocations USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_allocations_tenant_unit_idx ON public.plg_financial_allocations USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_bank_accounts_tenant_owner_idx ON public.plg_financial_bank_accounts USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_bank_accounts_tenant_unit_idx ON public.plg_financial_bank_accounts USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_card_brands_tenant_owner_idx ON public.plg_financial_card_brands USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_card_brands_tenant_unit_idx ON public.plg_financial_card_brands USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_card_fee_configs_account_idx ON public.plg_financial_card_fee_configs USING btree (account_id) WHERE is_active;\n\nCREATE INDEX plg_financial_card_fee_configs_tenant_owner_idx ON public.plg_financial_card_fee_configs USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_card_fee_configs_tenant_unit_idx ON public.plg_financial_card_fee_configs USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_cash_register_sessions_tenant_owner_idx ON public.plg_financial_cash_register_sessions USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_cash_register_sessions_tenant_unit_idx ON public.plg_financial_cash_register_sessions USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_cash_session_reviewers_tenant_owner_idx ON public.plg_financial_cash_session_reviewers USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_cash_session_reviewers_tenant_unit_idx ON public.plg_financial_cash_session_reviewers USING btree (tenant_id, unit_id);\n\nCREATE UNIQUE INDEX plg_financial_cash_sessions_one_open_uq ON public.plg_financial_cash_register_sessions USING btree (tenant_id, user_id, unit_id) WHERE (status = 'open'::text);\n\nCREATE UNIQUE INDEX plg_financial_cash_sessions_open_key_uq ON public.plg_financial_cash_register_sessions USING btree (tenant_id, open_idempotency_key) WHERE (open_idempotency_key IS NOT NULL);\n\nCREATE INDEX plg_financial_cash_sessions_user_idx ON public.plg_financial_cash_register_sessions USING btree (tenant_id, user_id, status);\n\nCREATE INDEX plg_financial_chart_of_accounts_tenant_owner_idx ON public.plg_financial_chart_of_accounts USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_chart_of_accounts_tenant_unit_idx ON public.plg_financial_chart_of_accounts USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_cost_centers_tenant_owner_idx ON public.plg_financial_cost_centers USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_cost_centers_tenant_unit_idx ON public.plg_financial_cost_centers USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_invoice_installments_due_idx ON public.plg_financial_invoice_installments USING btree (tenant_id, due_date);\n\nCREATE INDEX plg_financial_invoice_installments_invoice_idx ON public.plg_financial_invoice_installments USING btree (invoice_id, number);\n\nCREATE INDEX plg_financial_invoice_installments_tenant_owner_idx ON public.plg_financial_invoice_installments USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_invoice_installments_tenant_unit_idx ON public.plg_financial_invoice_installments USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_invoice_items_account_idx ON public.plg_financial_invoice_items USING btree (tenant_id, chart_account_id) WHERE (chart_account_id IS NOT NULL);\n\nCREATE INDEX plg_financial_invoice_items_cost_center_idx ON public.plg_financial_invoice_items USING btree (tenant_id, cost_center_id) WHERE (cost_center_id IS NOT NULL);\n\nCREATE INDEX plg_financial_invoices_competence_idx ON public.plg_financial_invoices USING btree (tenant_id, direction, competence_date);\n\nCREATE UNIQUE INDEX plg_financial_invoices_idempotency_uq ON public.plg_financial_invoices USING btree (tenant_id, idempotency_key) WHERE (idempotency_key IS NOT NULL);\n\nCREATE UNIQUE INDEX plg_financial_invoices_number_uq ON public.plg_financial_invoices USING btree (tenant_id, number) WHERE (number IS NOT NULL);\n\nCREATE INDEX plg_financial_invoices_order_idx ON public.plg_financial_invoices USING btree (tenant_id, order_id) WHERE (order_id IS NOT NULL);\n\nCREATE INDEX plg_financial_invoices_person_idx ON public.plg_financial_invoices USING btree (tenant_id, counterparty_person_id) WHERE (counterparty_person_id IS NOT NULL);\n\nCREATE INDEX plg_financial_invoices_tenant_owner_idx ON public.plg_financial_invoices USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_invoices_tenant_unit_idx ON public.plg_financial_invoices USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_movements_credit_idx ON public.plg_financial_movements USING btree (credit_account_id) WHERE (credit_account_id IS NOT NULL);\n\nCREATE INDEX plg_financial_movements_debit_idx ON public.plg_financial_movements USING btree (debit_account_id) WHERE (debit_account_id IS NOT NULL);\n\nCREATE UNIQUE INDEX plg_financial_movements_idempotency_uq ON public.plg_financial_movements USING btree (tenant_id, idempotency_key) WHERE (idempotency_key IS NOT NULL);\n\nCREATE INDEX plg_financial_movements_installment_idx ON public.plg_financial_movements USING btree (financial_invoice_installment_id) WHERE (financial_invoice_installment_id IS NOT NULL);\n\nCREATE INDEX plg_financial_movements_invoice_idx ON public.plg_financial_movements USING btree (financial_invoice_id) WHERE (financial_invoice_id IS NOT NULL);\n\nCREATE INDEX plg_financial_movements_tenant_owner_idx ON public.plg_financial_movements USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_movements_tenant_unit_idx ON public.plg_financial_movements USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_nfe_tenant_owner_idx ON public.plg_financial_nfe USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_nfe_tenant_unit_idx ON public.plg_financial_nfe USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_payment_method_types_tenant_owner_idx ON public.plg_financial_payment_method_types USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_payment_method_types_tenant_unit_idx ON public.plg_financial_payment_method_types USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_financial_payment_methods_tenant_owner_idx ON public.plg_financial_payment_methods USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_financial_payment_methods_tenant_unit_idx ON public.plg_financial_payment_methods USING btree (tenant_id, unit_id);\n\nCREATE UNIQUE INDEX uq_plg_financial_chart_of_accounts_code ON public.plg_financial_chart_of_accounts USING btree (tenant_id, code);\n\nCREATE UNIQUE INDEX uq_plg_financial_movements_external ON public.plg_financial_movements USING btree (tenant_id, external_source, external_id) WHERE ((external_id IS NOT NULL) AND (external_source IS NOT NULL));\n\n\n-- ── fk constraint ───────────────────────────────────────────────────────\n\nALTER TABLE ONLY public.items\n ADD CONSTRAINT items_invoice_id_fkey FOREIGN KEY (invoice_id) REFERENCES public.plg_financial_invoices(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_accounts\n ADD CONSTRAINT plg_financial_accounts_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_accounts\n ADD CONSTRAINT plg_financial_accounts_person_id_fkey FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_accounts\n ADD CONSTRAINT plg_financial_accounts_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_accounts\n ADD CONSTRAINT plg_financial_accounts_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_allocations\n ADD CONSTRAINT plg_financial_allocations_installment_id_fkey FOREIGN KEY (installment_id) REFERENCES public.plg_financial_invoice_installments(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_allocations\n ADD CONSTRAINT plg_financial_allocations_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_allocations\n ADD CONSTRAINT plg_financial_allocations_payment_movement_id_fkey FOREIGN KEY (payment_movement_id) REFERENCES public.plg_financial_movements(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_allocations\n ADD CONSTRAINT plg_financial_allocations_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_allocations\n ADD CONSTRAINT plg_financial_allocations_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_bank_accounts\n ADD CONSTRAINT plg_financial_bank_accounts_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_bank_accounts\n ADD CONSTRAINT plg_financial_bank_accounts_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_bank_accounts\n ADD CONSTRAINT plg_financial_bank_accounts_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_card_brands\n ADD CONSTRAINT plg_financial_card_brands_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_card_brands\n ADD CONSTRAINT plg_financial_card_brands_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_card_brands\n ADD CONSTRAINT plg_financial_card_brands_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_card_fee_configs\n ADD CONSTRAINT plg_financial_card_fee_configs_account_id_fkey FOREIGN KEY (account_id) REFERENCES public.plg_financial_accounts(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_card_fee_configs\n ADD CONSTRAINT plg_financial_card_fee_configs_card_brand_id_fkey FOREIGN KEY (card_brand_id) REFERENCES public.plg_financial_card_brands(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_card_fee_configs\n ADD CONSTRAINT plg_financial_card_fee_configs_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_card_fee_configs\n ADD CONSTRAINT plg_financial_card_fee_configs_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_card_fee_configs\n ADD CONSTRAINT plg_financial_card_fee_configs_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_cash_register_sessions\n ADD CONSTRAINT plg_financial_cash_register_sessions_account_fk FOREIGN KEY (account_id) REFERENCES public.plg_financial_accounts(id);\n\nALTER TABLE ONLY public.plg_financial_cash_register_sessions\n ADD CONSTRAINT plg_financial_cash_register_sessions_cofre_fk FOREIGN KEY (bank_account_id) REFERENCES public.plg_financial_accounts(id);\n\nALTER TABLE ONLY public.plg_financial_cash_register_sessions\n ADD CONSTRAINT plg_financial_cash_register_sessions_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_cash_register_sessions\n ADD CONSTRAINT plg_financial_cash_register_sessions_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_cash_register_sessions\n ADD CONSTRAINT plg_financial_cash_register_sessions_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_cash_session_reviewers\n ADD CONSTRAINT plg_financial_cash_session_reviewers_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_cash_session_reviewers\n ADD CONSTRAINT plg_financial_cash_session_reviewers_session_id_fkey FOREIGN KEY (session_id) REFERENCES public.plg_financial_cash_register_sessions(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_cash_session_reviewers\n ADD CONSTRAINT plg_financial_cash_session_reviewers_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_cash_session_reviewers\n ADD CONSTRAINT plg_financial_cash_session_reviewers_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_chart_of_accounts\n ADD CONSTRAINT plg_financial_chart_of_accounts_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_chart_of_accounts\n ADD CONSTRAINT plg_financial_chart_of_accounts_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.plg_financial_chart_of_accounts(id);\n\nALTER TABLE ONLY public.plg_financial_chart_of_accounts\n ADD CONSTRAINT plg_financial_chart_of_accounts_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_chart_of_accounts\n ADD CONSTRAINT plg_financial_chart_of_accounts_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_cost_centers\n ADD CONSTRAINT plg_financial_cost_centers_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_cost_centers\n ADD CONSTRAINT plg_financial_cost_centers_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_cost_centers\n ADD CONSTRAINT plg_financial_cost_centers_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_invoice_installments\n ADD CONSTRAINT plg_financial_invoice_installments_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_invoice_installments\n ADD CONSTRAINT plg_financial_invoice_installments_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_invoice_installments\n ADD CONSTRAINT plg_financial_invoice_installments_tenant_id_invoice_id_fkey FOREIGN KEY (tenant_id, invoice_id) REFERENCES public.plg_financial_invoices(tenant_id, id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_invoice_installments\n ADD CONSTRAINT plg_financial_invoice_installments_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_invoice_items\n ADD CONSTRAINT plg_financial_invoice_items_chart_account_id_fkey FOREIGN KEY (chart_account_id) REFERENCES public.plg_financial_chart_of_accounts(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_invoice_items\n ADD CONSTRAINT plg_financial_invoice_items_cost_center_id_fkey FOREIGN KEY (cost_center_id) REFERENCES public.plg_financial_cost_centers(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_invoice_items\n ADD CONSTRAINT plg_financial_invoice_items_item_id_fkey FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_invoice_items\n ADD CONSTRAINT plg_financial_invoice_items_parent_fk FOREIGN KEY (item_id, parent_kind) REFERENCES public.items(id, parent_kind) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_invoice_items\n ADD CONSTRAINT plg_financial_invoice_items_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_invoices\n ADD CONSTRAINT plg_financial_invoices_counterparty_person_id_fkey FOREIGN KEY (counterparty_person_id) REFERENCES public.people(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_invoices\n ADD CONSTRAINT plg_financial_invoices_order_id_fkey FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_invoices\n ADD CONSTRAINT plg_financial_invoices_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_invoices\n ADD CONSTRAINT plg_financial_invoices_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_invoices\n ADD CONSTRAINT plg_financial_invoices_tenant_order_fk FOREIGN KEY (tenant_id, order_id) REFERENCES public.orders(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_invoices\n ADD CONSTRAINT plg_financial_invoices_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_bank_account_fk FOREIGN KEY (bank_account_id) REFERENCES public.plg_financial_accounts(id);\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_cash_session_id_fkey FOREIGN KEY (cash_session_id) REFERENCES public.plg_financial_cash_register_sessions(id);\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_credit_fk FOREIGN KEY (credit_account_id) REFERENCES public.plg_financial_accounts(id);\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_debit_fk FOREIGN KEY (debit_account_id) REFERENCES public.plg_financial_accounts(id);\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_installment_fk FOREIGN KEY (financial_invoice_installment_id) REFERENCES public.plg_financial_invoice_installments(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_invoice_fk FOREIGN KEY (financial_invoice_id) REFERENCES public.plg_financial_invoices(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_invoice_id_fkey FOREIGN KEY (invoice_id) REFERENCES public.orders(id);\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_matched_movement_id_fkey FOREIGN KEY (matched_movement_id) REFERENCES public.plg_financial_movements(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_payment_method_id_fkey FOREIGN KEY (payment_method_id) REFERENCES public.plg_financial_payment_methods(id);\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_payment_method_type_id_fkey FOREIGN KEY (payment_method_type_id) REFERENCES public.plg_financial_payment_method_types(id);\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_reversed_by_fk FOREIGN KEY (reversed_by_id) REFERENCES public.plg_financial_movements(id);\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_reverses_fk FOREIGN KEY (reverses_id) REFERENCES public.plg_financial_movements(id);\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_settles_movement_id_fkey FOREIGN KEY (settles_movement_id) REFERENCES public.plg_financial_movements(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_movements\n ADD CONSTRAINT plg_financial_movements_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_nfe\n ADD CONSTRAINT plg_financial_nfe_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_nfe\n ADD CONSTRAINT plg_financial_nfe_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_nfe\n ADD CONSTRAINT plg_financial_nfe_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_payment_method_types\n ADD CONSTRAINT plg_financial_payment_method_types_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_payment_method_types\n ADD CONSTRAINT plg_financial_payment_method_types_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_payment_method_types\n ADD CONSTRAINT plg_financial_payment_method_types_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_payment_methods\n ADD CONSTRAINT plg_financial_payment_methods_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_financial_payment_methods\n ADD CONSTRAINT plg_financial_payment_methods_payment_method_type_id_fkey FOREIGN KEY (payment_method_type_id) REFERENCES public.plg_financial_payment_method_types(id);\n\nALTER TABLE ONLY public.plg_financial_payment_methods\n ADD CONSTRAINT plg_financial_payment_methods_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_financial_payment_methods\n ADD CONSTRAINT plg_financial_payment_methods_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\n\n-- ── trigger ─────────────────────────────────────────────────────────────\n\nCREATE TRIGGER items_invoice_line_immutable BEFORE UPDATE ON public.items FOR EACH ROW WHEN ((COALESCE(old.invoice_id, new.invoice_id) IS NOT NULL)) EXECUTE FUNCTION public.trg_invoice_line_immutable();\n\nCREATE TRIGGER items_invoice_line_immutable_del BEFORE DELETE ON public.items FOR EACH ROW WHEN ((old.invoice_id IS NOT NULL)) EXECUTE FUNCTION public.trg_invoice_line_immutable();\n\nCREATE TRIGGER plg_financial_accounts_updated_at BEFORE UPDATE ON public.plg_financial_accounts FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_allocations_check BEFORE INSERT OR UPDATE ON public.plg_financial_allocations FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_allocations_check();\n\nCREATE TRIGGER plg_financial_allocations_sync AFTER INSERT OR DELETE OR UPDATE ON public.plg_financial_allocations FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_allocations_sync();\n\nCREATE TRIGGER plg_financial_allocations_updated_at BEFORE UPDATE ON public.plg_financial_allocations FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_bank_accounts_forward AFTER INSERT OR UPDATE ON public.plg_financial_bank_accounts FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_bank_accounts_forward();\n\nCREATE TRIGGER plg_financial_bank_accounts_updated_at BEFORE UPDATE ON public.plg_financial_bank_accounts FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_card_brands_updated_at BEFORE UPDATE ON public.plg_financial_card_brands FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_card_fee_configs_check BEFORE INSERT OR UPDATE ON public.plg_financial_card_fee_configs FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_card_fee_configs_check();\n\nCREATE TRIGGER plg_financial_card_fee_configs_updated_at BEFORE UPDATE ON public.plg_financial_card_fee_configs FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_cash_register_sessions_owner BEFORE INSERT OR UPDATE OF user_id ON public.plg_financial_cash_register_sessions FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_cash_sessions_owner();\n\nCREATE TRIGGER plg_financial_cash_register_sessions_updated_at BEFORE UPDATE ON public.plg_financial_cash_register_sessions FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_cash_session_reviewers_updated_at BEFORE UPDATE ON public.plg_financial_cash_session_reviewers FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_chart_of_accounts_updated_at BEFORE UPDATE ON public.plg_financial_chart_of_accounts FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_cost_centers_updated_at BEFORE UPDATE ON public.plg_financial_cost_centers FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_invoice_installments_inherit BEFORE INSERT OR UPDATE OF invoice_id, tenant_id ON public.plg_financial_invoice_installments FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_installments_inherit();\n\nCREATE TRIGGER plg_financial_invoice_installments_updated_at BEFORE UPDATE ON public.plg_financial_invoice_installments FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_invoice_items_inherit BEFORE INSERT OR UPDATE OF item_id ON public.plg_financial_invoice_items FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_invoice_items_inherit();\n\nCREATE TRIGGER plg_financial_invoice_items_updated_at BEFORE UPDATE ON public.plg_financial_invoice_items FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_invoices_immutable BEFORE UPDATE ON public.plg_financial_invoices FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_invoices_immutable();\n\nCREATE TRIGGER plg_financial_invoices_updated_at BEFORE UPDATE ON public.plg_financial_invoices FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_movements_after_insert AFTER INSERT ON public.plg_financial_movements FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_movements_after_insert();\n\nCREATE TRIGGER plg_financial_movements_append_only BEFORE DELETE OR UPDATE ON public.plg_financial_movements FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_movements_append_only();\n\nCREATE TRIGGER plg_financial_movements_before_insert BEFORE INSERT ON public.plg_financial_movements FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_movements_before_insert();\n\nCREATE TRIGGER plg_financial_movements_updated_at BEFORE UPDATE ON public.plg_financial_movements FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_nfe_updated_at BEFORE UPDATE ON public.plg_financial_nfe FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_payment_method_types_updated_at BEFORE UPDATE ON public.plg_financial_payment_method_types FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_financial_payment_methods_updated_at BEFORE UPDATE ON public.plg_financial_payment_methods FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\n\n-- ── row security ────────────────────────────────────────────────────────\n\nALTER TABLE public.plg_financial_accounts ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_allocations ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_bank_accounts ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_card_brands ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_card_fee_configs ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_cash_register_sessions ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_cash_session_reviewers ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_chart_of_accounts ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_cost_centers ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_invoice_installments ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_invoice_items ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_invoices ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_movements ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_nfe ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_payment_method_types ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_financial_payment_methods ENABLE ROW LEVEL SECURITY;\n\n\n-- ── policy ──────────────────────────────────────────────────────────────\n\nCREATE POLICY plg_financial_accounts_authz_delete ON public.plg_financial_accounts FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_accounts.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.account'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.account'::text, plg_financial_accounts.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_accounts.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_accounts_authz_insert ON public.plg_financial_accounts FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_accounts.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_accounts_authz_select ON public.plg_financial_accounts FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_accounts.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.account'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.account'::text, plg_financial_accounts.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_accounts.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_accounts_authz_update ON public.plg_financial_accounts FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_accounts.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.account'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.account'::text, plg_financial_accounts.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_accounts.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_accounts.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_accounts.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_allocations_authz_delete ON public.plg_financial_allocations FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_allocations.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.allocation'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.allocation'::text, plg_financial_allocations.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_allocations.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_allocations.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_allocations_authz_insert ON public.plg_financial_allocations FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_allocations.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_allocations.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_allocations_authz_select ON public.plg_financial_allocations FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_allocations.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.allocation'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.allocation'::text, plg_financial_allocations.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_allocations.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_allocations.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_allocations_authz_update ON public.plg_financial_allocations FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_allocations.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.allocation'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.allocation'::text, plg_financial_allocations.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_allocations.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_allocations.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_allocations.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_allocations.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_bank_accounts_authz_delete ON public.plg_financial_bank_accounts FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_bank_accounts.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.bank_account'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.bank_account'::text, plg_financial_bank_accounts.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_bank_accounts.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_bank_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_bank_accounts_authz_insert ON public.plg_financial_bank_accounts FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_bank_accounts.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_bank_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_bank_accounts_authz_select ON public.plg_financial_bank_accounts FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_bank_accounts.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.bank_account'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.bank_account'::text, plg_financial_bank_accounts.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_bank_accounts.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_bank_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_bank_accounts_authz_update ON public.plg_financial_bank_accounts FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_bank_accounts.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.bank_account'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.bank_account'::text, plg_financial_bank_accounts.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_bank_accounts.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_bank_accounts.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_bank_accounts.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_bank_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_card_brands_authz_delete ON public.plg_financial_card_brands FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_card_brands.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.card_brand'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.card_brand'::text, plg_financial_card_brands.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_card_brands.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_card_brands.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_card_brands_authz_insert ON public.plg_financial_card_brands FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_card_brands.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_card_brands.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_card_brands_authz_select ON public.plg_financial_card_brands FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_card_brands.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.card_brand'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.card_brand'::text, plg_financial_card_brands.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_card_brands.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_card_brands.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_card_brands_authz_update ON public.plg_financial_card_brands FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_card_brands.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.card_brand'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.card_brand'::text, plg_financial_card_brands.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_card_brands.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_card_brands.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_card_brands.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_card_brands.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_card_fee_configs_authz_delete ON public.plg_financial_card_fee_configs FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_card_fee_configs.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.card_fee_config'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.card_fee_config'::text, plg_financial_card_fee_configs.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_card_fee_configs.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_card_fee_configs.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_card_fee_configs_authz_insert ON public.plg_financial_card_fee_configs FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_card_fee_configs.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_card_fee_configs.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_card_fee_configs_authz_select ON public.plg_financial_card_fee_configs FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_card_fee_configs.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.card_fee_config'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.card_fee_config'::text, plg_financial_card_fee_configs.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_card_fee_configs.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_card_fee_configs.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_card_fee_configs_authz_update ON public.plg_financial_card_fee_configs FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_card_fee_configs.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.card_fee_config'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.card_fee_config'::text, plg_financial_card_fee_configs.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_card_fee_configs.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_card_fee_configs.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_card_fee_configs.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_card_fee_configs.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cash_register_sessions_authz_delete ON public.plg_financial_cash_register_sessions FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cash_register_sessions.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.cash_session'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.cash_session'::text, plg_financial_cash_register_sessions.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_cash_register_sessions.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_cash_register_sessions.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cash_register_sessions_authz_insert ON public.plg_financial_cash_register_sessions FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cash_register_sessions.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_cash_register_sessions.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cash_register_sessions_authz_select ON public.plg_financial_cash_register_sessions FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cash_register_sessions.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.cash_session'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.cash_session'::text, plg_financial_cash_register_sessions.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_cash_register_sessions.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_cash_register_sessions.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cash_register_sessions_authz_update ON public.plg_financial_cash_register_sessions FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cash_register_sessions.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.cash_session'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.cash_session'::text, plg_financial_cash_register_sessions.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_cash_register_sessions.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_cash_register_sessions.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cash_register_sessions.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_cash_register_sessions.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cash_session_reviewers_authz_delete ON public.plg_financial_cash_session_reviewers FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cash_session_reviewers.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.cash_session_review'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.cash_session_review'::text, plg_financial_cash_session_reviewers.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_cash_session_reviewers.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_cash_session_reviewers.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cash_session_reviewers_authz_insert ON public.plg_financial_cash_session_reviewers FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cash_session_reviewers.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_cash_session_reviewers.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cash_session_reviewers_authz_select ON public.plg_financial_cash_session_reviewers FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cash_session_reviewers.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.cash_session_review'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.cash_session_review'::text, plg_financial_cash_session_reviewers.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_cash_session_reviewers.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_cash_session_reviewers.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cash_session_reviewers_authz_update ON public.plg_financial_cash_session_reviewers FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cash_session_reviewers.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.cash_session_review'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.cash_session_review'::text, plg_financial_cash_session_reviewers.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_cash_session_reviewers.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_cash_session_reviewers.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cash_session_reviewers.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_cash_session_reviewers.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_chart_of_accounts_authz_delete ON public.plg_financial_chart_of_accounts FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_chart_of_accounts.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.chart_of_account'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.chart_of_account'::text, plg_financial_chart_of_accounts.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_chart_of_accounts.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_chart_of_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_chart_of_accounts_authz_insert ON public.plg_financial_chart_of_accounts FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_chart_of_accounts.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_chart_of_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_chart_of_accounts_authz_select ON public.plg_financial_chart_of_accounts FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_chart_of_accounts.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.chart_of_account'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.chart_of_account'::text, plg_financial_chart_of_accounts.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_chart_of_accounts.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_chart_of_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_chart_of_accounts_authz_update ON public.plg_financial_chart_of_accounts FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_chart_of_accounts.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.chart_of_account'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.chart_of_account'::text, plg_financial_chart_of_accounts.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_chart_of_accounts.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_chart_of_accounts.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_chart_of_accounts.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_chart_of_accounts.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cost_centers_authz_delete ON public.plg_financial_cost_centers FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cost_centers.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.cost_center'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.cost_center'::text, plg_financial_cost_centers.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_cost_centers.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_cost_centers.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cost_centers_authz_insert ON public.plg_financial_cost_centers FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cost_centers.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_cost_centers.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cost_centers_authz_select ON public.plg_financial_cost_centers FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cost_centers.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.cost_center'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.cost_center'::text, plg_financial_cost_centers.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_cost_centers.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_cost_centers.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_cost_centers_authz_update ON public.plg_financial_cost_centers FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cost_centers.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.cost_center'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.cost_center'::text, plg_financial_cost_centers.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_cost_centers.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_cost_centers.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_cost_centers.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_cost_centers.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_invoice_installments_authz_delete ON public.plg_financial_invoice_installments FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_invoice_installments.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.invoice_installment'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.invoice_installment'::text, plg_financial_invoice_installments.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_invoice_installments.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_invoice_installments.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_invoice_installments_authz_insert ON public.plg_financial_invoice_installments FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_invoice_installments.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_invoice_installments.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_invoice_installments_authz_select ON public.plg_financial_invoice_installments FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_invoice_installments.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.invoice_installment'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.invoice_installment'::text, plg_financial_invoice_installments.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_invoice_installments.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_invoice_installments.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_invoice_installments_authz_update ON public.plg_financial_invoice_installments FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_invoice_installments.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.invoice_installment'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.invoice_installment'::text, plg_financial_invoice_installments.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_invoice_installments.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_invoice_installments.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_invoice_installments.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_invoice_installments.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_invoice_items_authz_delete ON public.plg_financial_invoice_items FOR DELETE TO authenticated USING (((item_id IN ( SELECT i.id\n FROM public.items i)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_invoice_items.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_invoice_items_authz_insert ON public.plg_financial_invoice_items FOR INSERT TO authenticated WITH CHECK (((item_id IN ( SELECT i.id\n FROM public.items i)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_invoice_items.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_invoice_items_authz_select ON public.plg_financial_invoice_items FOR SELECT TO authenticated USING ((item_id IN ( SELECT i.id\n FROM public.items i)));\n\nCREATE POLICY plg_financial_invoice_items_authz_update ON public.plg_financial_invoice_items FOR UPDATE TO authenticated USING (((item_id IN ( SELECT i.id\n FROM public.items i)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_invoice_items.unit_id) AS has_permission))) WITH CHECK (((item_id IN ( SELECT i.id\n FROM public.items i)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_invoice_items.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_invoices_authz_delete ON public.plg_financial_invoices FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_invoices.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.invoice'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.invoice'::text, plg_financial_invoices.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_invoices.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_invoices.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_invoices_authz_insert ON public.plg_financial_invoices FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_invoices.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_invoices.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_invoices_authz_select ON public.plg_financial_invoices FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_invoices.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.invoice'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.invoice'::text, plg_financial_invoices.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_invoices.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_invoices.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_invoices_authz_update ON public.plg_financial_invoices FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_invoices.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.invoice'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.invoice'::text, plg_financial_invoices.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_invoices.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_invoices.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_invoices.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_invoices.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_movements_authz_delete ON public.plg_financial_movements FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_movements.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.movement'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.movement'::text, plg_financial_movements.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_movements.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_movements.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_movements_authz_insert ON public.plg_financial_movements FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_movements.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_movements.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_movements_authz_select ON public.plg_financial_movements FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_movements.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.movement'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.movement'::text, plg_financial_movements.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_movements.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_movements.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_movements_authz_update ON public.plg_financial_movements FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_movements.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.movement'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.movement'::text, plg_financial_movements.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_movements.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_movements.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_movements.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_movements.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_nfe_authz_delete ON public.plg_financial_nfe FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_nfe.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.nfe'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.nfe'::text, plg_financial_nfe.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_nfe.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_nfe.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_nfe_authz_insert ON public.plg_financial_nfe FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_nfe.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_nfe.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_nfe_authz_select ON public.plg_financial_nfe FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_nfe.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.nfe'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.nfe'::text, plg_financial_nfe.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_nfe.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_nfe.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_nfe_authz_update ON public.plg_financial_nfe FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_nfe.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.nfe'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.nfe'::text, plg_financial_nfe.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_nfe.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_nfe.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_nfe.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_nfe.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_payment_method_types_authz_delete ON public.plg_financial_payment_method_types FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_payment_method_types.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.payment_method_type'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.payment_method_type'::text, plg_financial_payment_method_types.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_payment_method_types.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_payment_method_types.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_payment_method_types_authz_insert ON public.plg_financial_payment_method_types FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_payment_method_types.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_payment_method_types.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_payment_method_types_authz_select ON public.plg_financial_payment_method_types FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_payment_method_types.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.payment_method_type'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.payment_method_type'::text, plg_financial_payment_method_types.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_payment_method_types.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_payment_method_types.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_payment_method_types_authz_update ON public.plg_financial_payment_method_types FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_payment_method_types.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.payment_method_type'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.payment_method_type'::text, plg_financial_payment_method_types.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_payment_method_types.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_payment_method_types.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_payment_method_types.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_payment_method_types.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_payment_methods_authz_delete ON public.plg_financial_payment_methods FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_payment_methods.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.payment_method'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.payment_method'::text, plg_financial_payment_methods.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_payment_methods.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.delete'::text, plg_financial_payment_methods.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_payment_methods_authz_insert ON public.plg_financial_payment_methods FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_payment_methods.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.create'::text, plg_financial_payment_methods.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_payment_methods_authz_select ON public.plg_financial_payment_methods FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_payment_methods.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.payment_method'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.payment_method'::text, plg_financial_payment_methods.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_payment_methods.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.read'::text, plg_financial_payment_methods.unit_id) AS has_permission)));\n\nCREATE POLICY plg_financial_payment_methods_authz_update ON public.plg_financial_payment_methods FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_payment_methods.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('financial.payment_method'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('financial.payment_method'::text, plg_financial_payment_methods.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('financial.read_all'::text, plg_financial_payment_methods.unit_id) AS has_permission)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_payment_methods.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_financial_payment_methods.unit_id) AS has_unit)) AND ( SELECT app.has_permission('financial.edit'::text, plg_financial_payment_methods.unit_id) AS has_permission)));\n\n\n-- ── acl ─────────────────────────────────────────────────────────────────\n\nREVOKE ALL ON FUNCTION public.agent_financial_mark_payment_received(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.agent_financial_mark_payment_received(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) TO authenticated;\nGRANT ALL ON FUNCTION public.agent_financial_mark_payment_received(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_cancel_invoice(p_invoice_id uuid, p_reason text, p_idempotency_key text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_cancel_invoice(p_invoice_id uuid, p_reason text, p_idempotency_key text) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_cancel_invoice(p_invoice_id uuid, p_reason text, p_idempotency_key text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_cancel_paid_item(p_order_item_id uuid, p_reason text, p_idempotency_key text, p_amount numeric) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_cancel_paid_item(p_order_item_id uuid, p_reason text, p_idempotency_key text, p_amount numeric) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_cancel_paid_item(p_order_item_id uuid, p_reason text, p_idempotency_key text, p_amount numeric) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_cash_session_close(p_counted_cash numeric, p_counted_card numeric, p_idempotency_key text, p_notes text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_cash_session_close(p_counted_cash numeric, p_counted_card numeric, p_idempotency_key text, p_notes text) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_cash_session_close(p_counted_cash numeric, p_counted_card numeric, p_idempotency_key text, p_notes text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_cash_session_open(p_cofre_account_id uuid, p_opening_amount numeric, p_idempotency_key text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_cash_session_open(p_cofre_account_id uuid, p_opening_amount numeric, p_idempotency_key text) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_cash_session_open(p_cofre_account_id uuid, p_opening_amount numeric, p_idempotency_key text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_create_invoice(p_direction text, p_total numeric, p_counterparty_person_id uuid, p_order_id uuid, p_unit_id uuid, p_installments jsonb, p_competence_date date, p_description text, p_notes text, p_metadata jsonb, p_idempotency_key text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_create_invoice(p_direction text, p_total numeric, p_counterparty_person_id uuid, p_order_id uuid, p_unit_id uuid, p_installments jsonb, p_competence_date date, p_description text, p_notes text, p_metadata jsonb, p_idempotency_key text) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_create_invoice(p_direction text, p_total numeric, p_counterparty_person_id uuid, p_order_id uuid, p_unit_id uuid, p_installments jsonb, p_competence_date date, p_description text, p_notes text, p_metadata jsonb, p_idempotency_key text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_create_payable(p_description text, p_total numeric, p_due_date date, p_person_id uuid, p_unit_id uuid, p_chart_account_id uuid, p_cost_center_id uuid, p_installments jsonb, p_competence_date date, p_idempotency_key text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_create_payable(p_description text, p_total numeric, p_due_date date, p_person_id uuid, p_unit_id uuid, p_chart_account_id uuid, p_cost_center_id uuid, p_installments jsonb, p_competence_date date, p_idempotency_key text) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_create_payable(p_description text, p_total numeric, p_due_date date, p_person_id uuid, p_unit_id uuid, p_chart_account_id uuid, p_cost_center_id uuid, p_installments jsonb, p_competence_date date, p_idempotency_key text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_credit_customer(p_person_id uuid, p_amount numeric, p_to_account_id uuid, p_method text, p_reason text, p_notes text, p_idempotency_key text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_credit_customer(p_person_id uuid, p_amount numeric, p_to_account_id uuid, p_method text, p_reason text, p_notes text, p_idempotency_key text) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_credit_customer(p_person_id uuid, p_amount numeric, p_to_account_id uuid, p_method text, p_reason text, p_notes text, p_idempotency_key text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_invoice_from_lines(p_order_id uuid, p_lines jsonb, p_direction text, p_person_id uuid, p_installments jsonb, p_competence_date date, p_idempotency_key text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_invoice_from_lines(p_order_id uuid, p_lines jsonb, p_direction text, p_person_id uuid, p_installments jsonb, p_competence_date date, p_idempotency_key text) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_invoice_from_lines(p_order_id uuid, p_lines jsonb, p_direction text, p_person_id uuid, p_installments jsonb, p_competence_date date, p_idempotency_key text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_receive_payment(p_installment_id uuid, p_amount numeric, p_account_id uuid, p_method text, p_cash_session_id uuid, p_card_brand text, p_card_installments integer, p_nsu text, p_payment_date date, p_fee_amount numeric, p_notes text, p_idempotency_key text, p_metadata jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_receive_payment(p_installment_id uuid, p_amount numeric, p_account_id uuid, p_method text, p_cash_session_id uuid, p_card_brand text, p_card_installments integer, p_nsu text, p_payment_date date, p_fee_amount numeric, p_notes text, p_idempotency_key text, p_metadata jsonb) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_receive_payment(p_installment_id uuid, p_amount numeric, p_account_id uuid, p_method text, p_cash_session_id uuid, p_card_brand text, p_card_installments integer, p_nsu text, p_payment_date date, p_fee_amount numeric, p_notes text, p_idempotency_key text, p_metadata jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_retire_bill_mirror(p_execute boolean) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_retire_bill_mirror(p_execute boolean) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_reverse_movement(p_movement_id uuid, p_reason text, p_idempotency_key text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_reverse_movement(p_movement_id uuid, p_reason text, p_idempotency_key text) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_reverse_movement(p_movement_id uuid, p_reason text, p_idempotency_key text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_set_opening_balance(p_account_id uuid, p_amount numeric, p_date date, p_idempotency_key text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_set_opening_balance(p_account_id uuid, p_amount numeric, p_date date, p_idempotency_key text) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_set_opening_balance(p_account_id uuid, p_amount numeric, p_date date, p_idempotency_key text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.financial_transfer(p_from_account_id uuid, p_to_account_id uuid, p_amount numeric, p_date date, p_method text, p_notes text, p_idempotency_key text, p_metadata jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_transfer(p_from_account_id uuid, p_to_account_id uuid, p_amount numeric, p_date date, p_method text, p_notes text, p_idempotency_key text, p_metadata jsonb) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_transfer(p_from_account_id uuid, p_to_account_id uuid, p_amount numeric, p_date date, p_method text, p_notes text, p_idempotency_key text, p_metadata jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.fn_invoice_from_order(p_order_id uuid, p_due_date date, p_status text, p_installments integer, p_direction text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.fn_invoice_from_order(p_order_id uuid, p_due_date date, p_status text, p_installments integer, p_direction text) TO authenticated;\nGRANT ALL ON FUNCTION public.fn_invoice_from_order(p_order_id uuid, p_due_date date, p_status text, p_installments integer, p_direction text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.fn_invoice_from_order_internal(p_order_id uuid, p_due_date date, p_status text, p_installments integer, p_direction text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.fn_invoice_from_order_internal(p_order_id uuid, p_due_date date, p_status text, p_installments integer, p_direction text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.fn_order_to_cash(p_kind text, p_lines jsonb, p_party_id uuid, p_unit_id uuid, p_status text, p_channel text, p_assignee_id uuid, p_installments integer, p_due_date date, p_direction text, p_metadata jsonb, p_appointment_id uuid, p_idempotency_key text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.fn_order_to_cash(p_kind text, p_lines jsonb, p_party_id uuid, p_unit_id uuid, p_status text, p_channel text, p_assignee_id uuid, p_installments integer, p_due_date date, p_direction text, p_metadata jsonb, p_appointment_id uuid, p_idempotency_key text) TO authenticated;\nGRANT ALL ON FUNCTION public.fn_order_to_cash(p_kind text, p_lines jsonb, p_party_id uuid, p_unit_id uuid, p_status text, p_channel text, p_assignee_id uuid, p_installments integer, p_due_date date, p_direction text, p_metadata jsonb, p_appointment_id uuid, p_idempotency_key text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.next_sequence_internal(p_tenant_id uuid, p_kind text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.next_sequence_internal(p_tenant_id uuid, p_kind text) TO service_role;\n\nGRANT ALL ON FUNCTION public.plg_financial_account_kind_of(p_account_type text) TO authenticated;\nGRANT ALL ON FUNCTION public.plg_financial_account_kind_of(p_account_type text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_audit(p_tenant uuid, p_action text, p_entity_type text, p_entity_id text, p_metadata jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_audit(p_tenant uuid, p_action text, p_entity_type text, p_entity_id text, p_metadata jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_backfill_ledger() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_backfill_ledger() TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_claim_idempotency(p_tenant uuid, p_key text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_claim_idempotency(p_tenant uuid, p_key text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_installment_paid(p_installment uuid) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_installment_paid(p_installment uuid) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_legacy_legs(p_tenant uuid, p_kind text, p_direction text, p_bank_account_id uuid, p_cash_session_id uuid, p_invoice_id uuid, p_metadata jsonb, OUT o_debit uuid, OUT o_credit uuid, OUT o_mirror boolean) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_legacy_legs(p_tenant uuid, p_kind text, p_direction text, p_bank_account_id uuid, p_cash_session_id uuid, p_invoice_id uuid, p_metadata jsonb, OUT o_debit uuid, OUT o_credit uuid, OUT o_mirror boolean) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_migrate_legacy_invoices() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_migrate_legacy_invoices() TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_next_invoice_number(p_tenant_id uuid, p_direction text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_next_invoice_number(p_tenant_id uuid, p_direction text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_open_session_of(p_tenant uuid, p_user uuid, p_unit uuid) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_open_session_of(p_tenant uuid, p_user uuid, p_unit uuid) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_person_account(p_tenant uuid, p_person uuid) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_person_account(p_tenant uuid, p_person uuid) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_require_permission(p_perm text, p_unit uuid) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_require_permission(p_perm text, p_unit uuid) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_require_tenant() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_require_tenant() TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_sync_mirror(p_installment uuid) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_sync_mirror(p_installment uuid) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_system_account(p_tenant uuid, p_code text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_financial_system_account(p_tenant uuid, p_code text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_invoice_line_immutable() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_invoice_line_immutable() TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_plg_financial_allocations_check() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_plg_financial_allocations_check() TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_plg_financial_allocations_sync() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_plg_financial_allocations_sync() TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_plg_financial_bank_accounts_forward() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_plg_financial_bank_accounts_forward() TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_plg_financial_card_fee_configs_check() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_plg_financial_card_fee_configs_check() TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_plg_financial_cash_sessions_owner() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_plg_financial_cash_sessions_owner() TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_plg_financial_installments_inherit() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_plg_financial_installments_inherit() TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_plg_financial_invoice_items_inherit() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_plg_financial_invoice_items_inherit() TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_plg_financial_invoices_immutable() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_plg_financial_invoices_immutable() TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_plg_financial_movements_after_insert() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_plg_financial_movements_after_insert() TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_plg_financial_movements_append_only() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_plg_financial_movements_append_only() TO service_role;\n\nREVOKE ALL ON FUNCTION public.trg_plg_financial_movements_before_insert() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.trg_plg_financial_movements_before_insert() TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_allocations TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_allocations TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_invoice_installments TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_invoice_installments TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_invoices TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_invoices TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_movements TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_movements TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_financial_installment_balances TO anon;\nGRANT ALL ON TABLE public.v_financial_installment_balances TO authenticated;\nGRANT ALL ON TABLE public.v_financial_installment_balances TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_financial_invoice_balances TO anon;\nGRANT ALL ON TABLE public.v_financial_invoice_balances TO authenticated;\nGRANT ALL ON TABLE public.v_financial_invoice_balances TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_invoice_balances TO anon;\nGRANT ALL ON TABLE public.v_invoice_balances TO authenticated;\nGRANT ALL ON TABLE public.v_invoice_balances TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_accounts TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_accounts TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_bank_accounts TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_bank_accounts TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_card_brands TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_card_brands TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_card_fee_configs TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_card_fee_configs TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_cash_register_sessions TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_cash_register_sessions TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_cash_session_reviewers TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_cash_session_reviewers TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_chart_of_accounts TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_chart_of_accounts TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_cost_centers TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_cost_centers TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_financial_invoice_items TO anon;\nGRANT ALL ON TABLE public.plg_financial_invoice_items TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_invoice_items TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_nfe TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_nfe TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_payment_method_types TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_payment_method_types TO service_role;\n\nGRANT ALL ON TABLE public.plg_financial_payment_methods TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_payment_methods TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_financial_rep_receivables TO anon;\nGRANT ALL ON TABLE public.plg_financial_rep_receivables TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_rep_receivables TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_bill_mirror_writers TO anon;\nGRANT ALL ON TABLE public.v_bill_mirror_writers TO authenticated;\nGRANT ALL ON TABLE public.v_bill_mirror_writers TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_bill_mirror_pending TO anon;\nGRANT ALL ON TABLE public.v_bill_mirror_pending TO authenticated;\nGRANT ALL ON TABLE public.v_bill_mirror_pending TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_bill_mirror_readers TO anon;\nGRANT ALL ON TABLE public.v_bill_mirror_readers TO authenticated;\nGRANT ALL ON TABLE public.v_bill_mirror_readers TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_financial_account_balances TO anon;\nGRANT ALL ON TABLE public.v_financial_account_balances TO authenticated;\nGRANT ALL ON TABLE public.v_financial_account_balances TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_financial_card_receivables TO anon;\nGRANT ALL ON TABLE public.v_financial_card_receivables TO authenticated;\nGRANT ALL ON TABLE public.v_financial_card_receivables TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_financial_customer_credit TO anon;\nGRANT ALL ON TABLE public.v_financial_customer_credit TO authenticated;\nGRANT ALL ON TABLE public.v_financial_customer_credit TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_financial_invoice_lines TO anon;\nGRANT ALL ON TABLE public.v_financial_invoice_lines TO authenticated;\nGRANT ALL ON TABLE public.v_financial_invoice_lines TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_financial_ledger TO anon;\nGRANT ALL ON TABLE public.v_financial_ledger TO authenticated;\nGRANT ALL ON TABLE public.v_financial_ledger TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_financial_person_payments TO anon;\nGRANT ALL ON TABLE public.v_financial_person_payments TO authenticated;\nGRANT ALL ON TABLE public.v_financial_person_payments TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_order_billing TO anon;\nGRANT ALL ON TABLE public.v_order_billing TO authenticated;\nGRANT ALL ON TABLE public.v_order_billing TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_order_payment_status TO anon;\nGRANT ALL ON TABLE public.v_order_payment_status TO authenticated;\nGRANT ALL ON TABLE public.v_order_payment_status TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_orders_sem_invoice TO anon;\nGRANT ALL ON TABLE public.v_orders_sem_invoice TO authenticated;\nGRANT ALL ON TABLE public.v_orders_sem_invoice TO service_role;\n\n\n-- ── comment ─────────────────────────────────────────────────────────────\n\nCOMMENT ON FUNCTION public.agent_financial_mark_payment_received(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) IS 'The agent receives a payment (009), settling INSTALMENTS through financial_receive_payment instead of flipping legacy bill mirrors. Reader 2 of the 4 that kept movement_kind=''bill'' alive (PRD 04).';\n\nCOMMENT ON FUNCTION public.financial_create_payable(p_description text, p_total numeric, p_due_date date, p_person_id uuid, p_unit_id uuid, p_chart_account_id uuid, p_cost_center_id uuid, p_installments jsonb, p_competence_date date, p_idempotency_key text) IS 'A payable with no sale (PRD 04): the rent. A line with a description and no product, on an invoice with no order — which is the thing 100% of today''s invoices could not be.';\n\nCOMMENT ON FUNCTION public.financial_invoice_from_lines(p_order_id uuid, p_lines jsonb, p_direction text, p_person_id uuid, p_installments jsonb, p_competence_date date, p_idempotency_key text) IS 'Bill part of an order (PRD 04). Five people at one table is five calls to this: each raises its own invoice with its own lines, each line associated back to the order line with its own quantity, and the association guard refuses a sixth invoice that would bill more than was ordered.';\n\nCOMMENT ON FUNCTION public.financial_retire_bill_mirror(p_execute boolean) IS 'Removes the legacy bill mirror, and REFUSES while anything still WRITES one or while a bill row is not a mirror (PRD 04/10). Never a side effect of an apply: the migration that calls it reports and moves on when the census is not clean.';\n\nCOMMENT ON FUNCTION public.fn_invoice_from_order_internal(p_order_id uuid, p_due_date date, p_status text, p_installments integer, p_direction text) IS 'Promotes a Venda into an invoice with N instalments (018), and — since PRD 04 — writes NO legacy bill mirror. The mirror''s shape is served by v_financial_bills, derived; what is left of the old rows is removed by the guarded retirement in 041.';\n\nCOMMENT ON FUNCTION public.fn_order_to_cash(p_kind text, p_lines jsonb, p_party_id uuid, p_unit_id uuid, p_status text, p_channel text, p_assignee_id uuid, p_installments integer, p_due_date date, p_direction text, p_metadata jsonb, p_appointment_id uuid, p_idempotency_key text) IS 'A booking becomes a Venda and a Título in ONE call and ONE transaction (PRD 06). Either both are born or neither is — which is what the agenda''s two browser calls could not promise.';\n\nCOMMENT ON COLUMN public.plg_financial_movements.fee_amount IS 'Processing/MDR fee deducted at settlement. Net cash impact = paid_amount - fee_amount (credit). v1: same-day net, no D+N settlement-date modeling.';\n\nCOMMENT ON COLUMN public.plg_financial_movements.external_id IS 'Provider-unique id of the imported bank line. Forms the import idempotency key with external_source.';\n\nCOMMENT ON COLUMN public.plg_financial_movements.external_source IS 'Provider of an imported bank-statement line (e.g. plugbank, inter). NULL for app-native movements.';\n\nCOMMENT ON COLUMN public.plg_financial_movements.settles_movement_id IS 'The movement_kind=''bill'' row this payment settles. Not matched_movement_id — that is 007 reconciliation and unreconcileMovement() nulls it.';\n\nCOMMENT ON COLUMN public.plg_financial_chart_of_accounts.account_type IS 'asset | liability | equity | revenue | expense. Unconstrained on purpose — see 013 header.';\n\nCOMMENT ON TABLE public.plg_financial_invoice_items IS 'What only an invoice line has (PRD 04): chart of accounts, cost centre, tax and CFOP. Tied to public.items by a COMPOSITE foreign key on (item_id, parent_kind), so the database refuses this extension over a delivery line — which is why a delivery line carries no tax column and no cross-kind nulls exist.';\n\nCOMMENT ON VIEW public.v_bill_mirror_writers IS 'Who still writes a movement_kind=''bill'' row (044). It scanned only public and app until then, which is why it reported zero while migration.upsert_fin_movement was landing every V1 obligation as one. A census that cannot see a writer is a census that says a live thing has fallen.';\n\nCOMMENT ON VIEW public.v_bill_mirror_readers IS 'Everything IN THE DATABASE that mentions the legacy bill mirror (PRD 04/10) — informational. What the retirement blocks on is v_bill_mirror_writers: the rule is that a mirror falls when nobody WRITES to it, and a mention that only guards against a pool which has one is a reader.';\n\nCOMMENT ON VIEW public.v_financial_invoice_lines IS 'What an invoice charged (PRD 04): its lines, their chart-of-accounts and cost-centre, and what each was billed FROM. The origin is an order line — a row of public.items carrying order_id since 187.';\n\nCOMMENT ON VIEW public.v_financial_person_payments IS 'Payments attributable to a Person, from the two-leg ledger (#285). Replaces the admin timeline read of public.transactions, the legacy archetype the ledger superseded.';\n\nCOMMENT ON VIEW public.v_order_billing IS 'Order total against what has actually been invoiced (PRD 04). A table of five with five invoices reads as five rows summing to the order — the acceptance case, as a query.';\n\nCOMMENT ON VIEW public.v_order_payment_status IS 'How much of an order has been paid, straight from the installment balances (PRD 04/06). The agenda''s payment badge reads this instead of movement_kind=''bill'' — reader 3 of the 4 that kept the legacy mirror alive.';\n\nCOMMENT ON VIEW public.v_orders_sem_invoice IS 'Sales that COMPLETED and produced no invoice (PRD 02/04 acceptance). Empty is the bar. Two exclusions, both deliberate: a sale still open is billed when it closes, and an unbilled appointment is legitimate — what is not legitimate is a completed one, which v_appointments_sem_order counts separately.';\n\n\n-- ── default privileges: restore ─────────────────────────────────────────\n-- Put back, so the NEXT thing installed inherits what the chain had.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEND $dp$;\n`\n\nexport const MIGRATION_001_THE_COMANDA_BECOMES_A_RECEIVABLE_WHEN_IT_CLOSES = `-- ---------------------------------------------------------------------------\n-- 001 — a comanda vira conta a receber quando FECHA.\n--\n-- Medido antes de escrever: \\`dine_in\\` tinha 6 pedidos e ZERO faturas, enquanto\n-- \\`sale\\` tinha 7 e 6. Não era chamada esquecida — o gatilho que levanta o\n-- recebível chama-se \\`plg_shop_order_extensions_raise_receivable\\` e está\n-- pendurado na tabela de extensão da LOJA. Uma comanda não tem linha de\n-- extensão de loja, então nada disparava. O restaurante inteiro passava ao\n-- largo do Financeiro.\n--\n-- QUANDO, e por que não na abertura. Na loja o pedido nasce fechado, então\n-- faturar no checkout é certo. Uma comanda nasce aberta e cresce por uma hora:\n-- faturar na abertura produziria uma fatura errada a cada item novo, e depois\n-- uma correção para cada uma. Fatura-se no fechamento, e a comanda aberta\n-- aparece como serviço em andamento, não como recebível.\n--\n-- POR QUE UMA FUNÇÃO NOVA, e não uma das cinco que já existem:\n--\n-- · \\`financial_create_invoice\\` é a moderna e a correta em quase tudo — não\n-- mexe no \\`reference_number\\`, tem idempotência de verdade. Mas exige\n-- \\`financial.create\\` de QUEM CHAMA, e quem fecha uma comanda é o garçom.\n-- Levantar o recebível é consequência da venda, não um ato de escrituração\n-- dele; exigir a permissão faria o fechamento falhar para a pessoa certa.\n--\n-- · \\`fn_invoice_from_order_internal\\`, que a loja usa, não exige permissão —\n-- mas SOBRESCREVE \\`orders.reference_number\\` com o número da fatura. Isso é\n-- coerente com a origem dela (promover um orçamento em fatura, no lugar) e\n-- é destrutivo aqui: a comanda PED-000003 viraria REC-000004 e o salão\n-- perderia o número pelo qual a chama.\n--\n-- Então: a idempotência e o não-mexer-no-número da primeira, sem a exigência de\n-- permissão da segunda. Uma linha de código a mais, e o número da comanda\n-- sobrevive.\n-- ---------------------------------------------------------------------------\n\nCREATE OR REPLACE FUNCTION public.order_raise_receivable(\n p_order_id uuid,\n p_due_date date DEFAULT NULL\n) RETURNS uuid\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_order public.orders%ROWTYPE;\n v_invoice_id uuid;\n v_number text;\n v_summary text;\n v_total numeric;\nBEGIN\n SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;\n IF NOT FOUND THEN RETURN NULL; END IF;\n\n -- Já faturada é o caso comum numa reexecução, não um erro.\n SELECT id INTO v_invoice_id FROM public.plg_financial_invoices\n WHERE order_id = p_order_id AND cancelled_at IS NULL LIMIT 1;\n IF v_invoice_id IS NOT NULL THEN RETURN v_invoice_id; END IF;\n\n -- Comanda sem nada dentro não é conta a receber de zero real; é uma comanda\n -- que ninguém usou. Fechar uma dessas não deve sujar o Financeiro.\n v_total := round(coalesce(v_order.total, 0), 2);\n IF v_total <= 0 THEN RETURN NULL; END IF;\n\n SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary\n FROM public.items WHERE order_id = p_order_id;\n\n v_number := public.plg_financial_next_invoice_number(v_order.tenant_id, 'receivable');\n\n INSERT INTO public.plg_financial_invoices\n (tenant_id, unit_id, direction, counterparty_person_id, order_id, number,\n competence_date, issue_date, total, description, metadata, created_by)\n VALUES\n (v_order.tenant_id, v_order.unit_id, 'receivable', v_order.party_id, p_order_id, v_number,\n coalesce(v_order.created_at::date, current_date), current_date,\n v_total, nullif(v_summary, ''),\n jsonb_build_object('source', 'order_raise_receivable', 'orderKind', v_order.kind),\n auth.uid())\n RETURNING id INTO v_invoice_id;\n\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount)\n VALUES (v_order.tenant_id, v_order.unit_id, v_invoice_id, 1,\n coalesce(p_due_date, current_date), v_total);\n\n -- O pedido registra que foi faturado, com as MESMAS chaves que os leitores\n -- antigos usam — e sem tocar em \\`reference_number\\`, que é o nome da comanda.\n UPDATE public.orders SET\n stage = 'invoiced',\n direction = 'credit',\n metadata = coalesce(metadata, '{}'::jsonb)\n || jsonb_build_object('invoiceNumber', v_number, 'installmentCount', 1),\n updated_at = now()\n WHERE id = p_order_id;\n\n RETURN v_invoice_id;\nEND $$;\n\nCOMMENT ON FUNCTION public.order_raise_receivable(uuid, date) IS\n 'Levanta a conta a receber de um pedido, sem exigir permissão financeira de quem fecha e sem renomear o pedido (001). Idempotente por order_id.';\n\n-- ── o gatilho ──────────────────────────────────────────────────────────────\n--\n-- Só \\`dine_in\\`, e de propósito. A loja já fatura no checkout, pela extensão\n-- dela, e o guard de idempotência acima faria a segunda chamada não ter efeito\n-- — mas um gatilho genérico também passaria a faturar \\`appointment\\`, que tem 21\n-- pedidos e 1 fatura hoje. Se a agenda deve faturar ao concluir é uma decisão\n-- da agenda, tomada por quem conhece o fluxo dela, não um efeito colateral\n-- desta migration.\n\nCREATE OR REPLACE FUNCTION public.trg_order_raise_receivable_on_close()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nBEGIN\n IF NEW.kind = 'dine_in'\n AND NEW.status = 'completed'\n AND coalesce(OLD.status, '') IS DISTINCT FROM 'completed' THEN\n -- Nunca deixa o fechamento falhar por causa da escrituração: a comanda\n -- fechou de verdade, e uma fatura que não subiu é reparável (a função é\n -- idempotente, então reprocessar é seguro). O contrário — o garçom não\n -- conseguir fechar a mesa porque o Financeiro recusou — não é.\n BEGIN\n PERFORM public.order_raise_receivable(NEW.id);\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'order_raise_receivable falhou para o pedido %: %', NEW.id, SQLERRM;\n END;\n END IF;\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS orders_raise_receivable_on_close ON public.orders;\nCREATE TRIGGER orders_raise_receivable_on_close\n AFTER UPDATE OF status ON public.orders\n FOR EACH ROW\n EXECUTE FUNCTION public.trg_order_raise_receivable_on_close();\n\nGRANT EXECUTE ON FUNCTION public.order_raise_receivable(uuid, date) TO authenticated;\n`\n\nexport const MIGRATION_002_PAYMENT_TYPES_ARE_THE_COUNTRY_NOT_THE_TENANT = `-- ---------------------------------------------------------------------------\n-- 002 — as formas de pagamento que existem são do PAÍS, não do lojista.\n--\n-- \"Tipo de pagamento\" era um cadastro livre: cada tenant começava com a tabela\n-- vazia e tinha de inventar do zero que Pix existe. Isso é o software pedindo\n-- ao usuário uma informação que o software já tem — e o custo aparece na ponta:\n-- sem nenhum tipo cadastrado o modal de recebimento abre dizendo \"nenhuma forma\n-- de pagamento configurada\", e o lojista fica sem receber por causa de um\n-- cadastro que ninguém deveria ter de fazer.\n--\n-- Pix, cartão e boleto existem no Brasil independentemente de opinião. O que É\n-- do lojista é ACEITAR ou não cada um — e isso já tem coluna: \\`is_active\\`.\n--\n-- POR QUE POR TENANT E NÃO GLOBAL. \\`tenant_id\\` é NOT NULL, então uma linha\n-- global não cabe sem mudar o schema e a RLS de uma tabela que já tem dados.\n-- Semear por tenant dá o mesmo resultado visível — a lista vem pronta, e o\n-- lojista liga e desliga — sem tocar em isolamento. \\`unit_id\\` continua\n-- disponível para o dia em que uma filial aceitar o que a outra não aceita.\n--\n-- O QUE É PADRÃO LIGADO e o que vem desligado: dinheiro, Pix e os dois cartões\n-- são o balcão brasileiro e vêm aceitos. Boleto, transferência e cheque existem\n-- e vêm desligados — quem usa liga, e quem não usa nunca vê. Um menu com sete\n-- formas onde só quatro são reais é a mesma poluição que a tabela vazia, do\n-- outro lado.\n--\n-- \\`allowed_account_types\\` fala o vocabulário do RAZÃO — \\`plg_financial_accounts.kind\\`\n-- (bank, cash_physical, card_receivable_credit…) —, não o do cliente\n-- (\\`bank_account\\`, \\`cash_register\\`). A primeira volta desta migration copiou o\n-- mapa do PaymentModal e produziu exatamente a recusa que ela existia para\n-- evitar: \"method credit_card must settle in a card_receivable_credit account\".\n-- Quem valida é \\`financial_receive_payment\\`, então o vocabulário é o dela.\n-- ---------------------------------------------------------------------------\n\nCREATE OR REPLACE FUNCTION public.financial_seed_payment_method_types(p_tenant uuid)\n RETURNS integer\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_inserted integer := 0;\nBEGIN\n -- \\`transaction_type\\` é a chave: o nome é texto de tela e pode ser reescrito,\n -- o código é o que a RPC de recebimento valida. Conflito nele é \"já existe\",\n -- não erro — reexecutar não duplica e não sobrescreve o que o lojista mexeu.\n INSERT INTO public.plg_financial_payment_method_types\n (tenant_id, name, transaction_type, is_active, allowed_account_types)\n SELECT p_tenant, d.name, d.code, d.active, d.accounts\n FROM (VALUES\n ('Dinheiro', 'cash', true, '[\"cash_physical\",\"cash_session\"]'::jsonb),\n ('Pix', 'pix', true, '[\"bank\"]'::jsonb),\n ('Cartão de crédito', 'credit_card', true, '[\"card_receivable_credit\"]'::jsonb),\n ('Cartão de débito', 'debit_card', true, '[\"card_receivable_debit\"]'::jsonb),\n ('Boleto', 'boleto', false, '[\"bank\"]'::jsonb),\n ('Transferência bancária', 'bank_transfer', false, '[\"bank\"]'::jsonb),\n ('Cheque', 'check', false, '[\"bank\"]'::jsonb)\n ) AS d(name, code, active, accounts)\n WHERE NOT EXISTS (\n SELECT 1 FROM public.plg_financial_payment_method_types t\n WHERE t.tenant_id = p_tenant AND t.transaction_type = d.code\n );\n\n GET DIAGNOSTICS v_inserted = ROW_COUNT;\n RETURN v_inserted;\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_seed_payment_method_types(uuid) IS\n 'Semeia as formas de pagamento do Brasil para um tenant (002). Idempotente por transaction_type: reexecutar não duplica nem sobrescreve o que o lojista mudou.';\n\n-- ── toda conta nova já nasce com a lista ────────────────────────────────────\n--\n-- Sem isto o problema volta para o próximo tenant criado, e volta silencioso: a\n-- loja abre, funciona em tudo, e falha na primeira vez que alguém tenta receber\n-- — que é o pior momento para descobrir um cadastro faltando.\n\nCREATE OR REPLACE FUNCTION public.trg_seed_payment_method_types()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nBEGIN\n -- Não deixa a criação da conta falhar por causa do catálogo: um tenant sem\n -- formas de pagamento é reparável (a função é idempotente); um cadastro de\n -- workspace que não completa, não.\n BEGIN\n PERFORM public.financial_seed_payment_method_types(NEW.id);\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'financial_seed_payment_method_types falhou para o tenant %: %', NEW.id, SQLERRM;\n END;\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS tenants_seed_payment_method_types ON public.tenants;\nCREATE TRIGGER tenants_seed_payment_method_types\n AFTER INSERT ON public.tenants\n FOR EACH ROW\n EXECUTE FUNCTION public.trg_seed_payment_method_types();\n\n-- ── o que já existe ─────────────────────────────────────────────────────────\n--\n-- Um tenant migrado do pool antigo trouxe os seis tipos com nome em inglês\n-- (\"PIX\", \"Bank Transfer\"). O \\`NOT EXISTS\\` por código preserva as linhas dele —\n-- o id é referenciado por métodos e movimentos já gravados, e renomear seria\n-- mexer no que o histórico aponta. Ele ganha só o que falta (boleto).\n\nSELECT public.financial_seed_payment_method_types(t.id) FROM public.tenants t;\n\nGRANT EXECUTE ON FUNCTION public.financial_seed_payment_method_types(uuid) TO authenticated;\n`\n\nexport const MIGRATION_003_THE_COMMISSION_RULE_GETS_A_TABLE = `-- 003_the_commission_rule_gets_a_table.sql\n--\n-- A regra de comissão é a única entidade do financeiro que a tela oferece e o\n-- chain não cria. O caminho até aqui: \\`commission_rules\\` nasceu em SQL do app\n-- (financial_extras), a 028 renomeou para o nome prefixado, e a regeneração do\n-- baseline (#338) não trouxe nenhum dos dois — a tabela sobreviveu só nos pools\n-- que existiam antes. Hoje o cluster não a tem, e \\`data/registries.ts\\` ainda\n-- pede o nome SEM prefixo, que não existe em pool nenhum. Ligar\n-- \\`modules.commissions\\` abre uma tela sobre nada, dos dois lados.\n--\n-- Esta migration é escrita para os dois casos: cria onde falta, e converge o\n-- pool legado adotando a tabela que já está lá — unit_id e owner_id são o que a\n-- convenção de escopo passou a exigir depois que ela foi criada.\n--\n-- O que ela NÃO faz: calcular. A regra passa a ter onde morar; a apuração\n-- depende de saber quem EXECUTOU o item, que é trabalho de outro módulo.\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_commission_rules (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n name text NOT NULL,\n priority integer DEFAULT 1 NOT NULL,\n value_type text DEFAULT 'percentage'::text NOT NULL,\n value numeric(14,4) DEFAULT 0 NOT NULL,\n description text,\n is_active boolean DEFAULT true NOT NULL,\n owner_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\n-- Convergência do pool legado: a tabela existe lá sem as colunas de escopo.\nALTER TABLE public.plg_financial_commission_rules ADD COLUMN IF NOT EXISTS unit_id uuid;\nALTER TABLE public.plg_financial_commission_rules ADD COLUMN IF NOT EXISTS owner_id uuid;\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_commission_rules'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_commission_rules ADD CONSTRAINT plg_financial_commission_rules_pkey PRIMARY KEY (id);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_rules_tenant_id_id_key') THEN\n ALTER TABLE ONLY public.plg_financial_commission_rules ADD CONSTRAINT plg_financial_commission_rules_tenant_id_id_key UNIQUE (tenant_id, id);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_rules_value_type_check') THEN\n ALTER TABLE ONLY public.plg_financial_commission_rules ADD CONSTRAINT plg_financial_commission_rules_value_type_check\n CHECK (value_type = ANY (ARRAY['percentage'::text, 'fixed'::text]));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_rules_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_commission_rules ADD CONSTRAINT plg_financial_commission_rules_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_rules_unit_fk') THEN\n ALTER TABLE ONLY public.plg_financial_commission_rules ADD CONSTRAINT plg_financial_commission_rules_unit_fk\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_rules_owner_fk') THEN\n ALTER TABLE ONLY public.plg_financial_commission_rules ADD CONSTRAINT plg_financial_commission_rules_owner_fk\n FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS idx_plg_financial_commission_rules_tenant\n ON public.plg_financial_commission_rules USING btree (tenant_id);\nCREATE INDEX IF NOT EXISTS plg_financial_commission_rules_tenant_unit_idx\n ON public.plg_financial_commission_rules USING btree (tenant_id, unit_id);\nCREATE INDEX IF NOT EXISTS plg_financial_commission_rules_tenant_owner_idx\n ON public.plg_financial_commission_rules USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n-- A apuração lê por prioridade decrescente, só as ativas.\nCREATE INDEX IF NOT EXISTS plg_financial_commission_rules_active_priority_idx\n ON public.plg_financial_commission_rules USING btree (tenant_id, priority DESC) WHERE is_active;\n\nDROP TRIGGER IF EXISTS plg_financial_commission_rules_updated_at ON public.plg_financial_commission_rules;\nCREATE TRIGGER plg_financial_commission_rules_updated_at\n BEFORE UPDATE ON public.plg_financial_commission_rules\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nALTER TABLE public.plg_financial_commission_rules ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_commission_rules FORCE ROW LEVEL SECURITY;\n\n-- Mesmo recorte de plg_financial_cost_centers: tenant, unidade, dono e permissão.\n-- O escopo 'financial.commission_rule' é o que a lista de owner_scoped consulta.\nDROP POLICY IF EXISTS plg_financial_commission_rules_authz_select ON public.plg_financial_commission_rules;\nCREATE POLICY plg_financial_commission_rules_authz_select ON public.plg_financial_commission_rules\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (unit_id IS NULL OR (SELECT app.has_unit(unit_id)))\n AND ((NOT (SELECT app.owner_scoped('financial.commission_rule')))\n OR owner_id IS NULL\n OR owner_id = (SELECT auth.uid())\n OR (SELECT app.has_grant('financial.commission_rule', id, ARRAY['shared_with']))\n OR (SELECT app.has_permission('financial.read_all', unit_id)))\n AND (SELECT app.has_permission('financial.read', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_commission_rules_authz_insert ON public.plg_financial_commission_rules;\nCREATE POLICY plg_financial_commission_rules_authz_insert ON public.plg_financial_commission_rules\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (unit_id IS NULL OR (SELECT app.has_unit(unit_id)))\n AND (SELECT app.has_permission('financial.create', unit_id)));\n\n-- \\`financial.update\\` NÃO existe no catálogo: a chave da casa é \\`financial.edit\\`\n-- (app.permissions). Uma policy sobre chave inexistente nega tudo em silêncio.\nDROP POLICY IF EXISTS plg_financial_commission_rules_authz_update ON public.plg_financial_commission_rules;\nCREATE POLICY plg_financial_commission_rules_authz_update ON public.plg_financial_commission_rules\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (unit_id IS NULL OR (SELECT app.has_unit(unit_id)))\n AND ((NOT (SELECT app.owner_scoped('financial.commission_rule')))\n OR owner_id IS NULL\n OR owner_id = (SELECT auth.uid())\n OR (SELECT app.has_grant('financial.commission_rule', id, ARRAY['shared_with']))\n OR (SELECT app.has_permission('financial.read_all', unit_id)))\n AND (SELECT app.has_permission('financial.edit', unit_id)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (unit_id IS NULL OR (SELECT app.has_unit(unit_id)))\n AND (SELECT app.has_permission('financial.edit', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_commission_rules_authz_delete ON public.plg_financial_commission_rules;\nCREATE POLICY plg_financial_commission_rules_authz_delete ON public.plg_financial_commission_rules\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (unit_id IS NULL OR (SELECT app.has_unit(unit_id)))\n AND ((NOT (SELECT app.owner_scoped('financial.commission_rule')))\n OR owner_id IS NULL\n OR owner_id = (SELECT auth.uid())\n OR (SELECT app.has_grant('financial.commission_rule', id, ARRAY['shared_with']))\n OR (SELECT app.has_permission('financial.read_all', unit_id)))\n AND (SELECT app.has_permission('financial.delete', unit_id)));\n\n-- app.owner_scoped() devolve TRUE quando o resource_type não está registrado\n-- (\"configuration relaxes; it never tightens by omission\"). Sem esta linha a\n-- regra nasceria com escopo de dono, diferente de todo o resto do cadastro\n-- financeiro — que é \\`false\\`, como o centro de custo.\nINSERT INTO app.scaffold_registry\n (table_schema, table_name, resource_type, plugin, owner_scoping, mode, previous_policies, audit_hook, scope, distributable)\nVALUES\n ('public', 'plg_financial_commission_rules', 'financial.commission_rule', 'financial', false, 'enforce', ARRAY[]::text[], false, 'unit_optional', false)\nON CONFLICT DO NOTHING;\n\nGRANT ALL ON TABLE public.plg_financial_commission_rules TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_commission_rules TO service_role;\n\nCOMMENT ON TABLE public.plg_financial_commission_rules IS\n 'Regra de comissão: percentual ou fixo, aplicada por prioridade. Guarda a REGRA; a apuração por atendimento é outra tabela e depende de saber quem executou o item.';\n`\n\nexport const MIGRATION_004_ONE_BRIDGE_FOR_THREE_DOORS = `-- 004_one_bridge_for_three_doors.sql\n--\n-- Três portas levam uma venda ao Financeiro — a comanda que fecha, o checkout\n-- da loja, o atendimento que termina — e elas discordavam em três pontos que\n-- 006_three_doors_one_book.sql agora cobra. Este arquivo alinha a ponte da\n-- loja (\\`fn_invoice_from_order_internal\\`) com a da comanda\n-- (\\`order_raise_receivable\\`), que já estava certa.\n--\n-- 1. O PEDIDO PARA DE PERDER O NÚMERO DELE.\n--\n-- A função reescrevia \\`orders.reference_number\\` com o número da fatura.\n-- Isso vem da origem dela — promover um ORÇAMENTO em fatura, onde o\n-- documento realmente muda de nome — e é destrutivo em tudo o mais: no\n-- cluster, toda venda e o único atendimento faturado passaram a se chamar\n-- \\`REC-xxxxx\\`, e o lojista perdeu o número pelo qual procura o pedido.\n--\n-- O número da fatura nunca precisou morar ali: \\`v_invoice_balances\\`\n-- (que a loja lê) tira \\`reference_number\\` do NÚMERO DA FATURA, não do\n-- pedido, e \\`metadata.invoiceNumber\\` já guardava o mesmo valor. Ninguém\n-- perde informação; o pedido recupera o nome.\n--\n-- 2. A PARCELA GANHA UNIDADE.\n--\n-- O INSERT não passava \\`unit_id\\`, então 19 das 24 parcelas do cluster\n-- estavam sem unidade. Quem enxerga só a própria unidade não via a própria\n-- parcela — o extrato dela mentia por omissão, e não por erro de conta.\n-- \\`tenant_id\\` já vinha por gatilho; a unidade não.\n--\n-- 3. O \\`p_status\\` DEIXA DE SER UMA ARMADILHA.\n--\n-- A função força \\`status = p_status\\`, e o guard de espécie recusa um status\n-- que a espécie não declara. \\`invoiced\\` está no ciclo de \\`appointment\\` e\n-- NÃO no de \\`sale\\` — então a mesma chamada que a agenda faz estoura com\n-- 23514 numa venda. A loja só funcionava porque passava NULL, e isso não\n-- estava escrito em lugar nenhum.\n--\n-- O faturamento já é registrado em \\`stage = 'invoiced'\\`, que não é o ciclo\n-- de vida da espécie. Então: aplica-se \\`p_status\\` quando a espécie o\n-- declara, e ignora-se quando não — em vez de derrubar o faturamento por\n-- causa de uma palavra que aquele tipo de pedido não usa.\n\n-- ── a ponte ─────────────────────────────────────────────────────────────────\nCREATE OR REPLACE FUNCTION public.fn_invoice_from_order_internal(\n p_order_id uuid,\n p_due_date date DEFAULT NULL::date,\n p_status text DEFAULT NULL::text,\n p_installments integer DEFAULT 1,\n p_direction text DEFAULT 'credit'::text\n) RETURNS uuid\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_order public.orders%ROWTYPE;\n v_invoice_id uuid;\n v_dir text;\n v_number text;\n v_summary text;\n v_contact text;\n v_n int := GREATEST(COALESCE(p_installments, 1), 1);\n v_total numeric;\n v_base numeric;\n v_amt numeric;\n v_alloc numeric := 0;\n v_due date;\n v_status text;\n i int;\nBEGIN\n SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;\n IF NOT FOUND THEN RAISE EXCEPTION 'fn_invoice_from_order: order % not found', p_order_id; END IF;\n\n SELECT id INTO v_invoice_id FROM public.plg_financial_invoices\n WHERE order_id = p_order_id AND cancelled_at IS NULL LIMIT 1;\n IF v_invoice_id IS NOT NULL THEN RETURN p_order_id; END IF;\n -- A pool promoted before 016 carries legacy bill rows and no invoice; it is\n -- already invoiced and must not be invoiced twice.\n IF EXISTS (SELECT 1 FROM public.plg_financial_movements\n WHERE invoice_id = p_order_id AND movement_kind = 'bill'\n AND status <> 'cancelled') THEN\n RETURN p_order_id;\n END IF;\n\n v_dir := CASE WHEN p_direction = 'debit' THEN 'payable' ELSE 'receivable' END;\n v_number := public.plg_financial_next_invoice_number(v_order.tenant_id, v_dir);\n\n SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary\n FROM public.items WHERE order_id = p_order_id;\n SELECT name INTO v_contact FROM public.people WHERE id = v_order.party_id;\n\n -- Só o status que a espécie declara. Um pedido faturado é \\`stage =\n -- 'invoiced'\\`; o \\`status\\` continua sendo o ciclo de vida daquele tipo de\n -- pedido, e forçar nele uma palavra emprestada de outra espécie faz o guard\n -- derrubar o faturamento inteiro.\n v_status := v_order.status;\n IF p_status IS NOT NULL AND EXISTS (\n SELECT 1 FROM public.order_kinds k\n WHERE k.kind = v_order.kind\n AND (k.statuses IS NULL OR p_status = ANY (k.statuses))\n ) THEN\n v_status := p_status;\n END IF;\n\n UPDATE public.orders SET\n status = v_status,\n stage = 'invoiced',\n direction = p_direction,\n -- \\`reference_number\\` NÃO entra aqui: o pedido continua se chamando pelo\n -- número dele. O número da fatura vai para metadata, onde já ia.\n metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object(\n 'itemsSummary', COALESCE(v_summary, ''),\n 'contactName', v_contact,\n 'installmentCount', v_n,\n 'quoteNumber', v_order.reference_number,\n 'direction', p_direction,\n 'invoiceNumber', v_number\n ),\n updated_at = now()\n WHERE id = p_order_id;\n\n v_total := round(COALESCE(v_order.total, 0), 2);\n v_due := COALESCE(p_due_date, current_date);\n\n INSERT INTO public.plg_financial_invoices\n (tenant_id, unit_id, direction, counterparty_person_id, order_id, number, competence_date, issue_date,\n total, description, metadata, created_by)\n VALUES\n (v_order.tenant_id, v_order.unit_id, v_dir, v_order.party_id, p_order_id, v_number,\n COALESCE(v_order.created_at::date, current_date), current_date,\n v_total, NULLIF(v_summary, ''),\n jsonb_build_object('source', 'fn_invoice_from_order', 'orderKind', v_order.kind),\n auth.uid())\n RETURNING id INTO v_invoice_id;\n\n v_base := round(v_total / v_n, 2);\n FOR i IN 1..v_n LOOP\n IF i = v_n THEN v_amt := round(v_total - v_alloc, 2);\n ELSE v_amt := v_base; v_alloc := v_alloc + v_base; END IF;\n\n -- tenant E unidade: sem a unidade, quem lê escopado à própria unidade não\n -- enxerga a própria parcela.\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount)\n VALUES (v_order.tenant_id, v_order.unit_id, v_invoice_id, i,\n (v_due + ((i - 1) || ' months')::interval)::date, v_amt);\n -- No mirror. v_financial_bills gives the legacy shape to whoever still\n -- wants it, derived from exactly this row.\n END LOOP;\n\n RETURN p_order_id;\nEND;\n$$;\n\nCOMMENT ON FUNCTION public.fn_invoice_from_order_internal(uuid, date, text, integer, text) IS\n 'Promove a venda em fatura sem renomear o pedido, com unidade na parcela e sem forçar status que a espécie não declara (004).';\n\n-- ── reparo: devolver aos pedidos o número que era deles ─────────────────────\n--\n-- Guardado ao máximo de propósito: só mexe onde o número ATUAL é exatamente o\n-- da fatura (\\`invoiceNumber\\`) E existe o antigo guardado (\\`quoteNumber\\`). Um\n-- pedido que nunca passou por aqui, ou que já foi renomeado por outra razão,\n-- não é tocado.\nUPDATE public.orders o SET\n reference_number = o.metadata ->> 'quoteNumber',\n updated_at = now()\nWHERE o.metadata ? 'quoteNumber'\n AND o.metadata ? 'invoiceNumber'\n AND nullif(o.metadata ->> 'quoteNumber', '') IS NOT NULL\n AND o.reference_number IS NOT DISTINCT FROM (o.metadata ->> 'invoiceNumber')\n AND (o.metadata ->> 'quoteNumber') IS DISTINCT FROM (o.metadata ->> 'invoiceNumber');\n\n-- ── reparo: a parcela que nasceu sem unidade herda a da fatura ──────────────\nUPDATE public.plg_financial_invoice_installments p SET\n unit_id = f.unit_id,\n updated_at = now()\nFROM public.plg_financial_invoices f\nWHERE f.id = p.invoice_id\n AND p.unit_id IS NULL\n AND f.unit_id IS NOT NULL;\n`\n\nexport const MIGRATION_005_THE_APPOINTMENT_CLOSES_LIKE_THE_COMANDA = `-- 005_the_appointment_closes_like_the_comanda.sql\n--\n-- Concluir um atendimento não levantava conta a receber nenhuma. No cluster:\n-- 21 pedidos \\`appointment\\`, 1 fatura. A comanda fecha e vira recebível desde a\n-- 001; o atendimento, não — e o salão inteiro passava ao largo do Financeiro\n-- pelo mesmo motivo pelo qual o restaurante passava antes dela.\n--\n-- A 001 deixou isso explicitamente em aberto: \"se a agenda deve faturar ao\n-- concluir é uma decisão da agenda, tomada por quem conhece o fluxo dela, não\n-- um efeito colateral desta migration\". A decisão, agora tomada, é SIM — e a\n-- razão é o fluxo real da recepção:\n--\n-- agendar → confirmar → CHECK-IN → atender → concluir → receber\n--\n-- No V1 o pagamento não bloqueia o check-in (decisão de produto deles, e boa:\n-- a cliente já está na porta). Quem recebe abre a ficha e cobra o que está em\n-- aberto. Para existir \"o que está em aberto\", a conta precisa ter sido\n-- levantada — e o momento em que ela passa a ser devida é quando o serviço foi\n-- prestado, não quando alguém lembra de faturar.\n--\n-- POR QUE order_raise_receivable, e não agenda_booking_invoice(). A segunda\n-- existe, e não tem um único chamador em TypeScript. Ela chama\n-- fn_invoice_from_order, que exige \\`financial.create\\` de QUEM CHAMA — e quem\n-- conclui um atendimento é a recepção, não o financeiro. Exigir a permissão\n-- faria a conclusão falhar para a pessoa certa. É o mesmo raciocínio que a 001\n-- fez para o garçom, e a mesma função resolve.\n--\n-- agenda_booking_invoice() continua onde está, para quem quiser faturar um\n-- atendimento explicitamente, em N parcelas, sendo do Financeiro.\n\nCREATE OR REPLACE FUNCTION public.trg_order_raise_receivable_on_close()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nBEGIN\n -- Duas espécies, uma regra: o serviço acabou, então a conta é devida.\n -- \\`sale\\` não entra — a loja fatura no checkout, pela extensão dela, e o\n -- pedido de loja nasce fechado.\n IF NEW.kind IN ('dine_in', 'appointment', 'service_order')\n AND NEW.status = 'completed'\n AND coalesce(OLD.status, '') IS DISTINCT FROM 'completed' THEN\n -- Nunca deixa o fechamento falhar por causa da escrituração: o atendimento\n -- terminou de verdade, e uma fatura que não subiu é reparável (a função é\n -- idempotente, então reprocessar é seguro). O contrário — a recepção não\n -- conseguir concluir porque o Financeiro recusou — não é.\n BEGIN\n PERFORM public.order_raise_receivable(NEW.id);\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'order_raise_receivable falhou para o pedido %: %', NEW.id, SQLERRM;\n END;\n END IF;\n RETURN NEW;\nEND $$;\n\nCOMMENT ON FUNCTION public.trg_order_raise_receivable_on_close() IS\n 'A comanda, o atendimento e a ordem de serviço levantam o recebível ao concluir (001, 005). A loja fatura no checkout, pela extensão dela.';\n\n-- ── o que já foi concluído e nunca foi faturado ────────────────────────────\n--\n-- Idempotente por construção: order_raise_receivable devolve a fatura que já\n-- existe em vez de criar a segunda, e recusa pedido de total zero.\nDO $$\nDECLARE r record;\nBEGIN\n FOR r IN\n SELECT o.id FROM public.orders o\n WHERE o.kind IN ('appointment', 'service_order')\n AND o.status = 'completed'\n AND coalesce(o.total, 0) > 0\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_invoices f\n WHERE f.order_id = o.id AND f.cancelled_at IS NULL)\n LOOP\n BEGIN\n PERFORM public.order_raise_receivable(r.id);\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'backfill: order_raise_receivable falhou para %: %', r.id, SQLERRM;\n END;\n END LOOP;\nEND $$;\n`\n\nexport const MIGRATION_006_THE_MONTH_CAN_BE_CLOSED_AND_THE_BOOK_CAN_BE_PROVEN = `-- 006_the_month_can_be_closed_and_the_book_can_be_proven.sql\n--\n-- Duas ausências que separam \"telas de financeiro\" de \"um livro contábil\", e\n-- que ficam mais caras a cada tela nova construída sem elas.\n--\n-- 1. NÃO EXISTE FECHAMENTO DE COMPETÊNCIA. Nenhuma tabela de período, nenhuma\n-- trava. Qualquer lançamento pode ser datado para dentro de um mês já\n-- entregue ao contador — e o DRE daquele mês muda depois de ter sido\n-- reportado, em silêncio. Um relatório que pode mudar depois de fechado não\n-- é um relatório, é uma consulta.\n--\n-- 2. NADA PROVA QUE O LIVRO FECHA. O movimento já nasce com duas pernas\n-- (\\`debit_account_id\\` e \\`credit_account_id\\`) — a partida dobrada está no\n-- modelo. O que nunca existiu foi a soma: ninguém jamais afirmou que o total\n-- a débito é igual ao total a crédito. Estamos a uma view de poder afirmar,\n-- e essa afirmação é a diferença entre auditável e \"confia\".\n--\n-- POR QUE AS DUAS JUNTAS. Balancete sem fechamento é um número que muda quando\n-- alguém lança retroativo; fechamento sem balancete é uma tranca sem nada\n-- provado dentro. Uma é a foto, a outra é a moldura.\n--\n-- A COMPETÊNCIA É DO TENANT, não da unidade. Fechar o mês é um ato contábil da\n-- empresa; fragmentar por unidade produziria um mês meio fechado, que é a pior\n-- das respostas para quem precisa dizer ao contador que acabou.\n\n-- ── o período ───────────────────────────────────────────────────────────────\nCREATE TABLE IF NOT EXISTS public.plg_financial_periods (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n -- Sempre o dia 1º: a competência é o MÊS, e guardar o mês como data evita\n -- a dupla (ano int, mês int) que todo relatório teria de remontar.\n competence_month date NOT NULL,\n status text DEFAULT 'open'::text NOT NULL,\n closed_at timestamp with time zone,\n closed_by_user_id uuid,\n reopened_at timestamp with time zone,\n reopened_by_user_id uuid,\n -- Reabrir sem dizer por quê é como não ter fechado. O motivo é obrigatório\n -- na RPC; a coluna aceita nulo porque um período nunca reaberto não tem um.\n reopen_reason text,\n notes text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_periods'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_periods ADD CONSTRAINT plg_financial_periods_pkey PRIMARY KEY (id);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_periods_tenant_month_key') THEN\n ALTER TABLE ONLY public.plg_financial_periods ADD CONSTRAINT plg_financial_periods_tenant_month_key UNIQUE (tenant_id, competence_month);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_periods_status_check') THEN\n ALTER TABLE ONLY public.plg_financial_periods ADD CONSTRAINT plg_financial_periods_status_check\n CHECK (status = ANY (ARRAY['open'::text, 'closed'::text]));\n END IF;\n -- O dia 1º não é convenção de quem escreve: é constraint.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_periods_first_of_month_check') THEN\n ALTER TABLE ONLY public.plg_financial_periods ADD CONSTRAINT plg_financial_periods_first_of_month_check\n CHECK (competence_month = date_trunc('month', competence_month)::date);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_periods_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_periods ADD CONSTRAINT plg_financial_periods_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_periods_tenant_month_idx\n ON public.plg_financial_periods USING btree (tenant_id, competence_month DESC);\n-- A trava consulta \"este mês está fechado?\" a cada lançamento: índice parcial.\nCREATE INDEX IF NOT EXISTS plg_financial_periods_closed_idx\n ON public.plg_financial_periods USING btree (tenant_id, competence_month) WHERE status = 'closed';\n\nDROP TRIGGER IF EXISTS plg_financial_periods_updated_at ON public.plg_financial_periods;\nCREATE TRIGGER plg_financial_periods_updated_at\n BEFORE UPDATE ON public.plg_financial_periods\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nALTER TABLE public.plg_financial_periods ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_periods FORCE ROW LEVEL SECURITY;\n\n-- Quem lê o Financeiro vê o estado do mês — saber que fevereiro fechou não é\n-- informação privilegiada, e a tela precisa disso para desabilitar o botão.\nDROP POLICY IF EXISTS plg_financial_periods_authz_select ON public.plg_financial_periods;\nCREATE POLICY plg_financial_periods_authz_select ON public.plg_financial_periods\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL)));\n\n-- ESCREVER É SÓ PELA RPC. Sem policy de INSERT/UPDATE/DELETE: fechar e reabrir\n-- são atos, não edições de linha, e o ato é onde a permissão e o registro de\n-- QUEM moram. Uma policy de UPDATE aqui seria a porta dos fundos da trava.\n\nINSERT INTO app.scaffold_registry\n (table_schema, table_name, resource_type, plugin, owner_scoping, mode, previous_policies, audit_hook, scope, distributable)\nVALUES\n ('public', 'plg_financial_periods', 'financial.period', 'financial', false, 'enforce', ARRAY[]::text[], true, 'tenant', false)\nON CONFLICT DO NOTHING;\n\nGRANT SELECT ON TABLE public.plg_financial_periods TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_periods TO service_role;\n\nCOMMENT ON TABLE public.plg_financial_periods IS\n 'Competência mensal e seu estado. Fechado = nada mais entra nem muda com data naquele mês. Escrita só por financial_period_close/reopen (006).';\n\n-- ── a permissão de fechar é própria ─────────────────────────────────────────\n--\n-- Segregação de função: quem lança não é necessariamente quem fecha, e quem\n-- fecha não é necessariamente quem reabre. \\`financial.edit\\` é a permissão de\n-- mexer num título; fechar o mês é outra ordem de decisão.\nINSERT INTO app.permissions (key, plugin, description)\nVALUES ('financial.close_period', 'financial',\n 'Fechar e reabrir a competência mensal. Separada de financial.edit de propósito: quem lança não é quem fecha.')\nON CONFLICT (key) DO NOTHING;\n\n-- ── a trava ─────────────────────────────────────────────────────────────────\nCREATE OR REPLACE FUNCTION public.plg_financial_period_is_closed(p_tenant uuid, p_date date)\n RETURNS boolean\n LANGUAGE sql\n STABLE\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\n SELECT EXISTS (\n SELECT 1 FROM public.plg_financial_periods p\n WHERE p.tenant_id = p_tenant\n AND p.status = 'closed'\n AND p.competence_month = date_trunc('month', p_date)::date\n );\n$$;\n\nCOMMENT ON FUNCTION public.plg_financial_period_is_closed(uuid, date) IS\n 'O mês desta data está fechado para este tenant? (006)';\n\nCREATE OR REPLACE FUNCTION public.trg_financial_refuse_closed_period()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_date date;\n v_tenant uuid;\nBEGIN\n v_tenant := NEW.tenant_id;\n\n -- A data que conta é a da COMPETÊNCIA, não a de criação da linha. Num título\n -- é \\`competence_date\\`; num movimento é a data em que o dinheiro andou.\n IF TG_TABLE_NAME = 'plg_financial_invoices' THEN\n v_date := coalesce(NEW.competence_date, NEW.issue_date, current_date);\n ELSE\n v_date := coalesce(NEW.payment_date, NEW.due_date, current_date);\n END IF;\n\n IF public.plg_financial_period_is_closed(v_tenant, v_date) THEN\n RAISE EXCEPTION 'a competência de % está fechada — reabra o período para lançar nela', to_char(v_date, 'MM/YYYY')\n USING ERRCODE = '23514',\n HINT = 'financial_period_reopen(date_trunc(''month'', data)::date, ''motivo'') exige financial.close_period.';\n END IF;\n\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS plg_financial_invoices_refuse_closed_period ON public.plg_financial_invoices;\nCREATE TRIGGER plg_financial_invoices_refuse_closed_period\n BEFORE INSERT OR UPDATE OF competence_date, total ON public.plg_financial_invoices\n FOR EACH ROW EXECUTE FUNCTION public.trg_financial_refuse_closed_period();\n\nDROP TRIGGER IF EXISTS plg_financial_movements_refuse_closed_period ON public.plg_financial_movements;\nCREATE TRIGGER plg_financial_movements_refuse_closed_period\n BEFORE INSERT ON public.plg_financial_movements\n FOR EACH ROW EXECUTE FUNCTION public.trg_financial_refuse_closed_period();\n\n-- ── fechar e reabrir ────────────────────────────────────────────────────────\nCREATE OR REPLACE FUNCTION public.financial_period_close(p_month date, p_notes text DEFAULT NULL)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_month date := date_trunc('month', p_month)::date;\n v_open numeric;\n v_row public.plg_financial_periods%ROWTYPE;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_period_close: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.close_period', NULL) THEN\n RAISE EXCEPTION 'financial_period_close: exige financial.close_period' USING ERRCODE = '42501';\n END IF;\n\n -- Um mês no futuro não acabou; fechá-lo trancaria lançamentos que ainda vão\n -- acontecer no curso normal do negócio.\n IF v_month > date_trunc('month', current_date)::date THEN\n RAISE EXCEPTION 'financial_period_close: % ainda não terminou', to_char(v_month, 'MM/YYYY')\n USING ERRCODE = '22023';\n END IF;\n\n -- Caixa aberto é dinheiro que ainda vai se mexer naquele mês. Fechar por cima\n -- produz um saldo que muda depois de fechado, que é exatamente o que a trava\n -- existe para impedir.\n IF EXISTS (SELECT 1 FROM public.plg_financial_cash_register_sessions s\n WHERE s.tenant_id = v_tenant AND s.status = 'open'\n AND date_trunc('month', s.opened_at)::date <= v_month) THEN\n RAISE EXCEPTION 'financial_period_close: existe caixa aberto em % ou antes — feche o caixa primeiro', to_char(v_month, 'MM/YYYY')\n USING ERRCODE = '23514';\n END IF;\n\n INSERT INTO public.plg_financial_periods (tenant_id, competence_month, status, closed_at, closed_by_user_id, notes)\n VALUES (v_tenant, v_month, 'closed', now(), auth.uid(), p_notes)\n ON CONFLICT (tenant_id, competence_month) DO UPDATE\n SET status = 'closed', closed_at = now(), closed_by_user_id = auth.uid(),\n notes = coalesce(EXCLUDED.notes, public.plg_financial_periods.notes), updated_at = now()\n RETURNING * INTO v_row;\n\n SELECT coalesce(sum(t.balance), 0) INTO v_open\n FROM public.v_financial_installment_balances t\n WHERE t.tenant_id = v_tenant AND t.status <> 'cancelled'\n AND date_trunc('month', t.due_date)::date <= v_month;\n\n RETURN jsonb_build_object(\n 'period_id', v_row.id, 'competence_month', v_month, 'status', 'closed',\n 'closed_at', v_row.closed_at,\n -- Fechar não quita: o que estava em aberto continua em aberto, e quem fecha\n -- merece ver o número em vez de descobrir depois.\n 'open_balance_carried', v_open);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.financial_period_reopen(p_month date, p_reason text)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_month date := date_trunc('month', p_month)::date;\n v_row public.plg_financial_periods%ROWTYPE;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_period_reopen: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.close_period', NULL) THEN\n RAISE EXCEPTION 'financial_period_reopen: exige financial.close_period' USING ERRCODE = '42501';\n END IF;\n -- Reabrir sem motivo registrado é como não ter fechado: some o rastro de por\n -- que um mês entregue voltou a mudar.\n IF nullif(btrim(coalesce(p_reason, '')), '') IS NULL THEN\n RAISE EXCEPTION 'financial_period_reopen: o motivo é obrigatório' USING ERRCODE = '22023';\n END IF;\n\n UPDATE public.plg_financial_periods SET\n status = 'open', reopened_at = now(), reopened_by_user_id = auth.uid(),\n reopen_reason = p_reason, updated_at = now()\n WHERE tenant_id = v_tenant AND competence_month = v_month\n RETURNING * INTO v_row;\n\n IF v_row.id IS NULL THEN\n RAISE EXCEPTION 'financial_period_reopen: % nunca foi fechado', to_char(v_month, 'MM/YYYY')\n USING ERRCODE = 'P0002';\n END IF;\n\n RETURN jsonb_build_object('period_id', v_row.id, 'competence_month', v_month,\n 'status', 'open', 'reopened_at', v_row.reopened_at, 'reason', p_reason);\nEND $$;\n\nREVOKE ALL ON FUNCTION public.financial_period_close(date, text) FROM PUBLIC;\nREVOKE ALL ON FUNCTION public.financial_period_reopen(date, text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_period_close(date, text) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_period_reopen(date, text) TO authenticated;\n\n-- ── o balancete ─────────────────────────────────────────────────────────────\n--\n-- A partida dobrada já está no dado: todo pagamento nasce com as duas pernas.\n-- Esta view só faz a soma que ninguém fazia — cada conta com o que entrou a\n-- débito e a crédito, e o saldo.\n--\n-- Só movimentos LIQUIDADOS e não estornados: uma parcela em aberto é uma\n-- promessa, não um lançamento, e somá-la ao balancete faria o livro não fechar\n-- por motivo certo.\n--\n-- \\`security_invoker\\` para a RLS de plg_financial_movements continuar decidindo\n-- quem vê o quê — um balancete que ignora escopo é um vazamento com totais.\nCREATE OR REPLACE VIEW public.v_financial_trial_balance\nWITH (security_invoker = 'true') AS\nWITH legs AS (\n SELECT m.tenant_id, m.unit_id, m.debit_account_id AS account_id,\n date_trunc('month', coalesce(m.payment_date, m.due_date))::date AS competence_month,\n m.paid_amount AS debit, 0::numeric AS credit\n FROM public.plg_financial_movements m\n WHERE m.status = 'paid' AND m.reversed_by_id IS NULL AND m.debit_account_id IS NOT NULL\n UNION ALL\n SELECT m.tenant_id, m.unit_id, m.credit_account_id,\n date_trunc('month', coalesce(m.payment_date, m.due_date))::date,\n 0::numeric, m.paid_amount\n FROM public.plg_financial_movements m\n WHERE m.status = 'paid' AND m.reversed_by_id IS NULL AND m.credit_account_id IS NOT NULL\n)\nSELECT l.tenant_id,\n l.unit_id,\n l.competence_month,\n l.account_id,\n a.code AS account_code,\n a.name AS account_name,\n a.account_type,\n a.kind AS account_kind,\n sum(l.debit)::numeric(14,2) AS total_debit,\n sum(l.credit)::numeric(14,2) AS total_credit,\n (sum(l.debit) - sum(l.credit))::numeric(14,2) AS balance\n FROM legs l\n JOIN public.plg_financial_accounts a ON a.id = l.account_id\n GROUP BY l.tenant_id, l.unit_id, l.competence_month, l.account_id,\n a.code, a.name, a.account_type, a.kind;\n\nCOMMENT ON VIEW public.v_financial_trial_balance IS\n 'Balancete: cada conta com débito, crédito e saldo, por competência. Só movimentos liquidados e não estornados. A soma dos débitos tem de bater com a dos créditos — é a afirmação que torna o livro auditável (006).';\n\nGRANT SELECT ON public.v_financial_trial_balance TO authenticated;\nGRANT SELECT ON public.v_financial_trial_balance TO service_role;\n`\n\nexport const MIGRATION_007_THE_INVOICE_KNOWS_WHO_ISSUED_IT_AND_WHAT_IT_OWES = `-- 007_the_invoice_knows_who_issued_it_and_what_it_owes.sql\n--\n-- Dois buracos que impedem qualquer obrigação fiscal brasileira, e que a\n-- reforma tributária transforma de \"falta\" em \"impede de operar\".\n--\n-- 1. A FATURA NÃO SABIA QUEM A EMITIU.\n--\n-- \\`company_legal_entities\\` existe, com CNPJ e regime tributário, e uma\n-- unidade aponta para a sua (v_unit_company_legal_entities). A fatura, não:\n-- ZERO colunas fiscais em plg_financial_invoices. Um tenant com quatro\n-- CNPJs — que é o caso do salão no cluster — não conseguia dizer qual deles\n-- vendeu. Cada CNPJ apura e declara o SEU IBS/CBS; sem o emitente na\n-- fatura, não há o que apurar.\n--\n-- 2. O IMPOSTO ERA UM NÚMERO SÓ.\n--\n-- A linha tinha \\`tax_amount numeric\\` — um campo. CBS e IBS são dois tributos\n-- distintos, com bases, alíquotas e CADEIAS DE CRÉDITO separadas, e durante\n-- a transição (2026–2032) convivem com PIS/COFINS/ICMS/ISS, que vão saindo\n-- em datas diferentes. Um campo não carrega isso em nenhum ano do\n-- cronograma, e um par de campos novos por tributo envelheceria a cada\n-- etapa.\n--\n-- Então: uma LINHA POR TRIBUTO. Em 2026 a linha tem CBS a 0,9% e IBS a\n-- 0,1% ao lado de PIS/COFINS; em 2033 tem CBS e IBS e mais nada. A forma\n-- não muda — muda o conteúdo, que é o que uma transição de sete anos exige.\n--\n-- SOBRE O SPLIT PAYMENT, e por que ele NÃO entra aqui.\n--\n-- A partir de 2027 o banco/adquirente separa o tributo na liquidação e\n-- deposita o líquido. Contabilmente isso é UM lançamento de TRÊS pernas:\n-- débito banco (líquido) + débito imposto retido / crédito a receber\n-- (bruto). O \\`plg_financial_movements\\` tem exatamente DUAS pernas.\n--\n-- A saída certa dentro do modelo é DOIS movimentos alocados à mesma\n-- parcela — o líquido e o retido — que somam o bruto e mantêm o livro\n-- fechando. É desenho, não migration: fica registrado aqui e é o próximo\n-- passo, depois que a apuração souber quanto reter.\n\n-- ── 1. o emitente ───────────────────────────────────────────────────────────\nALTER TABLE public.plg_financial_invoices\n ADD COLUMN IF NOT EXISTS company_legal_entity_id uuid;\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_invoices_legal_entity_fk') THEN\n ALTER TABLE ONLY public.plg_financial_invoices\n ADD CONSTRAINT plg_financial_invoices_legal_entity_fk\n FOREIGN KEY (company_legal_entity_id) REFERENCES public.company_legal_entities(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_invoices_legal_entity_idx\n ON public.plg_financial_invoices USING btree (tenant_id, company_legal_entity_id)\n WHERE company_legal_entity_id IS NOT NULL;\n\nCOMMENT ON COLUMN public.plg_financial_invoices.company_legal_entity_id IS\n 'O CNPJ que emitiu. Cada pessoa jurídica apura o SEU IBS/CBS — sem isto, um tenant com vários CNPJs não tem o que declarar (007).';\n\n-- O emitente sai da unidade quando ninguém disse outra coisa: é a unidade que\n-- tem CNPJ, e obrigar toda chamada a informá-lo faria as três portas de\n-- faturamento repetirem a mesma consulta — e esquecerem em lugares diferentes.\nCREATE OR REPLACE FUNCTION public.trg_financial_invoice_default_issuer()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nBEGIN\n IF NEW.company_legal_entity_id IS NULL AND NEW.unit_id IS NOT NULL THEN\n SELECT e.id INTO NEW.company_legal_entity_id\n FROM public.company_legal_entities e\n WHERE e.tenant_id = NEW.tenant_id AND e.unit_id = NEW.unit_id AND e.is_active\n ORDER BY e.created_at\n LIMIT 1;\n END IF;\n -- Ainda nulo é legítimo: um tenant que nunca cadastrou pessoa jurídica\n -- continua faturando internamente. O que ele não pode é emitir documento\n -- fiscal, e quem cobra isso é a emissão, não o título.\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS plg_financial_invoices_default_issuer ON public.plg_financial_invoices;\nCREATE TRIGGER plg_financial_invoices_default_issuer\n BEFORE INSERT ON public.plg_financial_invoices\n FOR EACH ROW EXECUTE FUNCTION public.trg_financial_invoice_default_issuer();\n\n-- O que já existe herda o emitente da unidade.\nUPDATE public.plg_financial_invoices f SET company_legal_entity_id = e.id\n FROM public.company_legal_entities e\n WHERE f.company_legal_entity_id IS NULL\n AND e.tenant_id = f.tenant_id AND e.unit_id = f.unit_id AND e.is_active;\n\n-- ── 2. um tributo por linha ─────────────────────────────────────────────────\nCREATE TABLE IF NOT EXISTS public.plg_financial_invoice_item_taxes (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n item_id uuid NOT NULL,\n -- Aberto de propósito, e não um enum: os tributos deste país mudam por lei,\n -- e a transição 2026–2033 tem anos em que sete convivem. Um CHECK com a\n -- lista de hoje viraria uma migration a cada etapa do cronograma.\n tax text NOT NULL,\n base_amount numeric(14,2) DEFAULT 0 NOT NULL,\n rate numeric(9,6) DEFAULT 0 NOT NULL,\n amount numeric(14,2) DEFAULT 0 NOT NULL,\n -- Situação tributária e, na reforma, a classificação (cClassTrib) que diz\n -- por que a alíquota é a que é — reduzida 30/60/100%, isenta, monofásica.\n cst text,\n classification text,\n -- Retenção: o tributo que QUEM PAGA recolhe no lugar de quem recebe (ISS,\n -- IRRF, INSS sobre serviço). Muda o líquido, não o valor da venda.\n is_withheld boolean DEFAULT false NOT NULL,\n -- Crédito apropriável pelo comprador. A não-cumulatividade plena é o\n -- coração do IBS/CBS: sem guardar o crédito, o comprador não tem o que\n -- abater e a cadeia quebra no elo seguinte.\n credit_amount numeric(14,2) DEFAULT 0 NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n owner_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_invoice_item_taxes'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_invoice_item_taxes\n ADD CONSTRAINT plg_financial_invoice_item_taxes_pkey PRIMARY KEY (id);\n END IF;\n -- Um tributo aparece UMA vez por linha. Duas linhas de CBS na mesma linha da\n -- fatura é a forma mais silenciosa de dobrar um imposto.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_invoice_item_taxes_item_tax_key') THEN\n ALTER TABLE ONLY public.plg_financial_invoice_item_taxes\n ADD CONSTRAINT plg_financial_invoice_item_taxes_item_tax_key UNIQUE (item_id, tax);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_invoice_item_taxes_tax_check') THEN\n ALTER TABLE ONLY public.plg_financial_invoice_item_taxes\n ADD CONSTRAINT plg_financial_invoice_item_taxes_tax_check CHECK (tax ~ '^[a-z][a-z0-9_]*$');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_invoice_item_taxes_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_invoice_item_taxes\n ADD CONSTRAINT plg_financial_invoice_item_taxes_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_invoice_item_taxes_item_fk') THEN\n ALTER TABLE ONLY public.plg_financial_invoice_item_taxes\n ADD CONSTRAINT plg_financial_invoice_item_taxes_item_fk\n FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_invoice_item_taxes_unit_fk') THEN\n ALTER TABLE ONLY public.plg_financial_invoice_item_taxes\n ADD CONSTRAINT plg_financial_invoice_item_taxes_unit_fk\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_invoice_item_taxes_owner_fk') THEN\n ALTER TABLE ONLY public.plg_financial_invoice_item_taxes\n ADD CONSTRAINT plg_financial_invoice_item_taxes_owner_fk\n FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_invoice_item_taxes_item_idx\n ON public.plg_financial_invoice_item_taxes USING btree (item_id);\nCREATE INDEX IF NOT EXISTS plg_financial_invoice_item_taxes_tenant_tax_idx\n ON public.plg_financial_invoice_item_taxes USING btree (tenant_id, tax);\nCREATE INDEX IF NOT EXISTS plg_financial_invoice_item_taxes_tenant_unit_idx\n ON public.plg_financial_invoice_item_taxes USING btree (tenant_id, unit_id);\n\nDROP TRIGGER IF EXISTS plg_financial_invoice_item_taxes_updated_at ON public.plg_financial_invoice_item_taxes;\nCREATE TRIGGER plg_financial_invoice_item_taxes_updated_at\n BEFORE UPDATE ON public.plg_financial_invoice_item_taxes\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nALTER TABLE public.plg_financial_invoice_item_taxes ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_invoice_item_taxes FORCE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_financial_invoice_item_taxes_authz_select ON public.plg_financial_invoice_item_taxes;\nCREATE POLICY plg_financial_invoice_item_taxes_authz_select ON public.plg_financial_invoice_item_taxes\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (unit_id IS NULL OR (SELECT app.has_unit(unit_id)))\n AND (SELECT app.has_permission('financial.read', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_invoice_item_taxes_authz_insert ON public.plg_financial_invoice_item_taxes;\nCREATE POLICY plg_financial_invoice_item_taxes_authz_insert ON public.plg_financial_invoice_item_taxes\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (unit_id IS NULL OR (SELECT app.has_unit(unit_id)))\n AND (SELECT app.has_permission('financial.create', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_invoice_item_taxes_authz_update ON public.plg_financial_invoice_item_taxes;\nCREATE POLICY plg_financial_invoice_item_taxes_authz_update ON public.plg_financial_invoice_item_taxes\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (unit_id IS NULL OR (SELECT app.has_unit(unit_id)))\n AND (SELECT app.has_permission('financial.edit', unit_id)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_invoice_item_taxes_authz_delete ON public.plg_financial_invoice_item_taxes;\nCREATE POLICY plg_financial_invoice_item_taxes_authz_delete ON public.plg_financial_invoice_item_taxes\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (unit_id IS NULL OR (SELECT app.has_unit(unit_id)))\n AND (SELECT app.has_permission('financial.delete', unit_id)));\n\nINSERT INTO app.scaffold_registry\n (table_schema, table_name, resource_type, plugin, owner_scoping, mode, previous_policies, audit_hook, scope, distributable)\nVALUES\n ('public', 'plg_financial_invoice_item_taxes', 'financial.invoice_item_tax', 'financial', false, 'enforce', ARRAY[]::text[], false, 'unit_optional', false)\nON CONFLICT DO NOTHING;\n\nGRANT ALL ON TABLE public.plg_financial_invoice_item_taxes TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_invoice_item_taxes TO service_role;\n\nCOMMENT ON TABLE public.plg_financial_invoice_item_taxes IS\n 'Um tributo por linha da fatura: base, alíquota, valor, CST/classificação, retenção e crédito. Uma linha por tributo porque CBS e IBS são dois, com cadeias de crédito separadas, e a transição 2026-2033 tem anos em que sete convivem (007).';\n\n-- ── o que a apuração lê ─────────────────────────────────────────────────────\n--\n-- Por fatura e por tributo: o que se deve, o que foi retido na fonte e o que\n-- gera crédito. É a forma que uma apuração mensal consome, e o que a emissão\n-- do documento precisa preencher campo a campo.\nCREATE OR REPLACE VIEW public.v_financial_invoice_taxes\nWITH (security_invoker = 'true') AS\nSELECT f.id AS invoice_id,\n f.tenant_id,\n f.unit_id,\n f.company_legal_entity_id,\n e.document_number AS issuer_document,\n e.tax_regime AS issuer_tax_regime,\n f.direction,\n f.competence_date,\n t.tax,\n sum(t.base_amount)::numeric(14,2) AS base_amount,\n sum(t.amount)::numeric(14,2) AS amount,\n sum(t.credit_amount)::numeric(14,2) AS credit_amount,\n sum(t.amount) FILTER (WHERE t.is_withheld)::numeric(14,2) AS withheld_amount\n FROM public.plg_financial_invoices f\n JOIN public.items i ON i.invoice_id = f.id\n JOIN public.plg_financial_invoice_item_taxes t ON t.item_id = i.id\n LEFT JOIN public.company_legal_entities e ON e.id = f.company_legal_entity_id\n WHERE f.cancelled_at IS NULL\n GROUP BY f.id, f.tenant_id, f.unit_id, f.company_legal_entity_id,\n e.document_number, e.tax_regime, f.direction, f.competence_date, t.tax;\n\nCOMMENT ON VIEW public.v_financial_invoice_taxes IS\n 'Apuração por fatura e tributo, com o CNPJ emitente e o regime — que é quem declara (007).';\n\nGRANT SELECT ON public.v_financial_invoice_taxes TO authenticated;\nGRANT SELECT ON public.v_financial_invoice_taxes TO service_role;\n\n-- ── a irmã que ninguém tinha declarado ──────────────────────────────────────\n--\n-- \\`plg_financial_invoice_items\\` — a extensão que carrega plano de contas e\n-- centro de custo da LINHA — tem RLS ligada, forçada e quatro policies, e\n-- mesmo assim não estava em \\`app.scaffold_registry\\`. Não é um furo: a policy\n-- dela delega ao pai (\"vê o acessório da linha que você já vê\"), e \\`items\\`\n-- decide. É uma AUSÊNCIA DE DECLARAÇÃO: o censo de authz não a enxergava, e\n-- nada podia afirmar que o acesso dela fora pensado.\n--\n-- Encontrada porque a asserção de 004 deixou de contar tabelas e passou a\n-- cobrar que nenhuma fique de fora — a primeira coisa que ela achou.\nINSERT INTO app.scaffold_registry\n (table_schema, table_name, resource_type, plugin, owner_scoping, mode, previous_policies, audit_hook, scope, distributable)\nVALUES\n ('public', 'plg_financial_invoice_items', 'financial.invoice_item', 'financial', false, 'enforce', ARRAY[]::text[], false, 'unit_optional', false)\nON CONFLICT DO NOTHING;\n`\n\nexport const MIGRATION_008_THE_COMMISSION_RULE_LEARNS_TO_CHOOSE_AND_TO_SPLIT = `-- 008_the_commission_rule_learns_to_choose_and_to_split.sql\n--\n-- A regra de comissão existe desde a 003 e sabe uma coisa só: um percentual.\n-- No V1 ela sabe DUAS outras, e são elas que fazem a comissão de um salão\n-- funcionar de verdade:\n--\n-- QUANDO ela se aplica — nove tipos de critério, e a de maior prioridade\n-- entre as que casam é a que vale. \"10% em coloração\", \"15% para a Marta\",\n-- \"5% no que for pago em cartão\", \"sábado paga mais\".\n--\n-- QUEM recebe — uma regra pode pagar VÁRIAS pessoas pelo mesmo serviço: o\n-- executor, a assistente, o fornecedor do produto. Cada uma com o seu\n-- percentual, a sua base e a sua faixa.\n--\n-- E falta o terceiro: QUEM EXECUTOU cada linha. Hoje a comissão sai de\n-- \\`order.total × uma taxa do profissional\\`, o que só funciona quando uma pessoa\n-- fez tudo sozinha. Uma comanda com corte da Marta e escova da Bia é uma conta\n-- só, e dividir o total pela metade é errado nas duas pontas.\n--\n-- POR QUE POR LINHA. É como o V1 apura, e é a única forma que responde\n-- \"quanto a Marta ganhou hoje\" numa venda com quatro itens de três pessoas. A\n-- linha já é o lugar certo: \\`public.items\\` tem \\`invoice_id\\` desde a fatura\n-- canônica, e \\`plg_financial_invoice_items\\` já pendura contabilidade nela.\n--\n-- O que este arquivo NÃO faz é apurar. Aqui a regra ganha expressão e a linha\n-- ganha dono; a geração do lançamento é a 009.\n\n-- ── a regra pode ser a padrão da casa ───────────────────────────────────────\nALTER TABLE public.plg_financial_commission_rules\n ADD COLUMN IF NOT EXISTS is_default boolean DEFAULT false NOT NULL;\n\nCOMMENT ON COLUMN public.plg_financial_commission_rules.is_default IS\n 'A regra que vale quando nenhuma outra casa. Sem ela, um serviço fora de todo critério não comissiona nada e ninguém descobre até o fim do mês (008).';\n\n-- ── 1. QUANDO: os critérios ─────────────────────────────────────────────────\nCREATE TABLE IF NOT EXISTS public.plg_financial_commission_rule_criteria (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n rule_id uuid NOT NULL,\n -- Os nove do V1. CHECK e não enum: acrescentar um décimo é um ALTER de\n -- constraint, não uma migração de tipo em toda coluna que o referencia.\n criteria_type text NOT NULL,\n -- A forma muda com o tipo, e por isso é jsonb: listas de id\n -- (\\`{\"ids\": [...]}\\`), dias da semana (\\`{\"days\": [1,6]}\\`), faixas de horário\n -- (\\`{\"ranges\": [{\"from\": \"18:00\", \"to\": \"22:00\"}]}\\`).\n criteria_value jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_commission_rule_criteria'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_commission_rule_criteria\n ADD CONSTRAINT plg_financial_commission_rule_criteria_pkey PRIMARY KEY (id);\n END IF;\n -- Um tipo de critério aparece UMA vez por regra: dois \"professional\" na mesma\n -- regra são duas listas concorrentes, e nenhuma resposta é a certa.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_rule_criteria_rule_type_key') THEN\n ALTER TABLE ONLY public.plg_financial_commission_rule_criteria\n ADD CONSTRAINT plg_financial_commission_rule_criteria_rule_type_key UNIQUE (rule_id, criteria_type);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_rule_criteria_type_check') THEN\n ALTER TABLE ONLY public.plg_financial_commission_rule_criteria\n ADD CONSTRAINT plg_financial_commission_rule_criteria_type_check\n CHECK (criteria_type = ANY (ARRAY['service', 'category', 'professional', 'professional_group',\n 'profession', 'unit', 'payment_method_type', 'weekday', 'time_range']));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_rule_criteria_rule_fk') THEN\n ALTER TABLE ONLY public.plg_financial_commission_rule_criteria\n ADD CONSTRAINT plg_financial_commission_rule_criteria_rule_fk\n FOREIGN KEY (rule_id) REFERENCES public.plg_financial_commission_rules(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_rule_criteria_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_commission_rule_criteria\n ADD CONSTRAINT plg_financial_commission_rule_criteria_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_commission_rule_criteria_rule_idx\n ON public.plg_financial_commission_rule_criteria USING btree (rule_id);\n\nDROP TRIGGER IF EXISTS plg_financial_commission_rule_criteria_updated_at ON public.plg_financial_commission_rule_criteria;\nCREATE TRIGGER plg_financial_commission_rule_criteria_updated_at\n BEFORE UPDATE ON public.plg_financial_commission_rule_criteria\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\n-- ── 2. QUEM RECEBE: as funções da regra ─────────────────────────────────────\nCREATE TABLE IF NOT EXISTS public.plg_financial_commission_functions (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n rule_id uuid NOT NULL,\n function_name text NOT NULL,\n -- Quem é o destinatário. \\`professional_executor\\` é o mais usado e o mais\n -- interessante: não nomeia ninguém — paga QUEM FEZ, resolvido por linha na\n -- apuração. Os outros nomeiam (\\`person_id\\`), ou perguntam no lançamento.\n account_type text DEFAULT 'professional_executor'::text NOT NULL,\n person_id uuid,\n value_type text DEFAULT 'percentage'::text NOT NULL,\n value_amount numeric(14,4) DEFAULT 0 NOT NULL,\n -- Bruto ou líquido de descontos. Muda quem paga o desconto: no líquido, o\n -- profissional divide a conta do desconto com a casa; no bruto, a casa\n -- banca sozinha. É decisão comercial, não detalhe de cálculo.\n calculation_base text DEFAULT 'net'::text NOT NULL,\n -- Faixa de produção: \\`marginal\\` paga cada faixa na alíquota dela;\n -- \\`retroactive\\` aplica a alíquota da faixa alcançada a TUDO; \\`guaranteed_min\\`\n -- garante um piso. As três existem no V1 e pagam valores diferentes pelo\n -- mesmo mês — não são variações de estilo.\n tier_mode text DEFAULT 'none'::text NOT NULL,\n min_guaranteed numeric(14,2),\n tiers jsonb DEFAULT '[]'::jsonb NOT NULL,\n is_variable boolean DEFAULT false NOT NULL,\n display_order integer DEFAULT 0 NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_commission_functions'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_commission_functions\n ADD CONSTRAINT plg_financial_commission_functions_pkey PRIMARY KEY (id);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_functions_account_type_check') THEN\n ALTER TABLE ONLY public.plg_financial_commission_functions\n ADD CONSTRAINT plg_financial_commission_functions_account_type_check\n CHECK (account_type = ANY (ARRAY['professional_executor', 'professional', 'supplier', 'select_on_launch']));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_functions_value_type_check') THEN\n ALTER TABLE ONLY public.plg_financial_commission_functions\n ADD CONSTRAINT plg_financial_commission_functions_value_type_check\n CHECK (value_type = ANY (ARRAY['percentage', 'fixed']));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_functions_base_check') THEN\n ALTER TABLE ONLY public.plg_financial_commission_functions\n ADD CONSTRAINT plg_financial_commission_functions_base_check\n CHECK (calculation_base = ANY (ARRAY['total', 'net']));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_functions_tier_mode_check') THEN\n ALTER TABLE ONLY public.plg_financial_commission_functions\n ADD CONSTRAINT plg_financial_commission_functions_tier_mode_check\n CHECK (tier_mode = ANY (ARRAY['none', 'marginal', 'retroactive', 'guaranteed_min']));\n END IF;\n -- Nomear uma pessoa é obrigatório justamente quando o tipo diz que há uma.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_functions_person_required_check') THEN\n ALTER TABLE ONLY public.plg_financial_commission_functions\n ADD CONSTRAINT plg_financial_commission_functions_person_required_check\n CHECK (account_type NOT IN ('professional', 'supplier') OR person_id IS NOT NULL);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_functions_rule_fk') THEN\n ALTER TABLE ONLY public.plg_financial_commission_functions\n ADD CONSTRAINT plg_financial_commission_functions_rule_fk\n FOREIGN KEY (rule_id) REFERENCES public.plg_financial_commission_rules(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_functions_person_fk') THEN\n ALTER TABLE ONLY public.plg_financial_commission_functions\n ADD CONSTRAINT plg_financial_commission_functions_person_fk\n FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_functions_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_commission_functions\n ADD CONSTRAINT plg_financial_commission_functions_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_commission_functions_rule_idx\n ON public.plg_financial_commission_functions USING btree (rule_id, display_order);\n\nDROP TRIGGER IF EXISTS plg_financial_commission_functions_updated_at ON public.plg_financial_commission_functions;\nCREATE TRIGGER plg_financial_commission_functions_updated_at\n BEFORE UPDATE ON public.plg_financial_commission_functions\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\n-- ── 3. QUEM EXECUTOU: os participantes da linha ─────────────────────────────\n--\n-- A peça que faltava. Sem ela a comissão é \\`order.total × taxa\\`, que só está\n-- certo quando uma pessoa fez tudo. Uma comanda com corte da Marta e escova da\n-- Bia é uma conta só, e metade para cada uma erra nas duas pontas.\nCREATE TABLE IF NOT EXISTS public.plg_financial_item_participants (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n item_id uuid NOT NULL,\n person_id uuid NOT NULL,\n -- \\`executor\\` é quem prestou; \\`assistant\\` ajudou. O papel importa porque a\n -- regra paga percentuais diferentes por papel.\n role text DEFAULT 'executor'::text NOT NULL,\n -- Quanto DA LINHA é desta pessoa, em fração (1.0 = a linha inteira). Duas\n -- pessoas a 0.5 dividem; um executor a 1.0 com uma assistente a 0.2 é a\n -- casa pagando 120% da linha em comissão, o que é uma decisão comercial\n -- legítima — e por isso a soma NÃO é forçada a 1.\n share numeric(9,6) DEFAULT 1 NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_item_participants'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_item_participants\n ADD CONSTRAINT plg_financial_item_participants_pkey PRIMARY KEY (id);\n END IF;\n -- A mesma pessoa no mesmo papel na mesma linha é a mesma participação.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_item_participants_item_person_role_key') THEN\n ALTER TABLE ONLY public.plg_financial_item_participants\n ADD CONSTRAINT plg_financial_item_participants_item_person_role_key UNIQUE (item_id, person_id, role);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_item_participants_role_check') THEN\n ALTER TABLE ONLY public.plg_financial_item_participants\n ADD CONSTRAINT plg_financial_item_participants_role_check\n CHECK (role = ANY (ARRAY['executor', 'assistant']));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_item_participants_share_check') THEN\n ALTER TABLE ONLY public.plg_financial_item_participants\n ADD CONSTRAINT plg_financial_item_participants_share_check CHECK (share > 0 AND share <= 1);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_item_participants_item_fk') THEN\n ALTER TABLE ONLY public.plg_financial_item_participants\n ADD CONSTRAINT plg_financial_item_participants_item_fk\n FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_item_participants_person_fk') THEN\n ALTER TABLE ONLY public.plg_financial_item_participants\n ADD CONSTRAINT plg_financial_item_participants_person_fk\n FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_item_participants_unit_fk') THEN\n ALTER TABLE ONLY public.plg_financial_item_participants\n ADD CONSTRAINT plg_financial_item_participants_unit_fk\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_item_participants_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_item_participants\n ADD CONSTRAINT plg_financial_item_participants_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_item_participants_item_idx\n ON public.plg_financial_item_participants USING btree (item_id);\n-- \"Quanto a Marta produziu este mês\" lê por pessoa.\nCREATE INDEX IF NOT EXISTS plg_financial_item_participants_person_idx\n ON public.plg_financial_item_participants USING btree (tenant_id, person_id);\n\nDROP TRIGGER IF EXISTS plg_financial_item_participants_updated_at ON public.plg_financial_item_participants;\nCREATE TRIGGER plg_financial_item_participants_updated_at\n BEFORE UPDATE ON public.plg_financial_item_participants\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\n-- ── RLS: escrita por extenso, e não num laço ────────────────────────────────\n--\n-- A primeira versão ligava RLS num \\`DO ... EXECUTE format(...)\\` sobre a lista\n-- de tabelas — menos texto, e invisível. O revisor de plugins leu o arquivo,\n-- não achou \\`ENABLE ROW LEVEL SECURITY\\` em lugar nenhum e reprovou: \"cria\n-- tabela com tenant_id e nunca liga row security\". Ele estava certo pelo\n-- motivo certo — quem revisa lê o mesmo texto, e uma proteção que só existe\n-- depois de um \\`format()\\` ser montado não é legível por ninguém.\n--\n-- Mais linhas, e cada uma diz o que faz.\n\nALTER TABLE public.plg_financial_commission_rule_criteria ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_commission_rule_criteria FORCE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_financial_commission_rule_criteria_authz_select ON public.plg_financial_commission_rule_criteria;\nCREATE POLICY plg_financial_commission_rule_criteria_authz_select ON public.plg_financial_commission_rule_criteria\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_rule_criteria_authz_insert ON public.plg_financial_commission_rule_criteria;\nCREATE POLICY plg_financial_commission_rule_criteria_authz_insert ON public.plg_financial_commission_rule_criteria\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.create', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_rule_criteria_authz_update ON public.plg_financial_commission_rule_criteria;\nCREATE POLICY plg_financial_commission_rule_criteria_authz_update ON public.plg_financial_commission_rule_criteria\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', NULL)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_rule_criteria_authz_delete ON public.plg_financial_commission_rule_criteria;\nCREATE POLICY plg_financial_commission_rule_criteria_authz_delete ON public.plg_financial_commission_rule_criteria\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.delete', NULL)));\n\nGRANT ALL ON TABLE public.plg_financial_commission_rule_criteria TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_commission_rule_criteria TO service_role;\n\nALTER TABLE public.plg_financial_commission_functions ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_commission_functions FORCE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_financial_commission_functions_authz_select ON public.plg_financial_commission_functions;\nCREATE POLICY plg_financial_commission_functions_authz_select ON public.plg_financial_commission_functions\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_functions_authz_insert ON public.plg_financial_commission_functions;\nCREATE POLICY plg_financial_commission_functions_authz_insert ON public.plg_financial_commission_functions\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.create', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_functions_authz_update ON public.plg_financial_commission_functions;\nCREATE POLICY plg_financial_commission_functions_authz_update ON public.plg_financial_commission_functions\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', NULL)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_functions_authz_delete ON public.plg_financial_commission_functions;\nCREATE POLICY plg_financial_commission_functions_authz_delete ON public.plg_financial_commission_functions\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.delete', NULL)));\n\nGRANT ALL ON TABLE public.plg_financial_commission_functions TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_commission_functions TO service_role;\n\nALTER TABLE public.plg_financial_item_participants ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_item_participants FORCE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_financial_item_participants_authz_select ON public.plg_financial_item_participants;\nCREATE POLICY plg_financial_item_participants_authz_select ON public.plg_financial_item_participants\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_item_participants_authz_insert ON public.plg_financial_item_participants;\nCREATE POLICY plg_financial_item_participants_authz_insert ON public.plg_financial_item_participants\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.create', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_item_participants_authz_update ON public.plg_financial_item_participants;\nCREATE POLICY plg_financial_item_participants_authz_update ON public.plg_financial_item_participants\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', NULL)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_item_participants_authz_delete ON public.plg_financial_item_participants;\nCREATE POLICY plg_financial_item_participants_authz_delete ON public.plg_financial_item_participants\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.delete', NULL)));\n\nGRANT ALL ON TABLE public.plg_financial_item_participants TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_item_participants TO service_role;\n\nINSERT INTO app.scaffold_registry\n (table_schema, table_name, resource_type, plugin, owner_scoping, mode, previous_policies, audit_hook, scope, distributable)\nVALUES\n ('public', 'plg_financial_commission_rule_criteria', 'financial.commission_criteria', 'financial', false, 'enforce', ARRAY[]::text[], false, 'tenant', false),\n ('public', 'plg_financial_commission_functions', 'financial.commission_function', 'financial', false, 'enforce', ARRAY[]::text[], false, 'tenant', false),\n ('public', 'plg_financial_item_participants', 'financial.item_participant', 'financial', false, 'enforce', ARRAY[]::text[], false, 'unit_optional', false)\nON CONFLICT DO NOTHING;\n\nCOMMENT ON TABLE public.plg_financial_commission_rule_criteria IS\n 'QUANDO a regra vale: serviço, categoria, profissional, grupo, profissão, unidade, forma de pagamento, dia da semana, faixa de horário (008).';\nCOMMENT ON TABLE public.plg_financial_commission_functions IS\n 'QUEM a regra paga: uma regra pode pagar várias pessoas pelo mesmo serviço, cada uma com percentual, base e faixa próprios (008).';\nCOMMENT ON TABLE public.plg_financial_item_participants IS\n 'QUEM EXECUTOU cada linha, e em que fração. Sem isto a comissão é order.total × taxa, que só está certo quando uma pessoa fez tudo (008).';\n`\n\nexport const MIGRATION_009_THE_COMMISSION_IS_CALCULATED_LINE_BY_LINE = `-- 009_the_commission_is_calculated_line_by_line.sql\n--\n-- A regra sabe quando se aplica e quem paga (008); a linha sabe quem executou.\n-- Falta apurar — transformar isso em VALOR, por linha e por pessoa, de forma\n-- que alguém possa conferir cada centavo até a venda que o originou.\n--\n-- O LANÇAMENTO É POR LINHA E POR PESSOA. É a menor unidade que responde às\n-- perguntas reais: \"quanto a Marta ganhou hoje\", \"por que este mês veio menos\",\n-- \"esta comissão saiu de qual atendimento\". Um total por profissional por mês\n-- responde a primeira e nenhuma das outras.\n--\n-- A APURAÇÃO É UM LOTE, e o lote é o que se confere e se paga. Sem ele, um\n-- reprocessamento duplica silenciosamente e a conferência do mês passa a ser\n-- uma pergunta sobre quando cada linha foi gerada.\n--\n-- O QUE ELA NÃO FAZ AINDA, e de propósito:\n--\n-- · faixas de produção (\\`tier_mode\\`). Marginal, retroativa e piso garantido\n-- dependem do ACUMULADO do profissional no período, e um acumulado só é\n-- conhecido depois que o período fecha — é outra passada, sobre o mesmo\n-- lançamento, e ela merece o próprio arquivo em vez de virar um \\`CASE\\`\n-- dentro deste.\n-- · pagar. Comissão apurada é conta a pagar ao profissional, e juntar N\n-- comissões num pagamento só é o borderô — outro conceito, com outra tela.\n\n-- ── o lote ──────────────────────────────────────────────────────────────────\nCREATE TABLE IF NOT EXISTS public.plg_financial_commission_generations (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n period_start date NOT NULL,\n period_end date NOT NULL,\n status text DEFAULT 'draft'::text NOT NULL,\n total_amount numeric(14,2) DEFAULT 0 NOT NULL,\n entry_count integer DEFAULT 0 NOT NULL,\n generated_by_user_id uuid,\n notes text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_commission_generations'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_commission_generations\n ADD CONSTRAINT plg_financial_commission_generations_pkey PRIMARY KEY (id);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_generations_status_check') THEN\n ALTER TABLE ONLY public.plg_financial_commission_generations\n ADD CONSTRAINT plg_financial_commission_generations_status_check\n CHECK (status = ANY (ARRAY['draft', 'approved', 'cancelled']));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_generations_period_check') THEN\n ALTER TABLE ONLY public.plg_financial_commission_generations\n ADD CONSTRAINT plg_financial_commission_generations_period_check CHECK (period_end >= period_start);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_generations_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_commission_generations\n ADD CONSTRAINT plg_financial_commission_generations_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_commission_generations_tenant_period_idx\n ON public.plg_financial_commission_generations USING btree (tenant_id, period_start DESC);\n\nDROP TRIGGER IF EXISTS plg_financial_commission_generations_updated_at ON public.plg_financial_commission_generations;\nCREATE TRIGGER plg_financial_commission_generations_updated_at\n BEFORE UPDATE ON public.plg_financial_commission_generations\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\n-- ── o lançamento ────────────────────────────────────────────────────────────\nCREATE TABLE IF NOT EXISTS public.plg_financial_commission_entries (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n generation_id uuid,\n item_id uuid NOT NULL,\n person_id uuid NOT NULL,\n rule_id uuid,\n function_id uuid,\n -- A conta inteira, guardada: base × alíquota = valor. Guardar só o valor\n -- torna \"por que deu isso\" uma reconstituição a partir de regras que podem\n -- ter mudado desde então — e a pergunta aparece justamente quando mudaram.\n base_amount numeric(14,2) DEFAULT 0 NOT NULL,\n rate numeric(14,4) DEFAULT 0 NOT NULL,\n amount numeric(14,2) DEFAULT 0 NOT NULL,\n share numeric(9,6) DEFAULT 1 NOT NULL,\n role text,\n status text DEFAULT 'pending'::text NOT NULL,\n -- A conta a pagar que quitou esta comissão, quando houver. É o elo entre a\n -- apuração e o dinheiro que saiu.\n payable_invoice_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_commission_entries'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_commission_entries\n ADD CONSTRAINT plg_financial_commission_entries_pkey PRIMARY KEY (id);\n END IF;\n -- A trava contra a duplicata: uma linha paga uma pessoa por uma função UMA\n -- vez. Reprocessar o mês não pode pagar duas.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_entries_item_person_function_key') THEN\n ALTER TABLE ONLY public.plg_financial_commission_entries\n ADD CONSTRAINT plg_financial_commission_entries_item_person_function_key\n UNIQUE (item_id, person_id, function_id);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_entries_status_check') THEN\n ALTER TABLE ONLY public.plg_financial_commission_entries\n ADD CONSTRAINT plg_financial_commission_entries_status_check\n CHECK (status = ANY (ARRAY['pending', 'approved', 'paid', 'cancelled']));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_entries_item_fk') THEN\n ALTER TABLE ONLY public.plg_financial_commission_entries\n ADD CONSTRAINT plg_financial_commission_entries_item_fk\n FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_entries_person_fk') THEN\n ALTER TABLE ONLY public.plg_financial_commission_entries\n ADD CONSTRAINT plg_financial_commission_entries_person_fk\n FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE CASCADE;\n END IF;\n -- A regra pode ser apagada depois; o lançamento não some junto, porque ele é\n -- o registro do que FOI pago e a regra é só o motivo.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_entries_rule_fk') THEN\n ALTER TABLE ONLY public.plg_financial_commission_entries\n ADD CONSTRAINT plg_financial_commission_entries_rule_fk\n FOREIGN KEY (rule_id) REFERENCES public.plg_financial_commission_rules(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_entries_function_fk') THEN\n ALTER TABLE ONLY public.plg_financial_commission_entries\n ADD CONSTRAINT plg_financial_commission_entries_function_fk\n FOREIGN KEY (function_id) REFERENCES public.plg_financial_commission_functions(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_entries_generation_fk') THEN\n ALTER TABLE ONLY public.plg_financial_commission_entries\n ADD CONSTRAINT plg_financial_commission_entries_generation_fk\n FOREIGN KEY (generation_id) REFERENCES public.plg_financial_commission_generations(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_entries_payable_fk') THEN\n ALTER TABLE ONLY public.plg_financial_commission_entries\n ADD CONSTRAINT plg_financial_commission_entries_payable_fk\n FOREIGN KEY (payable_invoice_id) REFERENCES public.plg_financial_invoices(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_entries_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_commission_entries\n ADD CONSTRAINT plg_financial_commission_entries_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_commission_entries_person_idx\n ON public.plg_financial_commission_entries USING btree (tenant_id, person_id, status);\nCREATE INDEX IF NOT EXISTS plg_financial_commission_entries_generation_idx\n ON public.plg_financial_commission_entries USING btree (generation_id);\nCREATE INDEX IF NOT EXISTS plg_financial_commission_entries_item_idx\n ON public.plg_financial_commission_entries USING btree (item_id);\n\nDROP TRIGGER IF EXISTS plg_financial_commission_entries_updated_at ON public.plg_financial_commission_entries;\nCREATE TRIGGER plg_financial_commission_entries_updated_at\n BEFORE UPDATE ON public.plg_financial_commission_entries\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nALTER TABLE public.plg_financial_commission_generations ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_commission_generations FORCE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_financial_commission_generations_authz_select ON public.plg_financial_commission_generations;\nCREATE POLICY plg_financial_commission_generations_authz_select ON public.plg_financial_commission_generations\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_generations_authz_insert ON public.plg_financial_commission_generations;\nCREATE POLICY plg_financial_commission_generations_authz_insert ON public.plg_financial_commission_generations\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.create', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_generations_authz_update ON public.plg_financial_commission_generations;\nCREATE POLICY plg_financial_commission_generations_authz_update ON public.plg_financial_commission_generations\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', NULL)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_generations_authz_delete ON public.plg_financial_commission_generations;\nCREATE POLICY plg_financial_commission_generations_authz_delete ON public.plg_financial_commission_generations\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.delete', NULL)));\n\nGRANT ALL ON TABLE public.plg_financial_commission_generations TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_commission_generations TO service_role;\n\nALTER TABLE public.plg_financial_commission_entries ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_commission_entries FORCE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_financial_commission_entries_authz_select ON public.plg_financial_commission_entries;\nCREATE POLICY plg_financial_commission_entries_authz_select ON public.plg_financial_commission_entries\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_entries_authz_insert ON public.plg_financial_commission_entries;\nCREATE POLICY plg_financial_commission_entries_authz_insert ON public.plg_financial_commission_entries\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.create', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_entries_authz_update ON public.plg_financial_commission_entries;\nCREATE POLICY plg_financial_commission_entries_authz_update ON public.plg_financial_commission_entries\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', NULL)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', NULL)));\n\nDROP POLICY IF EXISTS plg_financial_commission_entries_authz_delete ON public.plg_financial_commission_entries;\nCREATE POLICY plg_financial_commission_entries_authz_delete ON public.plg_financial_commission_entries\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.delete', NULL)));\n\nGRANT ALL ON TABLE public.plg_financial_commission_entries TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_commission_entries TO service_role;\n\nINSERT INTO app.scaffold_registry\n (table_schema, table_name, resource_type, plugin, owner_scoping, mode, previous_policies, audit_hook, scope, distributable)\nVALUES\n ('public', 'plg_financial_commission_generations', 'financial.commission_generation', 'financial', false, 'enforce', ARRAY[]::text[], true, 'unit_optional', false),\n ('public', 'plg_financial_commission_entries', 'financial.commission_entry', 'financial', false, 'enforce', ARRAY[]::text[], true, 'unit_optional', false)\nON CONFLICT DO NOTHING;\n\n-- ── qual regra vale para esta linha ─────────────────────────────────────────\n--\n-- A de maior prioridade cujos critérios TODOS casam. Um critério ausente não\n-- restringe: uma regra sem critério de serviço vale para qualquer serviço — é\n-- assim que \"10% para a Marta\" funciona sem listar os cem serviços dela.\n-- Nenhuma casando, vale a \\`is_default\\`, e nenhuma padrão significa que aquele\n-- serviço não comissiona.\nCREATE OR REPLACE FUNCTION public.plg_financial_commission_rule_for_item(p_item_id uuid, p_person_id uuid)\n RETURNS uuid\n LANGUAGE plpgsql\n STABLE\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_item record;\n v_rule uuid;\nBEGIN\n SELECT i.id, i.tenant_id, i.unit_id, i.product_id, i.order_id, i.invoice_id,\n coalesce(o.created_at, i.created_at) AS happened_at,\n p.category_id\n INTO v_item\n FROM public.items i\n LEFT JOIN public.orders o ON o.id = coalesce(i.order_id,\n (SELECT f2.order_id FROM public.plg_financial_invoices f2 WHERE f2.id = i.invoice_id))\n LEFT JOIN public.products p ON p.id = i.product_id\n WHERE i.id = p_item_id;\n IF v_item.id IS NULL THEN RETURN NULL; END IF;\n\n SELECT r.id INTO v_rule\n FROM public.plg_financial_commission_rules r\n WHERE r.tenant_id = v_item.tenant_id\n AND r.is_active\n AND (r.unit_id IS NULL OR r.unit_id = v_item.unit_id)\n AND NOT EXISTS (\n SELECT 1 FROM public.plg_financial_commission_rule_criteria c\n WHERE c.rule_id = r.id\n AND NOT CASE c.criteria_type\n WHEN 'service' THEN\n v_item.product_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'category' THEN\n v_item.category_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'professional' THEN\n p_person_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'unit' THEN\n v_item.unit_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'weekday' THEN\n (extract(dow FROM v_item.happened_at))::int IN (\n SELECT (jsonb_array_elements_text(coalesce(c.criteria_value -> 'days', '[]'::jsonb)))::int)\n -- Os critérios que dependem de cadastros de OUTROS módulos (grupo,\n -- profissão) e o de forma de pagamento — que só se conhece quando o\n -- pagamento acontece, e não quando a linha nasce — ainda não\n -- restringem. Deixar passar é o comportamento seguro: comissiona a\n -- mais, e alguém vê. Recusar em silêncio não comissiona e ninguém vê.\n ELSE true\n END)\n ORDER BY r.priority DESC, r.is_default ASC, r.created_at\n LIMIT 1;\n\n IF v_rule IS NOT NULL THEN RETURN v_rule; END IF;\n\n SELECT r.id INTO v_rule FROM public.plg_financial_commission_rules r\n WHERE r.tenant_id = v_item.tenant_id AND r.is_active AND r.is_default\n ORDER BY r.priority DESC LIMIT 1;\n RETURN v_rule;\nEND $$;\n\nCOMMENT ON FUNCTION public.plg_financial_commission_rule_for_item(uuid, uuid) IS\n 'A regra de maior prioridade cujos critérios todos casam; senão a padrão da casa. Critério ausente não restringe (009).';\n\n-- ── apurar um período ───────────────────────────────────────────────────────\nCREATE OR REPLACE FUNCTION public.financial_commission_generate(\n p_period_start date,\n p_period_end date,\n p_unit_id uuid DEFAULT NULL\n) RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_gen uuid;\n v_row record;\n v_rule uuid;\n v_fn record;\n v_base numeric;\n v_amount numeric;\n v_count int := 0;\n v_total numeric := 0;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_commission_generate: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.commissions.manage', p_unit_id) THEN\n RAISE EXCEPTION 'financial_commission_generate: exige financial.commissions.manage' USING ERRCODE = '42501';\n END IF;\n\n INSERT INTO public.plg_financial_commission_generations\n (tenant_id, unit_id, period_start, period_end, status, generated_by_user_id)\n VALUES (v_tenant, p_unit_id, p_period_start, p_period_end, 'draft', auth.uid())\n RETURNING id INTO v_gen;\n\n -- Só linha de fatura NÃO cancelada: comissão sai de venda que existe. E o\n -- período é o da COMPETÊNCIA da fatura, não o da geração — reprocessar\n -- fevereiro em março não pode mudar de mês.\n -- O participante mora na linha do PEDIDO: é lá que se sabe quem prestou, e\n -- se sabe na hora da prestação. A fatura chega pelo pedido, que é o elo que\n -- \\`items_one_parent\\` deixa existir. Só fatura não cancelada — comissão sai de\n -- venda que existe — e a competência é a DELA: reapurar fevereiro em março\n -- não pode mudar de mês.\n FOR v_row IN\n SELECT i.id AS item_id, i.tenant_id, i.unit_id, i.total, i.discount,\n part.person_id, part.share, part.role\n FROM public.items i\n JOIN public.orders o ON o.id = i.order_id\n JOIN public.plg_financial_invoices f ON f.order_id = o.id AND f.cancelled_at IS NULL\n JOIN public.plg_financial_item_participants part ON part.item_id = i.id\n WHERE i.tenant_id = v_tenant\n AND (p_unit_id IS NULL OR i.unit_id = p_unit_id)\n AND f.competence_date BETWEEN p_period_start AND p_period_end\n LOOP\n v_rule := public.plg_financial_commission_rule_for_item(v_row.item_id, v_row.person_id);\n CONTINUE WHEN v_rule IS NULL;\n\n -- UMA REGRA SEM FUNÇÃO PAGA QUEM EXECUTOU.\n --\n -- A regra diz QUANTO; quem diz A QUEM é a função, e é nela que o laço\n -- abaixo itera — sem função ele não roda nenhuma vez e a regra não paga\n -- ninguém. A tela cadastra a REGRA e não existe tela para função, então\n -- toda regra criada pela interface era, por construção, incapaz de pagar.\n --\n -- A semeadura é aqui, na APURAÇÃO, e não num gatilho de cadastro: no\n -- AFTER INSERT da regra as funções ainda não existem, e o default entraria\n -- por cima de quem estava prestes a declarar as suas.\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_commission_functions f\n WHERE f.rule_id = v_rule) THEN\n INSERT INTO public.plg_financial_commission_functions\n (tenant_id, rule_id, function_name, account_type, value_type, value_amount,\n calculation_base, display_order, is_active)\n SELECT r.tenant_id, r.id, coalesce(nullif(r.name, ''), 'Executor'),\n 'professional_executor', r.value_type, r.value, 'net', 0, true\n FROM public.plg_financial_commission_rules r\n WHERE r.id = v_rule;\n END IF;\n\n FOR v_fn IN\n SELECT * FROM public.plg_financial_commission_functions\n WHERE rule_id = v_rule AND is_active ORDER BY display_order\n LOOP\n -- \\`professional_executor\\` paga quem fez; os que nomeiam pagam o nomeado.\n -- \\`select_on_launch\\` espera alguém dizer, e não se apura sozinho.\n CONTINUE WHEN v_fn.account_type = 'select_on_launch';\n\n v_base := CASE v_fn.calculation_base\n WHEN 'total' THEN coalesce(v_row.total, 0)\n ELSE coalesce(v_row.total, 0) - coalesce(v_row.discount, 0)\n END * coalesce(v_row.share, 1);\n\n v_amount := round(CASE v_fn.value_type\n WHEN 'fixed' THEN v_fn.value_amount\n ELSE v_base * (v_fn.value_amount / 100)\n END, 2);\n CONTINUE WHEN v_amount <= 0;\n\n INSERT INTO public.plg_financial_commission_entries\n (tenant_id, unit_id, generation_id, item_id, person_id, rule_id, function_id,\n base_amount, rate, amount, share, role, status)\n VALUES\n (v_tenant, v_row.unit_id, v_gen, v_row.item_id,\n CASE WHEN v_fn.account_type = 'professional_executor' THEN v_row.person_id ELSE v_fn.person_id END,\n v_rule, v_fn.id, round(v_base, 2), v_fn.value_amount, v_amount,\n coalesce(v_row.share, 1), v_row.role, 'pending')\n -- Reprocessar não paga duas vezes: recalcula o que já existe. O\n -- \\`generation_id\\` NÃO é sobrescrito — o lançamento pertence ao lote que\n -- o produziu, e recálculo muda o VALOR, não a procedência. Trocá-lo\n -- esvaziaria o lote que alguém já conferiu, e a conferência do mês\n -- viraria uma pergunta sobre qual reapuração olhar.\n ON CONFLICT (item_id, person_id, function_id) DO UPDATE\n SET base_amount = EXCLUDED.base_amount, rate = EXCLUDED.rate, amount = EXCLUDED.amount,\n updated_at = now()\n WHERE public.plg_financial_commission_entries.status = 'pending';\n\n v_count := v_count + 1;\n v_total := v_total + v_amount;\n END LOOP;\n END LOOP;\n\n UPDATE public.plg_financial_commission_generations\n SET entry_count = v_count, total_amount = v_total, updated_at = now()\n WHERE id = v_gen;\n\n RETURN jsonb_build_object('generation_id', v_gen, 'period_start', p_period_start,\n 'period_end', p_period_end, 'entries', v_count, 'total', v_total);\nEND $$;\n\nREVOKE ALL ON FUNCTION public.financial_commission_generate(date, date, uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_commission_generate(date, date, uuid) TO authenticated;\n\n-- ── o que a tela e o relatório leem ─────────────────────────────────────────\nCREATE OR REPLACE VIEW public.v_financial_commissions\nWITH (security_invoker = 'true') AS\nSELECT e.id, e.tenant_id, e.unit_id, e.generation_id, e.item_id, e.person_id,\n pe.name AS person_name,\n i.name AS item_name,\n i.order_id,\n f.id AS invoice_id,\n f.number AS invoice_number,\n f.competence_date,\n r.name AS rule_name,\n fn.function_name,\n e.base_amount, e.rate, e.amount, e.share, e.role, e.status, e.payable_invoice_id,\n e.created_at, e.updated_at\n FROM public.plg_financial_commission_entries e\n JOIN public.items i ON i.id = e.item_id\n -- A linha do lançamento é a do PEDIDO, e uma linha tem um pai só: a fatura\n -- vem pelo pedido. \\`coalesce\\` cobre também o caso de a comissão vir de uma\n -- linha que já é da fatura (uma despesa lançada à mão que comissiona).\n LEFT JOIN public.plg_financial_invoices f\n ON f.id = coalesce(i.invoice_id, (SELECT f2.id FROM public.plg_financial_invoices f2\n WHERE f2.order_id = i.order_id AND f2.cancelled_at IS NULL\n ORDER BY f2.created_at LIMIT 1))\n LEFT JOIN public.people pe ON pe.id = e.person_id\n LEFT JOIN public.plg_financial_commission_rules r ON r.id = e.rule_id\n LEFT JOIN public.plg_financial_commission_functions fn ON fn.id = e.function_id;\n\nCOMMENT ON VIEW public.v_financial_commissions IS\n 'A comissão com a venda que a originou: linha, fatura, regra e função. É o que responde \"esta comissão saiu de qual atendimento\" (009).';\n\nGRANT SELECT ON public.v_financial_commissions TO authenticated;\nGRANT SELECT ON public.v_financial_commissions TO service_role;\n`\n\nexport const MIGRATION_010_THE_INVOICE_RAISED_FROM_A_SALE_GETS_ITS_OWN_LINES = `-- 010_the_invoice_raised_from_a_sale_gets_its_own_lines.sql\n--\n-- Uma fatura levantada a partir de uma venda NÃO TINHA LINHAS. Só um texto de\n-- resumo em \\`description\\` — \"Corte, Escova\" — e nada mais.\n--\n-- Isso não incomodava enquanto ninguém lia a fatura por linha. Agora três\n-- coisas leem, e as três encontravam vazio na maioria das faturas:\n--\n-- · \\`plg_financial_invoice_items\\` pendura plano de contas e centro de custo\n-- NA LINHA, e a apuração contábil entra pela fatura;\n-- · \\`plg_financial_invoice_item_taxes\\` (007) guarda CBS e IBS por linha;\n-- · a comissão (009) apura por linha — e devolveu ZERO lançamento num\n-- cenário com duas linhas e dois profissionais. Foi assim que apareceu.\n--\n-- POR QUE COPIAR, e não apontar. A primeira tentativa foi carimbar\n-- \\`items.invoice_id\\` na linha do pedido, e o banco recusou:\n-- \\`items_one_parent\\` exige \\`num_nonnulls(order_id, invoice_id,\n-- fulfillment_id, quote_id) = 1\\`. Uma linha tem UM pai, e isso é desenho, não\n-- obstáculo — a linha do pedido é o que foi VENDIDO, a linha da fatura é o que\n-- foi COBRADO, e as duas divergem no mundo real: fatura-se parcialmente,\n-- agrupa-se, corrige-se preço.\n--\n-- \\`item_associations\\` existe exatamente para isso — \\`from_item_id\\` (a venda)\n-- → \\`to_item_id\\` (a cobrança), com quantidade. A tabela estava lá e ninguém a\n-- alimentava; \\`v_financial_invoice_lines\\` já lê por ela.\n--\n-- Então: a fatura ganha as linhas dela, associadas às do pedido. Faturar tudo\n-- é o caso simples de um modelo que sabe fazer mais.\n\n-- ── as duas pontes passam a compor as linhas da fatura ─────────────────────\nCREATE OR REPLACE FUNCTION public.plg_financial_copy_order_lines_to_invoice(p_order_id uuid, p_invoice_id uuid)\n RETURNS integer\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE v_n integer := 0; v_row record; v_new uuid;\nBEGIN\n -- Já composta: não duplica. Reexecutar uma ponte idempotente é o caminho\n -- normal de reparo, e ele não pode cobrar duas vezes.\n IF EXISTS (SELECT 1 FROM public.items WHERE invoice_id = p_invoice_id) THEN\n RETURN 0;\n END IF;\n\n FOR v_row IN\n SELECT * FROM public.items WHERE order_id = p_order_id ORDER BY sort_order, created_at\n LOOP\n INSERT INTO public.items\n (tenant_id, unit_id, invoice_id, product_id, name, description,\n quantity, unit_price, discount, total, sort_order, metadata)\n VALUES\n (v_row.tenant_id, v_row.unit_id, p_invoice_id, v_row.product_id,\n v_row.name, v_row.description, v_row.quantity, v_row.unit_price,\n v_row.discount, v_row.total, v_row.sort_order, coalesce(v_row.metadata, '{}'::jsonb))\n RETURNING id INTO v_new;\n\n -- O elo entre o que foi vendido e o que foi cobrado. É por aqui que\n -- v_financial_invoice_lines volta da cobrança para a venda.\n INSERT INTO public.item_associations (tenant_id, unit_id, from_item_id, to_item_id, quantity)\n VALUES (v_row.tenant_id, v_row.unit_id, v_row.id, v_new, v_row.quantity);\n\n v_n := v_n + 1;\n END LOOP;\n\n RETURN v_n;\nEND $$;\n\nCOMMENT ON FUNCTION public.plg_financial_copy_order_lines_to_invoice(uuid, uuid) IS\n 'Compõe as linhas da fatura a partir das do pedido, com a associação venda→cobrança. Uma linha tem um pai só (items_one_parent), então cobrar é copiar (010).';\n\n-- A ponte da loja e da agenda.\nCREATE OR REPLACE FUNCTION public.fn_invoice_from_order_internal(\n p_order_id uuid,\n p_due_date date DEFAULT NULL::date,\n p_status text DEFAULT NULL::text,\n p_installments integer DEFAULT 1,\n p_direction text DEFAULT 'credit'::text\n) RETURNS uuid\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_order public.orders%ROWTYPE;\n v_invoice_id uuid;\n v_dir text;\n v_number text;\n v_summary text;\n v_contact text;\n v_n int := GREATEST(COALESCE(p_installments, 1), 1);\n v_total numeric;\n v_base numeric;\n v_amt numeric;\n v_alloc numeric := 0;\n v_due date;\n v_status text;\n i int;\nBEGIN\n SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;\n IF NOT FOUND THEN RAISE EXCEPTION 'fn_invoice_from_order: order % not found', p_order_id; END IF;\n\n SELECT id INTO v_invoice_id FROM public.plg_financial_invoices\n WHERE order_id = p_order_id AND cancelled_at IS NULL LIMIT 1;\n IF v_invoice_id IS NOT NULL THEN\n -- Idempotente, e ainda assim carimba: um pedido faturado ANTES da 010 tem\n -- fatura e não tem o elo, e reexecutar é o caminho natural de reparo.\n PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);\n RETURN p_order_id;\n END IF;\n IF EXISTS (SELECT 1 FROM public.plg_financial_movements\n WHERE invoice_id = p_order_id AND movement_kind = 'bill'\n AND status <> 'cancelled') THEN\n RETURN p_order_id;\n END IF;\n\n v_dir := CASE WHEN p_direction = 'debit' THEN 'payable' ELSE 'receivable' END;\n v_number := public.plg_financial_next_invoice_number(v_order.tenant_id, v_dir);\n\n SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary\n FROM public.items WHERE order_id = p_order_id;\n SELECT name INTO v_contact FROM public.people WHERE id = v_order.party_id;\n\n v_status := v_order.status;\n IF p_status IS NOT NULL AND EXISTS (\n SELECT 1 FROM public.order_kinds k\n WHERE k.kind = v_order.kind\n AND (k.statuses IS NULL OR p_status = ANY (k.statuses))\n ) THEN\n v_status := p_status;\n END IF;\n\n UPDATE public.orders SET\n status = v_status,\n stage = 'invoiced',\n direction = p_direction,\n metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object(\n 'itemsSummary', COALESCE(v_summary, ''),\n 'contactName', v_contact,\n 'installmentCount', v_n,\n 'quoteNumber', v_order.reference_number,\n 'direction', p_direction,\n 'invoiceNumber', v_number\n ),\n updated_at = now()\n WHERE id = p_order_id;\n\n v_total := round(COALESCE(v_order.total, 0), 2);\n v_due := COALESCE(p_due_date, current_date);\n\n INSERT INTO public.plg_financial_invoices\n (tenant_id, unit_id, direction, counterparty_person_id, order_id, number, competence_date, issue_date,\n total, description, metadata, created_by)\n VALUES\n (v_order.tenant_id, v_order.unit_id, v_dir, v_order.party_id, p_order_id, v_number,\n COALESCE(v_order.created_at::date, current_date), current_date,\n v_total, NULLIF(v_summary, ''),\n jsonb_build_object('source', 'fn_invoice_from_order', 'orderKind', v_order.kind),\n auth.uid())\n RETURNING id INTO v_invoice_id;\n\n PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);\n\n v_base := round(v_total / v_n, 2);\n FOR i IN 1..v_n LOOP\n IF i = v_n THEN v_amt := round(v_total - v_alloc, 2);\n ELSE v_amt := v_base; v_alloc := v_alloc + v_base; END IF;\n\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount)\n VALUES (v_order.tenant_id, v_order.unit_id, v_invoice_id, i,\n (v_due + ((i - 1) || ' months')::interval)::date, v_amt);\n END LOOP;\n\n RETURN p_order_id;\nEND;\n$$;\n\n-- A ponte da comanda e do atendimento.\nCREATE OR REPLACE FUNCTION public.order_raise_receivable(\n p_order_id uuid,\n p_due_date date DEFAULT NULL\n) RETURNS uuid\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_order public.orders%ROWTYPE;\n v_invoice_id uuid;\n v_number text;\n v_summary text;\n v_total numeric;\nBEGIN\n SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;\n IF NOT FOUND THEN RETURN NULL; END IF;\n\n SELECT id INTO v_invoice_id FROM public.plg_financial_invoices\n WHERE order_id = p_order_id AND cancelled_at IS NULL LIMIT 1;\n IF v_invoice_id IS NOT NULL THEN\n PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);\n RETURN v_invoice_id;\n END IF;\n\n v_total := round(coalesce(v_order.total, 0), 2);\n IF v_total <= 0 THEN RETURN NULL; END IF;\n\n SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary\n FROM public.items WHERE order_id = p_order_id;\n\n v_number := public.plg_financial_next_invoice_number(v_order.tenant_id, 'receivable');\n\n INSERT INTO public.plg_financial_invoices\n (tenant_id, unit_id, direction, counterparty_person_id, order_id, number,\n competence_date, issue_date, total, description, metadata, created_by)\n VALUES\n (v_order.tenant_id, v_order.unit_id, 'receivable', v_order.party_id, p_order_id, v_number,\n coalesce(v_order.created_at::date, current_date), current_date,\n v_total, nullif(v_summary, ''),\n jsonb_build_object('source', 'order_raise_receivable', 'orderKind', v_order.kind),\n auth.uid())\n RETURNING id INTO v_invoice_id;\n\n PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);\n\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount)\n VALUES (v_order.tenant_id, v_order.unit_id, v_invoice_id, 1,\n coalesce(p_due_date, current_date), v_total);\n\n UPDATE public.orders SET\n stage = 'invoiced',\n direction = 'credit',\n metadata = coalesce(metadata, '{}'::jsonb)\n || jsonb_build_object('invoiceNumber', v_number, 'installmentCount', 1),\n updated_at = now()\n WHERE id = p_order_id;\n\n RETURN v_invoice_id;\nEND $$;\n\n-- ── reparo: só o que ainda pode ser composto ───────────────────────────────\n--\n-- A primeira versão deste reparo fazia duas coisas que o próprio banco recusou,\n-- e recusou com razão:\n--\n-- CARIMBAVA \\`items.invoice_id\\` NA LINHA DO PEDIDO. É exatamente o que o\n-- cabeçalho deste arquivo diz que não se faz — \\`items_one_parent\\` quer UM pai\n-- por linha. Escrevi o desenho certo em cima e o errado embaixo.\n--\n-- COMPUNHA LINHAS EM FATURA JÁ EMITIDA. \\`trg_invoice_line_immutable\\` levantou\n-- 23514 num pool real: uma fatura emitida é imutável (PRD 04). Voltar atrás\n-- para lhe dar linhas é reescrever documento fechado — e um livro que se pode\n-- reescrever não prova nada.\n--\n-- Então o reparo alcança só a fatura AINDA NÃO EMITIDA. A emitida antiga segue\n-- sem linhas próprias e continua legível: \\`v_financial_receipt\\` (017) cai para\n-- as linhas do pedido, e \\`v_financial_invoice_lines\\` lê pela associação. O que\n-- ela não terá é apuração retroativa de comissão — e isso é consequência de\n-- fechar documento, não defeito deste arquivo.\nDO $$\nDECLARE r record;\nBEGIN\n FOR r IN\n SELECT f.id AS invoice_id, f.order_id\n FROM public.plg_financial_invoices f\n WHERE f.order_id IS NOT NULL\n AND f.cancelled_at IS NULL\n AND f.issue_date IS NULL\n AND NOT EXISTS (SELECT 1 FROM public.items i WHERE i.invoice_id = f.id)\n LOOP\n PERFORM public.plg_financial_copy_order_lines_to_invoice(r.order_id, r.invoice_id);\n END LOOP;\nEND $$;\n`\n\nexport const MIGRATION_011_A_NEW_TENANT_CAN_TAKE_MONEY_ON_THE_FIRST_DAY = `-- 011_a_new_tenant_can_take_money_on_the_first_day.sql\n--\n-- Um tenant novo não consegue receber. Medido no cluster, e cada linha é uma\n-- casa que não pode trabalhar:\n--\n-- clinica-teste 0 formas de pagamento, 0 contas — nada\n-- joias-do-vini 7 formas de pagamento, 0 CONTAS — não recebe nada\n-- espaco-renova-rio 3 contas, sem cofre e sem conta de cartão\n-- nenhum tenant plano de contas\n--\n-- É daí que vêm dois erros que parecem bugs isolados e são o mesmo buraco:\n--\n-- \"method credit_card must settle in a card_receivable_credit account\" —\n-- a regra está certa, e não existe conta desse tipo para oferecer.\n--\n-- abrir caixa exige um cofre \\`cash_physical\\` com unidade, e não há cofre.\n--\n-- O QUE É O MÍNIMO, e por que exatamente isto. Não é um plano de contas de\n-- contador — é o conjunto sem o qual as telas que já existem não funcionam:\n-- uma gaveta para o dinheiro vivo, uma conta para o banco, e duas contas de\n-- recebível de cartão, porque crédito e débito liquidam em prazos diferentes e\n-- misturá-los é perder a conciliação antes de começar.\n--\n-- O plano de contas nasce RASO e em português, com os grupos que qualquer\n-- prestador de serviço usa. Um contador vai reorganizá-lo; o ponto é que a\n-- linha da fatura tenha onde ser classificada no primeiro dia, em vez de o\n-- campo abrir vazio e a classificação nunca acontecer.\n--\n-- É O PLUGIN QUE SE PENDURA, e não o \\`create_tenant\\` que sabe de finanças. A\n-- casca do tenant não deve conhecer conta de cartão; quem sabe o que um tenant\n-- financeiro precisa é o Financeiro. Daí o gatilho vir daqui.\n\nCREATE OR REPLACE FUNCTION public.financial_seed_tenant(p_tenant uuid)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_unit uuid;\n v_made int := 0;\n v_chart int := 0;\nBEGIN\n IF p_tenant IS NULL THEN RETURN jsonb_build_object('seeded', false, 'reason', 'no tenant'); END IF;\n\n -- A unidade da sede. O cofre PRECISA de uma: financial_cash_session_open\n -- recusa cofre sem unidade, e um cofre que não abre caixa não serve.\n SELECT u.id INTO v_unit FROM app.units u\n WHERE u.tenant_id = p_tenant AND u.active\n ORDER BY (u.kind = 'hq') DESC, u.created_at\n LIMIT 1;\n\n -- ── as contas ───────────────────────────────────────────────────────────\n -- \\`code\\` é o que torna a semeadura idempotente: rodar de novo não cria a\n -- segunda gaveta.\n INSERT INTO public.plg_financial_accounts (tenant_id, unit_id, kind, code, name, account_type, is_active, settlement_days, metadata)\n SELECT p_tenant, v_unit, s.kind, s.code, s.name, s.account_type, true, s.settlement_days,\n jsonb_build_object('seeded', true)\n FROM (VALUES\n ('cash_physical', 'cofre', 'Caixa (dinheiro)', 'caixa_fisico', 0),\n ('bank', 'banco', 'Conta bancária', 'bank_account', 0),\n -- Crédito e débito separados de propósito: liquidam em prazos\n -- diferentes, e uma conta só faria a conciliação nascer errada.\n ('card_receivable_credit', 'cartao-cred', 'Recebíveis de crédito', 'cartao_credito_recebimento', 30),\n ('card_receivable_debit', 'cartao-deb', 'Recebíveis de débito', 'cartao_debito_recebimento', 1)\n ) AS s(kind, code, name, account_type, settlement_days)\n WHERE NOT EXISTS (\n SELECT 1 FROM public.plg_financial_accounts a\n WHERE a.tenant_id = p_tenant AND a.code = s.code);\n GET DIAGNOSTICS v_made = ROW_COUNT;\n\n -- O cofre adota a unidade quando ainda não tem uma. O gatilho roda no INSERT\n -- do tenant, e \\`create_tenant\\` só cria a unidade DEPOIS — então na primeira\n -- passada não há sede para apontar, e um cofre sem unidade não abre caixa.\n -- Idempotente não pode significar \"erra uma vez e congela\": a segunda\n -- chamada conserta em vez de não fazer nada.\n IF v_unit IS NOT NULL THEN\n UPDATE public.plg_financial_accounts\n SET unit_id = v_unit, updated_at = now()\n WHERE tenant_id = p_tenant AND unit_id IS NULL AND kind = 'cash_physical';\n END IF;\n\n -- ── o plano de contas, raso ─────────────────────────────────────────────\n INSERT INTO public.plg_financial_chart_of_accounts (tenant_id, unit_id, code, name, node_type, account_type, is_active)\n SELECT p_tenant, NULL, s.code, s.name, 'leaf', s.account_type, true\n FROM (VALUES\n ('3.01', 'Receita de serviços', 'revenue'),\n ('3.02', 'Receita de produtos', 'revenue'),\n ('4.01', 'Custo dos produtos', 'expense'),\n ('4.02', 'Comissões', 'expense'),\n ('4.03', 'Folha e encargos', 'expense'),\n ('4.04', 'Aluguel e condomínio', 'expense'),\n ('4.05', 'Água, luz e telefone', 'expense'),\n ('4.06', 'Marketing', 'expense'),\n ('4.07', 'Taxas de cartão', 'expense'),\n ('4.08', 'Impostos', 'expense'),\n ('4.99', 'Outras despesas', 'expense')\n ) AS s(code, name, account_type)\n WHERE NOT EXISTS (\n SELECT 1 FROM public.plg_financial_chart_of_accounts c\n WHERE c.tenant_id = p_tenant AND c.code = s.code);\n GET DIAGNOSTICS v_chart = ROW_COUNT;\n\n -- ── um centro de custo, para o campo não abrir vazio ────────────────────\n INSERT INTO public.plg_financial_cost_centers (tenant_id, unit_id, code, name, is_active)\n SELECT p_tenant, NULL, 'GERAL', 'Geral', true\n WHERE NOT EXISTS (\n SELECT 1 FROM public.plg_financial_cost_centers c\n WHERE c.tenant_id = p_tenant AND c.code = 'GERAL');\n\n RETURN jsonb_build_object('seeded', true, 'tenant_id', p_tenant, 'unit_id', v_unit,\n 'accounts', v_made, 'chart_accounts', v_chart);\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_seed_tenant(uuid) IS\n 'O mínimo para um tenant receber dinheiro no primeiro dia: cofre, banco, recebíveis de crédito e de débito, plano de contas raso e um centro de custo. Idempotente por \\`code\\` (011).';\n\n-- ── o Financeiro se pendura na criação do tenant ───────────────────────────\nCREATE OR REPLACE FUNCTION public.trg_financial_seed_new_tenant()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nBEGIN\n -- Nunca derruba a criação do tenant: uma casa sem conta de cartão é\n -- reparável com uma chamada; uma casa que não nasceu, não.\n BEGIN\n PERFORM public.financial_seed_tenant(NEW.id);\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'financial_seed_tenant falhou para o tenant %: %', NEW.id, SQLERRM;\n END;\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS tenants_financial_seed ON public.tenants;\nCREATE TRIGGER tenants_financial_seed\n AFTER INSERT ON public.tenants\n FOR EACH ROW\n EXECUTE FUNCTION public.trg_financial_seed_new_tenant();\n\n-- ── e as casas que já nasceram sem ─────────────────────────────────────────\nDO $$\nDECLARE r record;\nBEGIN\n FOR r IN SELECT t.id FROM public.tenants t LOOP\n BEGIN\n PERFORM public.financial_seed_tenant(r.id);\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'backfill financial_seed_tenant % : %', r.id, SQLERRM;\n END;\n END LOOP;\nEND $$;\n\nREVOKE ALL ON FUNCTION public.financial_seed_tenant(uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_seed_tenant(uuid) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_seed_tenant(uuid) TO service_role;\n`\n\nexport const MIGRATION_012_THE_COMMISSIONS_BECOME_ONE_BILL_PER_PERSON = `-- 012_the_commissions_become_one_bill_per_person.sql\n--\n-- A apuração (009) produz N lançamentos por pessoa — um por linha atendida. No\n-- fim do mês a Ana tem quarenta, e ninguém paga quarenta vezes. Falta o elo\n-- entre \"apurado\" e \"o dinheiro saiu\": o borderô.\n--\n-- O QUE ELE FAZ. Junta os lançamentos de um lote por PESSOA e levanta uma\n-- conta a pagar para cada uma, com o valor somado, e carimba em cada\n-- lançamento qual conta o cobre. Depois disso a comissão vive no mesmo lugar\n-- que o aluguel e a conta de luz — a tela de contas a pagar, o extrato, o\n-- fluxo de caixa — em vez de num relatório paralelo que ninguém concilia.\n--\n-- POR QUE UMA CONTA POR PESSOA, e não uma por lançamento. Porque é assim que\n-- se paga: um Pix para a Ana, não quarenta. E não uma só para todo mundo,\n-- porque cada profissional recebe em separado, em datas que podem divergir, e\n-- uma conta única não teria a quem ser paga.\n--\n-- A CLASSIFICAÇÃO VEM JUNTO. \\`financial_create_payable\\` aceita conta contábil,\n-- e a semeadura (011) cria \"4.02 Comissões\" em toda casa nova. Uma despesa de\n-- comissão que nasce sem classificação é uma linha a mais para alguém\n-- classificar à mão no fechamento — e o DRE fica errado até lá.\n--\n-- APROVAR ANTES DE PAGAR. O borderô só recolhe o que está \\`approved\\`: apurar\n-- não é autorizar. Quem apura pode ser quem confere; quem manda pagar é outra\n-- decisão, e o lançamento carrega os dois estados de propósito.\n\nCREATE OR REPLACE FUNCTION public.financial_commission_approve(p_generation_id uuid)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_n int;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_commission_approve: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.commissions.manage', NULL) THEN\n RAISE EXCEPTION 'financial_commission_approve: exige financial.commissions.manage' USING ERRCODE = '42501';\n END IF;\n\n UPDATE public.plg_financial_commission_entries\n SET status = 'approved', updated_at = now()\n WHERE tenant_id = v_tenant AND generation_id = p_generation_id AND status = 'pending';\n GET DIAGNOSTICS v_n = ROW_COUNT;\n\n UPDATE public.plg_financial_commission_generations\n SET status = 'approved', updated_at = now()\n WHERE tenant_id = v_tenant AND id = p_generation_id;\n\n RETURN jsonb_build_object('generation_id', p_generation_id, 'approved', v_n);\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_commission_approve(uuid) IS\n 'Autoriza os lançamentos de um lote. Apurar não é autorizar: são duas decisões e dois estados (012).';\n\n-- ── o borderô ───────────────────────────────────────────────────────────────\nCREATE OR REPLACE FUNCTION public.financial_commission_pay_run(\n p_generation_id uuid,\n p_due_date date DEFAULT NULL\n) RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_chart uuid;\n v_row record;\n v_payable uuid;\n v_bills int := 0;\n v_total numeric := 0;\n v_person text;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_commission_pay_run: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.commissions.manage', NULL) THEN\n RAISE EXCEPTION 'financial_commission_pay_run: exige financial.commissions.manage' USING ERRCODE = '42501';\n END IF;\n\n -- \"4.02 Comissões\" da semeadura (011). Ausente numa casa antiga: a conta a\n -- pagar nasce sem classificação em vez de o borderô falhar — pagar a pessoa\n -- importa mais do que o DRE sair pronto.\n SELECT id INTO v_chart FROM public.plg_financial_chart_of_accounts\n WHERE tenant_id = v_tenant AND code = '4.02' LIMIT 1;\n\n FOR v_row IN\n SELECT e.person_id, e.unit_id, sum(e.amount) AS total, count(*) AS n\n FROM public.plg_financial_commission_entries e\n WHERE e.tenant_id = v_tenant\n AND e.generation_id = p_generation_id\n AND e.status = 'approved'\n AND e.payable_invoice_id IS NULL\n GROUP BY e.person_id, e.unit_id\n HAVING sum(e.amount) > 0\n LOOP\n SELECT name INTO v_person FROM public.people WHERE id = v_row.person_id;\n\n -- Uma conta por pessoa, e a chave de idempotência é lote+pessoa: rodar o\n -- borderô duas vezes não levanta a segunda conta.\n -- Devolve jsonb, não uuid: o id da conta vem em \\`invoice_id\\`.\n v_payable := (public.financial_create_payable(\n 'Comissões · ' || coalesce(v_person, 'profissional') || ' · ' || v_row.n || ' lançamento(s)',\n round(v_row.total, 2),\n coalesce(p_due_date, current_date),\n v_row.person_id,\n v_row.unit_id,\n v_chart,\n NULL,\n NULL,\n NULL,\n 'commission-run:' || p_generation_id::text || ':' || v_row.person_id::text\n ) ->> 'invoice_id')::uuid;\n\n UPDATE public.plg_financial_commission_entries\n SET payable_invoice_id = v_payable, updated_at = now()\n WHERE tenant_id = v_tenant AND generation_id = p_generation_id\n AND person_id = v_row.person_id AND status = 'approved'\n AND payable_invoice_id IS NULL;\n\n v_bills := v_bills + 1;\n v_total := v_total + v_row.total;\n END LOOP;\n\n RETURN jsonb_build_object('generation_id', p_generation_id, 'payables', v_bills,\n 'total', round(v_total, 2), 'due_date', coalesce(p_due_date, current_date));\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_commission_pay_run(uuid, date) IS\n 'Junta os lançamentos aprovados de um lote por pessoa e levanta UMA conta a pagar para cada. É o elo entre \"apurado\" e \"o dinheiro saiu\" (012).';\n\nREVOKE ALL ON FUNCTION public.financial_commission_approve(uuid) FROM PUBLIC;\nREVOKE ALL ON FUNCTION public.financial_commission_pay_run(uuid, date) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_commission_approve(uuid) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_commission_pay_run(uuid, date) TO authenticated;\n\n-- ── o que a folha do mês mostra ─────────────────────────────────────────────\nCREATE OR REPLACE VIEW public.v_financial_commission_payroll\nWITH (security_invoker = 'true') AS\nSELECT e.tenant_id,\n e.unit_id,\n e.generation_id,\n g.period_start,\n g.period_end,\n e.person_id,\n p.name AS person_name,\n count(*) AS entry_count,\n sum(e.amount)::numeric(14,2) AS total_amount,\n count(*) FILTER (WHERE e.status = 'pending')::int AS pending_count,\n count(*) FILTER (WHERE e.status = 'approved')::int AS approved_count,\n count(*) FILTER (WHERE e.status = 'paid')::int AS paid_count,\n -- Uma conta a pagar por pessoa, então \\`min\\` é ela mesma.\n min(e.payable_invoice_id::text)::uuid AS payable_invoice_id\n FROM public.plg_financial_commission_entries e\n LEFT JOIN public.plg_financial_commission_generations g ON g.id = e.generation_id\n LEFT JOIN public.people p ON p.id = e.person_id\n WHERE e.status <> 'cancelled'\n GROUP BY e.tenant_id, e.unit_id, e.generation_id, g.period_start, g.period_end, e.person_id, p.name;\n\nCOMMENT ON VIEW public.v_financial_commission_payroll IS\n 'A folha de comissões do período: uma linha por pessoa, com quantos lançamentos, quanto, em que estado, e a conta a pagar que a cobre (012).';\n\nGRANT SELECT ON public.v_financial_commission_payroll TO authenticated;\nGRANT SELECT ON public.v_financial_commission_payroll TO service_role;\n`\n\nexport const MIGRATION_013_A_DISCOUNT_HAS_A_REASON_AND_SOMEONE_WHO_ALLOWED_IT = `-- 013_a_discount_has_a_reason_and_someone_who_allowed_it.sql\n--\n-- Hoje qualquer pessoa com acesso à venda desconta qualquer valor, sem motivo\n-- e sem rastro. \\`items.discount\\` e \\`orders.discount\\` são números que aparecem\n-- e ninguém sabe quem os pôs lá nem por quê.\n--\n-- É a fraude mais barata que um sistema de varejo permite, e o dano nem precisa\n-- de má-fé: no fim do mês a margem caiu e não há a quem perguntar. O V1 tem\n-- três tabelas para isso (\\`discount_reasons\\`, \\`discount_users\\`,\n-- \\`order_discounts\\`) e nós tínhamos zero.\n--\n-- TRÊS PEÇAS, e cada uma responde uma pergunta:\n--\n-- POR QUÊ o motivo vem de um catálogo, não de um campo livre. \"Cliente\n-- antigo\" e \"cliente antigo \" são o mesmo desconto e dois\n-- relatórios; e um motivo cadastrado é o que permite perguntar\n-- \"quanto custou a política de fidelidade este mês\".\n--\n-- QUEM PODE o teto é por PESSOA e por motivo. A recepção desconta 5%; a\n-- gerente, 20%; a dona, o que quiser. Sem teto, \"quem pode\n-- descontar\" é a mesma pergunta que \"quem tem acesso à tela\".\n--\n-- QUEM DEU o registro, com quem pediu, quem autorizou e quando. Acima do\n-- teto exige uma segunda pessoa — segregação de função, que é o\n-- que separa um controle de uma boa intenção.\n--\n-- O QUE ESTE ARQUIVO NÃO FAZ: bloquear a venda. A trava vive no fluxo que\n-- concede (a tela e a RPC de desconto), não num gatilho sobre \\`items\\` — um\n-- gatilho recusando \\`UPDATE items SET discount\\` derrubaria importação,\n-- migração e correção de dados, que são caminhos legítimos e sem tela.\n\n-- ── por quê ─────────────────────────────────────────────────────────────────\nCREATE TABLE IF NOT EXISTS public.plg_financial_discount_reasons (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n code text NOT NULL,\n name text NOT NULL,\n -- O teto do MOTIVO, independente de quem concede: \"amostra grátis\" pode\n -- ser 100% e \"cortesia\" 10%, mesmo para a dona.\n max_percent numeric(6,3),\n requires_approval boolean DEFAULT false NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n owner_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_discount_reasons'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_discount_reasons ADD CONSTRAINT plg_financial_discount_reasons_pkey PRIMARY KEY (id);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_discount_reasons_tenant_code_key') THEN\n ALTER TABLE ONLY public.plg_financial_discount_reasons ADD CONSTRAINT plg_financial_discount_reasons_tenant_code_key UNIQUE (tenant_id, code);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_discount_reasons_pct_check') THEN\n ALTER TABLE ONLY public.plg_financial_discount_reasons ADD CONSTRAINT plg_financial_discount_reasons_pct_check\n CHECK (max_percent IS NULL OR (max_percent >= 0 AND max_percent <= 100));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_discount_reasons_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_discount_reasons ADD CONSTRAINT plg_financial_discount_reasons_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_discount_reasons_tenant_idx\n ON public.plg_financial_discount_reasons USING btree (tenant_id) WHERE is_active;\n\nDROP TRIGGER IF EXISTS plg_financial_discount_reasons_updated_at ON public.plg_financial_discount_reasons;\nCREATE TRIGGER plg_financial_discount_reasons_updated_at\n BEFORE UPDATE ON public.plg_financial_discount_reasons\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nALTER TABLE public.plg_financial_discount_reasons ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_discount_reasons FORCE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_financial_discount_reasons_authz_select ON public.plg_financial_discount_reasons;\nCREATE POLICY plg_financial_discount_reasons_authz_select ON public.plg_financial_discount_reasons\n -- Um catálogo de motivos é o que a tela de venda oferece numa lista; ler\n -- exige estar na casa e ter leitura do Financeiro. CONCEDER é outra coisa e\n -- mora no verbo, com teto.\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_discount_reasons_authz_insert ON public.plg_financial_discount_reasons;\nCREATE POLICY plg_financial_discount_reasons_authz_insert ON public.plg_financial_discount_reasons\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.create', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_discount_reasons_authz_update ON public.plg_financial_discount_reasons;\nCREATE POLICY plg_financial_discount_reasons_authz_update ON public.plg_financial_discount_reasons\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', unit_id)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_discount_reasons_authz_delete ON public.plg_financial_discount_reasons;\nCREATE POLICY plg_financial_discount_reasons_authz_delete ON public.plg_financial_discount_reasons\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.delete', unit_id)));\n\nGRANT ALL ON TABLE public.plg_financial_discount_reasons TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_discount_reasons TO service_role;\n\n-- ── quem pode, e até quanto ─────────────────────────────────────────────────\nCREATE TABLE IF NOT EXISTS public.plg_financial_discount_authorities (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n user_id uuid NOT NULL,\n -- Nulo = vale para qualquer motivo. Um teto por motivo é a exceção, não a\n -- regra, e obrigar uma linha por motivo faria a configuração inviável.\n reason_id uuid,\n max_percent numeric(6,3) DEFAULT 0 NOT NULL,\n max_amount numeric(14,2),\n is_active boolean DEFAULT true NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_discount_authorities'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_discount_authorities ADD CONSTRAINT plg_financial_discount_authorities_pkey PRIMARY KEY (id);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_discount_authorities_pct_check') THEN\n ALTER TABLE ONLY public.plg_financial_discount_authorities ADD CONSTRAINT plg_financial_discount_authorities_pct_check\n CHECK (max_percent >= 0 AND max_percent <= 100);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_discount_authorities_reason_fk') THEN\n ALTER TABLE ONLY public.plg_financial_discount_authorities ADD CONSTRAINT plg_financial_discount_authorities_reason_fk\n FOREIGN KEY (reason_id) REFERENCES public.plg_financial_discount_reasons(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_discount_authorities_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_discount_authorities ADD CONSTRAINT plg_financial_discount_authorities_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_discount_authorities_user_idx\n ON public.plg_financial_discount_authorities USING btree (tenant_id, user_id) WHERE is_active;\n\nDROP TRIGGER IF EXISTS plg_financial_discount_authorities_updated_at ON public.plg_financial_discount_authorities;\nCREATE TRIGGER plg_financial_discount_authorities_updated_at\n BEFORE UPDATE ON public.plg_financial_discount_authorities\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nALTER TABLE public.plg_financial_discount_authorities ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_discount_authorities FORCE ROW LEVEL SECURITY;\n\n-- Ver o próprio teto é legítimo — a tela precisa saber até quanto oferecer.\n-- MUDAR o teto é de quem administra: um vendedor que edita o próprio limite\n-- não tem limite.\nDROP POLICY IF EXISTS plg_financial_discount_authorities_authz_select ON public.plg_financial_discount_authorities;\nCREATE POLICY plg_financial_discount_authorities_authz_select ON public.plg_financial_discount_authorities\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (user_id = (SELECT auth.uid())\n OR (SELECT app.has_permission('financial.manage', unit_id))));\n\nDROP POLICY IF EXISTS plg_financial_discount_authorities_authz_insert ON public.plg_financial_discount_authorities;\nCREATE POLICY plg_financial_discount_authorities_authz_insert ON public.plg_financial_discount_authorities\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.manage', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_discount_authorities_authz_update ON public.plg_financial_discount_authorities;\nCREATE POLICY plg_financial_discount_authorities_authz_update ON public.plg_financial_discount_authorities\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.manage', unit_id)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.manage', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_discount_authorities_authz_delete ON public.plg_financial_discount_authorities;\nCREATE POLICY plg_financial_discount_authorities_authz_delete ON public.plg_financial_discount_authorities\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.manage', unit_id)));\n\nGRANT ALL ON TABLE public.plg_financial_discount_authorities TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_discount_authorities TO service_role;\n\n-- ── quem deu, e com o aval de quem ──────────────────────────────────────────\nCREATE TABLE IF NOT EXISTS public.plg_financial_discount_grants (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n order_id uuid,\n item_id uuid,\n reason_id uuid,\n percent numeric(6,3) DEFAULT 0 NOT NULL,\n amount numeric(14,2) DEFAULT 0 NOT NULL,\n base_amount numeric(14,2) DEFAULT 0 NOT NULL,\n notes text,\n requested_by_user_id uuid,\n -- Nulo enquanto o desconto está dentro do teto de quem pediu: não houve\n -- segunda pessoa porque não precisou.\n approved_by_user_id uuid,\n approved_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_discount_grants'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_discount_grants ADD CONSTRAINT plg_financial_discount_grants_pkey PRIMARY KEY (id);\n END IF;\n -- Um desconto é sobre uma venda ou sobre uma linha, não sobre nada.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_discount_grants_target_check') THEN\n ALTER TABLE ONLY public.plg_financial_discount_grants ADD CONSTRAINT plg_financial_discount_grants_target_check\n CHECK (order_id IS NOT NULL OR item_id IS NOT NULL);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_discount_grants_order_fk') THEN\n ALTER TABLE ONLY public.plg_financial_discount_grants ADD CONSTRAINT plg_financial_discount_grants_order_fk\n FOREIGN KEY (order_id) REFERENCES public.orders(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_discount_grants_item_fk') THEN\n ALTER TABLE ONLY public.plg_financial_discount_grants ADD CONSTRAINT plg_financial_discount_grants_item_fk\n FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_discount_grants_reason_fk') THEN\n ALTER TABLE ONLY public.plg_financial_discount_grants ADD CONSTRAINT plg_financial_discount_grants_reason_fk\n FOREIGN KEY (reason_id) REFERENCES public.plg_financial_discount_reasons(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_discount_grants_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_discount_grants ADD CONSTRAINT plg_financial_discount_grants_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_discount_grants_order_idx\n ON public.plg_financial_discount_grants USING btree (order_id);\nCREATE INDEX IF NOT EXISTS plg_financial_discount_grants_tenant_created_idx\n ON public.plg_financial_discount_grants USING btree (tenant_id, created_at DESC);\n\nDROP TRIGGER IF EXISTS plg_financial_discount_grants_updated_at ON public.plg_financial_discount_grants;\nCREATE TRIGGER plg_financial_discount_grants_updated_at\n BEFORE UPDATE ON public.plg_financial_discount_grants\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nALTER TABLE public.plg_financial_discount_grants ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_discount_grants FORCE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_financial_discount_grants_authz_select ON public.plg_financial_discount_grants;\nCREATE POLICY plg_financial_discount_grants_authz_select ON public.plg_financial_discount_grants\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', unit_id)));\n\n-- Sem INSERT/UPDATE/DELETE: o registro nasce pelo verbo, que é onde o teto é\n-- conferido. Uma policy de INSERT aqui seria a porta pela qual se registra um\n-- desconto que ninguém autorizou.\n\nGRANT SELECT ON TABLE public.plg_financial_discount_grants TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_discount_grants TO service_role;\n\nINSERT INTO app.scaffold_registry\n (table_schema, table_name, resource_type, plugin, owner_scoping, mode, previous_policies, audit_hook, scope, distributable)\nVALUES\n ('public', 'plg_financial_discount_reasons', 'financial.discount_reason', 'financial', false, 'enforce', ARRAY[]::text[], false, 'unit_optional', false),\n ('public', 'plg_financial_discount_authorities', 'financial.discount_authority', 'financial', false, 'enforce', ARRAY[]::text[], true, 'unit_optional', false),\n ('public', 'plg_financial_discount_grants', 'financial.discount_grant', 'financial', false, 'enforce', ARRAY[]::text[], true, 'unit_optional', false)\nON CONFLICT DO NOTHING;\n\nINSERT INTO app.permissions (key, plugin, description)\nVALUES ('financial.discount.approve', 'financial',\n 'Autorizar desconto acima do teto de quem pediu. Segregação de função: quem pede não aprova o próprio pedido.')\nON CONFLICT (key) DO NOTHING;\n\n-- ── o verbo ─────────────────────────────────────────────────────────────────\nCREATE OR REPLACE FUNCTION public.financial_grant_discount(\n p_order_id uuid,\n p_percent numeric,\n p_reason_id uuid DEFAULT NULL,\n p_item_id uuid DEFAULT NULL,\n p_notes text DEFAULT NULL,\n p_approver_user_id uuid DEFAULT NULL\n) RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_actor uuid := auth.uid();\n v_unit uuid;\n v_base numeric;\n v_ceiling numeric;\n v_reason record;\n v_amount numeric;\n v_grant uuid;\n v_approved uuid;\nBEGIN\n IF v_tenant IS NULL OR v_actor IS NULL THEN\n RAISE EXCEPTION 'financial_grant_discount: sem sessão' USING ERRCODE = '42501';\n END IF;\n IF p_percent IS NULL OR p_percent <= 0 OR p_percent > 100 THEN\n RAISE EXCEPTION 'financial_grant_discount: percentual tem de estar entre 0 e 100' USING ERRCODE = '22023';\n END IF;\n\n SELECT o.unit_id, o.total INTO v_unit, v_base FROM public.orders o\n WHERE o.id = p_order_id AND o.tenant_id = v_tenant;\n IF v_base IS NULL THEN\n RAISE EXCEPTION 'financial_grant_discount: pedido % não encontrado', p_order_id USING ERRCODE = 'P0002';\n END IF;\n\n IF p_item_id IS NOT NULL THEN\n SELECT i.total INTO v_base FROM public.items i WHERE i.id = p_item_id AND i.tenant_id = v_tenant;\n END IF;\n\n -- O teto do motivo vale para todo mundo, inclusive para a dona.\n IF p_reason_id IS NOT NULL THEN\n SELECT * INTO v_reason FROM public.plg_financial_discount_reasons\n WHERE id = p_reason_id AND tenant_id = v_tenant AND is_active;\n IF v_reason.id IS NULL THEN\n RAISE EXCEPTION 'financial_grant_discount: motivo inválido' USING ERRCODE = '22023';\n END IF;\n IF v_reason.max_percent IS NOT NULL AND p_percent > v_reason.max_percent THEN\n RAISE EXCEPTION 'financial_grant_discount: % excede o teto do motivo (%%%)', p_percent, v_reason.max_percent\n USING ERRCODE = '23514';\n END IF;\n END IF;\n\n -- O teto de QUEM PEDE. Sem linha de autoridade o teto é zero: não ter sido\n -- configurado não é permissão para descontar o que quiser.\n SELECT max(a.max_percent) INTO v_ceiling\n FROM public.plg_financial_discount_authorities a\n WHERE a.tenant_id = v_tenant AND a.user_id = v_actor AND a.is_active\n AND (a.reason_id IS NULL OR a.reason_id = p_reason_id)\n AND (a.unit_id IS NULL OR a.unit_id = v_unit);\n v_ceiling := coalesce(v_ceiling, 0);\n\n IF p_percent > v_ceiling THEN\n -- Acima do teto exige uma SEGUNDA pessoa, e ela não pode ser a primeira:\n -- aprovar o próprio pedido é o mesmo que não ter teto.\n IF p_approver_user_id IS NULL THEN\n RAISE EXCEPTION 'financial_grant_discount: % excede seu teto de %%% — precisa de aprovação', p_percent, v_ceiling\n USING ERRCODE = '42501',\n HINT = 'Peça a quem tem financial.discount.approve.';\n END IF;\n IF p_approver_user_id = v_actor THEN\n RAISE EXCEPTION 'financial_grant_discount: quem pede não aprova o próprio desconto' USING ERRCODE = '42501';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM app.memberships m\n WHERE m.tenant_id = v_tenant AND m.user_id = p_approver_user_id AND m.active) THEN\n RAISE EXCEPTION 'financial_grant_discount: aprovador não é da casa' USING ERRCODE = '42501';\n END IF;\n v_approved := p_approver_user_id;\n END IF;\n\n v_amount := round(coalesce(v_base, 0) * (p_percent / 100), 2);\n\n INSERT INTO public.plg_financial_discount_grants\n (tenant_id, unit_id, order_id, item_id, reason_id, percent, amount, base_amount,\n notes, requested_by_user_id, approved_by_user_id, approved_at)\n VALUES\n (v_tenant, v_unit, p_order_id, p_item_id, p_reason_id, p_percent, v_amount, coalesce(v_base, 0),\n p_notes, v_actor, v_approved, CASE WHEN v_approved IS NOT NULL THEN now() END)\n RETURNING id INTO v_grant;\n\n RETURN jsonb_build_object('grant_id', v_grant, 'percent', p_percent, 'amount', v_amount,\n 'base_amount', v_base, 'required_approval', v_approved IS NOT NULL);\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_grant_discount(uuid, numeric, uuid, uuid, text, uuid) IS\n 'Registra um desconto com motivo, teto de quem pede e, acima do teto, o aval de uma SEGUNDA pessoa (013).';\n\nREVOKE ALL ON FUNCTION public.financial_grant_discount(uuid, numeric, uuid, uuid, text, uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_grant_discount(uuid, numeric, uuid, uuid, text, uuid) TO authenticated;\n\n-- ── o que o fechamento do mês pergunta ──────────────────────────────────────\nCREATE OR REPLACE VIEW public.v_financial_discounts_given\nWITH (security_invoker = 'true') AS\nSELECT g.id, g.tenant_id, g.unit_id, g.order_id, g.item_id,\n o.reference_number AS order_number,\n r.code AS reason_code, r.name AS reason_name,\n g.percent, g.amount, g.base_amount,\n g.requested_by_user_id, g.approved_by_user_id, g.approved_at,\n (g.approved_by_user_id IS NOT NULL) AS needed_approval,\n g.notes, g.created_at\n FROM public.plg_financial_discount_grants g\n LEFT JOIN public.orders o ON o.id = g.order_id\n LEFT JOIN public.plg_financial_discount_reasons r ON r.id = g.reason_id;\n\nCOMMENT ON VIEW public.v_financial_discounts_given IS\n 'Quanto a casa deu de desconto, por motivo e por quem — a pergunta \"por que a margem caiu\" com resposta (013).';\n\nGRANT SELECT ON public.v_financial_discounts_given TO authenticated;\nGRANT SELECT ON public.v_financial_discounts_given TO service_role;\n`\n\nexport const MIGRATION_014_THE_BILL_THAT_COMES_BACK_EVERY_MONTH = `-- 014_the_bill_that_comes_back_every_month.sql\n--\n-- Aluguel, contabilidade, internet, assinatura de software: contas que chegam\n-- todo mês, com o mesmo valor e o mesmo dono. Hoje alguém digita cada uma, doze\n-- vezes por ano, e a que for esquecida só aparece quando o fornecedor liga.\n--\n-- O MODELO É UM MOLDE, e não doze contas criadas de uma vez.\n--\n-- Doze contas adiantadas poluem o fluxo de caixa com previsão que ninguém\n-- revisou, e mudar o valor do aluguel em março obrigaria a corrigir nove.\n-- O molde guarda a REGRA; a conta nasce quando o mês chega, com o valor que\n-- o molde tem naquele momento.\n--\n-- QUEM DISPARA. Uma função que gera \"o que vence até tal data\", chamada por\n-- quem quiser — um cron, a tela ao abrir, uma rotina noturna. Não um gatilho\n-- de tempo, que o banco não tem, nem um agendador escondido: gerar é um ato\n-- com data, e quem o chamou fica no lote.\n--\n-- IDEMPOTENTE POR COMPETÊNCIA. A chave é molde + mês. Rodar duas vezes no\n-- mesmo dia, ou o cron rodar de novo depois de uma falha, não cria a segunda\n-- conta de aluguel de março — que é o erro que faria alguém pagar duas vezes.\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_recurring_bills (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n description text NOT NULL,\n -- \\`payable\\` (aluguel) ou \\`receivable\\` (mensalidade, assinatura). O mesmo\n -- molde serve os dois: o que muda é a direção do título que ele levanta.\n direction text DEFAULT 'payable'::text NOT NULL,\n counterparty_person_id uuid,\n amount numeric(14,2) NOT NULL,\n chart_account_id uuid,\n cost_center_id uuid,\n -- Mensal é o caso de 95% das contas recorrentes de um SMB. Semanal e anual\n -- entram pelo mesmo campo quando alguém precisar.\n frequency text DEFAULT 'monthly'::text NOT NULL,\n -- O dia do vencimento. 31 num mês de 30 cai no último dia, e não no dia 1\n -- do mês seguinte — quem escreveu 31 quis dizer \"no fim do mês\".\n due_day integer DEFAULT 1 NOT NULL,\n starts_on date DEFAULT current_date NOT NULL,\n -- Nulo = sem fim previsto, que é o normal de um aluguel.\n ends_on date,\n is_active boolean DEFAULT true NOT NULL,\n notes text,\n owner_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_recurring_bills'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_recurring_bills ADD CONSTRAINT plg_financial_recurring_bills_pkey PRIMARY KEY (id);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_recurring_bills_direction_check') THEN\n ALTER TABLE ONLY public.plg_financial_recurring_bills ADD CONSTRAINT plg_financial_recurring_bills_direction_check\n CHECK (direction = ANY (ARRAY['payable', 'receivable']));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_recurring_bills_frequency_check') THEN\n ALTER TABLE ONLY public.plg_financial_recurring_bills ADD CONSTRAINT plg_financial_recurring_bills_frequency_check\n CHECK (frequency = ANY (ARRAY['weekly', 'monthly', 'quarterly', 'yearly']));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_recurring_bills_due_day_check') THEN\n ALTER TABLE ONLY public.plg_financial_recurring_bills ADD CONSTRAINT plg_financial_recurring_bills_due_day_check\n CHECK (due_day >= 1 AND due_day <= 31);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_recurring_bills_amount_check') THEN\n ALTER TABLE ONLY public.plg_financial_recurring_bills ADD CONSTRAINT plg_financial_recurring_bills_amount_check\n CHECK (amount > 0);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_recurring_bills_person_fk') THEN\n ALTER TABLE ONLY public.plg_financial_recurring_bills ADD CONSTRAINT plg_financial_recurring_bills_person_fk\n FOREIGN KEY (counterparty_person_id) REFERENCES public.people(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_recurring_bills_tenant_id_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_recurring_bills ADD CONSTRAINT plg_financial_recurring_bills_tenant_id_fkey\n FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_recurring_bills_tenant_idx\n ON public.plg_financial_recurring_bills USING btree (tenant_id) WHERE is_active;\n\nDROP TRIGGER IF EXISTS plg_financial_recurring_bills_updated_at ON public.plg_financial_recurring_bills;\nCREATE TRIGGER plg_financial_recurring_bills_updated_at\n BEFORE UPDATE ON public.plg_financial_recurring_bills\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nALTER TABLE public.plg_financial_recurring_bills ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_recurring_bills FORCE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_financial_recurring_bills_authz_select ON public.plg_financial_recurring_bills;\nCREATE POLICY plg_financial_recurring_bills_authz_select ON public.plg_financial_recurring_bills\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_recurring_bills_authz_insert ON public.plg_financial_recurring_bills;\nCREATE POLICY plg_financial_recurring_bills_authz_insert ON public.plg_financial_recurring_bills\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.create', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_recurring_bills_authz_update ON public.plg_financial_recurring_bills;\nCREATE POLICY plg_financial_recurring_bills_authz_update ON public.plg_financial_recurring_bills\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', unit_id)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_recurring_bills_authz_delete ON public.plg_financial_recurring_bills;\nCREATE POLICY plg_financial_recurring_bills_authz_delete ON public.plg_financial_recurring_bills\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.delete', unit_id)));\n\nGRANT ALL ON TABLE public.plg_financial_recurring_bills TO authenticated;\nGRANT ALL ON TABLE public.plg_financial_recurring_bills TO service_role;\n\nINSERT INTO app.scaffold_registry\n (table_schema, table_name, resource_type, plugin, owner_scoping, mode, previous_policies, audit_hook, scope, distributable)\nVALUES\n ('public', 'plg_financial_recurring_bills', 'financial.recurring_bill', 'financial', false, 'enforce', ARRAY[]::text[], false, 'unit_optional', false)\nON CONFLICT DO NOTHING;\n\nCOMMENT ON TABLE public.plg_financial_recurring_bills IS\n 'O molde de uma conta que volta: guarda a REGRA, e o título nasce quando o mês chega — não doze de uma vez (014).';\n\n-- ── gerar o que vence até uma data ──────────────────────────────────────────\nCREATE OR REPLACE FUNCTION public.financial_recurring_run(p_through date DEFAULT NULL)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_through date := coalesce(p_through, (date_trunc('month', current_date) + interval '1 month - 1 day')::date);\n v_bill record;\n v_month date;\n v_due date;\n v_key text;\n v_made int := 0;\n v_total numeric := 0;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_recurring_run: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.create', NULL) THEN\n RAISE EXCEPTION 'financial_recurring_run: exige financial.create' USING ERRCODE = '42501';\n END IF;\n\n FOR v_bill IN\n SELECT * FROM public.plg_financial_recurring_bills\n WHERE tenant_id = v_tenant AND is_active\n AND starts_on <= v_through\n AND (ends_on IS NULL OR ends_on >= starts_on)\n LOOP\n v_month := date_trunc('month', greatest(v_bill.starts_on, date_trunc('month', v_through)::date))::date;\n\n WHILE v_month <= v_through LOOP\n -- O dia 31 num mês de 30 cai no ÚLTIMO dia, não no dia 1 do mês\n -- seguinte: quem escreveu 31 quis dizer \"no fim do mês\".\n v_due := least(\n (v_month + ((v_bill.due_day - 1) || ' days')::interval)::date,\n (v_month + interval '1 month - 1 day')::date);\n\n EXIT WHEN v_bill.ends_on IS NOT NULL AND v_due > v_bill.ends_on;\n\n -- molde + competência: rodar de novo não cria o segundo aluguel de março.\n v_key := 'recurring:' || v_bill.id::text || ':' || to_char(v_month, 'YYYY-MM');\n\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_financial_invoices f\n WHERE f.tenant_id = v_tenant\n AND f.metadata ->> 'recurring_key' = v_key\n AND f.cancelled_at IS NULL\n ) THEN\n PERFORM public.financial_create_invoice(\n v_bill.direction,\n v_bill.amount,\n v_bill.counterparty_person_id,\n NULL,\n v_bill.unit_id,\n jsonb_build_array(jsonb_build_object('number', 1, 'due_date', v_due, 'amount', v_bill.amount)),\n v_month,\n v_bill.description,\n v_bill.notes,\n jsonb_build_object('source', 'financial_recurring_run',\n 'recurring_bill_id', v_bill.id,\n 'recurring_key', v_key),\n v_key\n );\n v_made := v_made + 1;\n v_total := v_total + v_bill.amount;\n END IF;\n\n v_month := (v_month + CASE v_bill.frequency\n WHEN 'weekly' THEN interval '7 days'\n WHEN 'quarterly' THEN interval '3 months'\n WHEN 'yearly' THEN interval '1 year'\n ELSE interval '1 month'\n END)::date;\n END LOOP;\n END LOOP;\n\n RETURN jsonb_build_object('through', v_through, 'created', v_made, 'total', round(v_total, 2));\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_recurring_run(date) IS\n 'Levanta os títulos das contas recorrentes que vencem até a data. Idempotente por molde + competência: rodar duas vezes não cria a segunda conta do mesmo mês (014).';\n\nREVOKE ALL ON FUNCTION public.financial_recurring_run(date) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_recurring_run(date) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_recurring_run(date) TO service_role;\n`\n\nexport const MIGRATION_015_THE_SALE_KNOWS_WHAT_IT_COST = `-- 015_the_sale_knows_what_it_cost.sql\n--\n-- O Financeiro sabe o que ENTROU. Não sabe o que aquilo CUSTOU — e faturamento\n-- sem custo é o número que faz uma casa crescer quebrando.\n--\n-- As duas metades já existem e nunca se falaram:\n--\n-- \\`plg_inventory_stock_movements\\` guarda \\`unit_cost\\`, \\`total_cost\\`, e\n-- \\`source_item_type\\`/\\`source_item_id\\` apontando para a linha do pedido que\n-- consumiu o estoque. O elo estava construído.\n--\n-- \\`plg_financial_commission_entries\\` (009) guarda quanto daquela linha foi\n-- para quem a executou.\n--\n-- Faltava somar. Esta migration não cria tabela nenhuma: cria a VISTA que\n-- responde \"esta venda deu lucro?\", que é a pergunta que nenhuma tela do\n-- módulo respondia.\n--\n-- MARGEM = RECEITA − CMV − COMISSÃO, por linha.\n--\n-- Receita é a linha faturada, líquida de desconto — o que a casa realmente\n-- cobrou, não o de tabela.\n-- CMV é o custo do estoque que aquela linha consumiu, do razão do inventário.\n-- Comissão é o que a linha pagou a quem a executou.\n--\n-- POR QUE UMA VIEW, E NÃO UMA TABELA DE CUSTO. Um custo copiado para o\n-- Financeiro é um segundo lugar onde o custo mora, e dois lugares divergem: uma\n-- correção de inventário não voltaria atrás. A vista lê o razão, sempre atual.\n--\n-- SERVIÇO NÃO TEM CMV, e isso é resposta, não lacuna. Um corte de cabelo não\n-- consome estoque, então o custo é zero e a margem é receita menos comissão —\n-- que é exatamente a conta de um salão.\n--\n-- O QUE ELA NÃO FAZ: custo de mão de obra fixa, rateio de aluguel, energia.\n-- Isso é DRE, e o DRE lê o plano de contas — outra pergunta, outro caminho.\n\n-- O INVENTÁRIO PODE NÃO ESTAR INSTALADO, e o Financeiro não pode depender dele\n-- para existir. Um salão que só vende serviço nunca terá razão de estoque — e a\n-- corrente aplica o financeiro ANTES do inventário, então a tabela não existe\n-- nem no momento em que esta migration roda.\n--\n-- A primeira tentativa foi criar a vista em duas formas conforme a tabela\n-- existisse. Ela aplica e mente: no db-from-zero a vista nasce com CMV zero\n-- FIXO, e o inventário chegando depois não a corrige — o custo ficaria zero\n-- para sempre, que é a pior resposta possível numa conta de margem.\n--\n-- O custo passa a ser resolvido em TEMPO DE EXECUÇÃO. SQL dinâmico dentro da\n-- função resolve o nome quando alguém pergunta, não quando o arquivo roda:\n-- sem inventário devolve zero, e no dia em que ele entra a mesma função passa a\n-- ler o razão, sem republicar nada.\nCREATE OR REPLACE FUNCTION public.plg_financial_item_cogs(p_item_id uuid)\n RETURNS numeric\n LANGUAGE plpgsql\n STABLE\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE v_cogs numeric;\nBEGIN\n IF to_regclass('public.plg_inventory_stock_movements') IS NULL THEN\n RETURN 0;\n END IF;\n -- \\`out\\` e não estornado: devolver ao estoque não é custo da venda.\n EXECUTE $q$\n SELECT coalesce(sum(coalesce(m.total_cost, m.unit_cost * m.quantity, 0)), 0)\n FROM public.plg_inventory_stock_movements m\n WHERE m.source_item_type = 'order_item'\n AND m.source_item_id = $1\n AND m.kind = 'out'\n AND m.reverses_movement_id IS NULL\n $q$ INTO v_cogs USING p_item_id;\n RETURN coalesce(v_cogs, 0);\nEND $$;\n\nCOMMENT ON FUNCTION public.plg_financial_item_cogs(uuid) IS\n 'O custo do estoque que esta linha consumiu, do razão do inventário. Resolve o nome em tempo de execução: sem inventário devolve zero, e passa a ler sozinha no dia em que ele entra (015).';\n\nCREATE OR REPLACE VIEW public.v_financial_line_margin\nWITH (security_invoker = 'true') AS\nWITH comm AS (\n SELECT e.item_id, sum(e.amount) AS commission\n FROM public.plg_financial_commission_entries e\n WHERE e.status <> 'cancelled'\n GROUP BY e.item_id\n)\nSELECT i.id AS item_id,\n i.tenant_id,\n i.unit_id,\n i.order_id,\n f.id AS invoice_id,\n f.number AS invoice_number,\n f.competence_date,\n i.product_id,\n i.name AS item_name,\n i.quantity,\n -- O que a casa cobrou de verdade, líquido de desconto.\n (coalesce(i.total, 0) - coalesce(i.discount, 0))::numeric(14,2) AS revenue,\n public.plg_financial_item_cogs(i.id)::numeric(14,2) AS cogs,\n coalesce(k.commission, 0)::numeric(14,2) AS commission,\n (coalesce(i.total, 0) - coalesce(i.discount, 0)\n - public.plg_financial_item_cogs(i.id) - coalesce(k.commission, 0))::numeric(14,2) AS margin,\n -- Percentual só quando há receita: dividir por zero numa cortesia\n -- devolveria NULL e a tela mostraria um traço onde deveria dizer 0.\n CASE WHEN (coalesce(i.total, 0) - coalesce(i.discount, 0)) > 0\n THEN round(((coalesce(i.total, 0) - coalesce(i.discount, 0)\n - public.plg_financial_item_cogs(i.id) - coalesce(k.commission, 0))\n / (coalesce(i.total, 0) - coalesce(i.discount, 0))) * 100, 2)\n ELSE 0\n END AS margin_percent\n FROM public.items i\n -- A linha do PEDIDO é a que consome estoque e a que tem participante; a\n -- fatura chega por ela (items_one_parent: uma linha tem um pai só).\n LEFT JOIN public.plg_financial_invoices f\n ON f.order_id = i.order_id AND f.cancelled_at IS NULL\n LEFT JOIN comm k ON k.item_id = i.id\n WHERE i.order_id IS NOT NULL;\n\nCOMMENT ON VIEW public.v_financial_line_margin IS\n 'Receita − CMV − comissão, por linha vendida. O CMV vem do razão do inventário pelo elo source_item_id, que já existia e ninguém somava. Serviço tem CMV zero, e isso é resposta (015).';\n\nGRANT SELECT ON public.v_financial_line_margin TO authenticated;\nGRANT SELECT ON public.v_financial_line_margin TO service_role;\n\n-- ── e a mesma pergunta no tamanho em que o dono a faz ───────────────────────\nCREATE OR REPLACE VIEW public.v_financial_margin_by_month\nWITH (security_invoker = 'true') AS\nSELECT tenant_id,\n unit_id,\n date_trunc('month', coalesce(competence_date, current_date))::date AS competence_month,\n count(*)::int AS lines,\n sum(revenue)::numeric(14,2) AS revenue,\n sum(cogs)::numeric(14,2) AS cogs,\n sum(commission)::numeric(14,2) AS commission,\n sum(margin)::numeric(14,2) AS margin,\n CASE WHEN sum(revenue) > 0\n THEN round((sum(margin) / sum(revenue)) * 100, 2) ELSE 0 END AS margin_percent\n FROM public.v_financial_line_margin\n GROUP BY tenant_id, unit_id, date_trunc('month', coalesce(competence_date, current_date))::date;\n\nCOMMENT ON VIEW public.v_financial_margin_by_month IS\n 'A margem do mês: receita, CMV, comissão e o que sobrou. A pergunta que o dono faz, no tamanho em que ele a faz (015).';\n\nGRANT SELECT ON public.v_financial_margin_by_month TO authenticated;\nGRANT SELECT ON public.v_financial_margin_by_month TO service_role;\n`\n\nexport const MIGRATION_016_THE_OVERDUE_BILL_GETS_A_LETTER = `-- 016_the_overdue_bill_gets_a_letter.sql\n--\n-- Uma parcela vence e nada acontece. Ninguém é avisado, nada é escalado, e a\n-- inadimplência só aparece quando alguém abre a lista e olha — o que é o mesmo\n-- que dizer: quando já é tarde.\n--\n-- \\`chain.json\\` diz, sobre o plugin de notificações: \"a perna de cobrança do\n-- financeiro (…) é este motor com outro template, então ele tem de estar em\n-- todo cluster para esses fluxos existirem\". O motor está lá desde então e a\n-- perna nunca foi ligada.\n--\n-- DUAS PEÇAS, e a primeira vale sozinha:\n--\n-- A IDADE DA DÍVIDA. Um saldo em aberto não é um número, são faixas: o que\n-- vence amanhã, o que venceu ontem, o que venceu há noventa dias. A primeira\n-- se cobra com um lembrete, a última com uma decisão. Uma lista sem faixas\n-- trata as três igual, que é como se perde a primeira e se insiste na\n-- terceira.\n--\n-- O AVISO, por faixa, com deduplicação. Cobrar a mesma pessoa pela mesma\n-- parcela todo dia é como não cobrar: ela para de ler. A chave de dedupe é\n-- parcela + faixa, então cada degrau avisa UMA vez, e o próximo degrau é um\n-- evento novo porque a dívida mudou de idade.\n--\n-- E COBRAR NÃO PODE DERRUBAR NADA: se o motor de notificações não estiver\n-- instalado, a idade continua sendo calculada e o aviso simplesmente não sai.\n-- A resolução é em tempo de execução, como no CMV (015) — a corrente aplica o\n-- financeiro antes do resto, e um financeiro que exige o mundo inteiro para\n-- existir não serve a quem vende só serviço.\n\n-- ── a idade da dívida ───────────────────────────────────────────────────────\nCREATE OR REPLACE VIEW public.v_financial_aging\nWITH (security_invoker = 'true') AS\nSELECT b.tenant_id,\n b.unit_id,\n b.installment_id,\n b.invoice_id,\n b.invoice_number,\n b.direction,\n b.counterparty_person_id,\n p.name AS counterparty_name,\n b.due_date,\n b.amount,\n b.paid,\n b.balance,\n (current_date - b.due_date)::int AS days_overdue,\n -- As faixas que um cobrador usa, e não intervalos redondos por estética:\n -- \\`current\\` ainda não venceu; \\`d1_15\\` é esquecimento; \\`d16_30\\` é aperto;\n -- \\`d31_60\\` já pede conversa; acima disso é decisão.\n CASE\n WHEN b.due_date >= current_date THEN 'current'\n WHEN current_date - b.due_date <= 15 THEN 'd1_15'\n WHEN current_date - b.due_date <= 30 THEN 'd16_30'\n WHEN current_date - b.due_date <= 60 THEN 'd31_60'\n WHEN current_date - b.due_date <= 90 THEN 'd61_90'\n ELSE 'd90_plus'\n END AS bucket\n FROM public.v_financial_installment_balances b\n LEFT JOIN public.people p ON p.id = b.counterparty_person_id\n WHERE b.status <> 'cancelled'\n AND b.balance > 0;\n\nCOMMENT ON VIEW public.v_financial_aging IS\n 'O saldo em aberto por IDADE, não por total: o que vence amanhã se cobra com um lembrete e o que venceu há noventa dias com uma decisão (016).';\n\nGRANT SELECT ON public.v_financial_aging TO authenticated;\nGRANT SELECT ON public.v_financial_aging TO service_role;\n\n-- ── o aviso ─────────────────────────────────────────────────────────────────\nCREATE OR REPLACE FUNCTION public.financial_dunning_run(p_bucket text DEFAULT NULL)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_row record;\n v_sent int := 0;\n v_skip int := 0;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_dunning_run: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.read', NULL) THEN\n RAISE EXCEPTION 'financial_dunning_run: exige financial.read' USING ERRCODE = '42501';\n END IF;\n\n -- Sem motor instalado a idade continua valendo; o aviso é que não sai.\n IF to_regprocedure('public.notifications_enqueue(uuid, text, uuid, jsonb, text, timestamptz, text, text, uuid, uuid)') IS NULL THEN\n RETURN jsonb_build_object('sent', 0, 'skipped', 0,\n 'note', 'notifications engine not installed - aging still available');\n END IF;\n\n FOR v_row IN\n SELECT * FROM public.v_financial_aging a\n WHERE a.tenant_id = v_tenant\n -- Só o que a CASA tem a receber. Uma conta a pagar vencida é problema\n -- da casa com o fornecedor, e mandar cobrança ao fornecedor por ela é\n -- o oposto do que se quer.\n AND a.direction = 'receivable'\n AND a.bucket <> 'current'\n AND (p_bucket IS NULL OR a.bucket = p_bucket)\n AND a.counterparty_person_id IS NOT NULL\n LOOP\n BEGIN\n -- parcela + faixa: cada degrau avisa UMA vez. Cobrar todo dia é como não\n -- cobrar — a pessoa para de ler.\n EXECUTE format(\n 'SELECT public.notifications_enqueue(%L::uuid, %L, %L::uuid, %L::jsonb, %L)',\n v_tenant,\n 'financial.dunning.' || v_row.bucket,\n v_row.counterparty_person_id,\n jsonb_build_object(\n 'invoice_number', v_row.invoice_number,\n 'due_date', v_row.due_date,\n 'amount', v_row.balance,\n 'days_overdue', v_row.days_overdue,\n 'bucket', v_row.bucket),\n 'dunning:' || v_row.installment_id::text || ':' || v_row.bucket);\n v_sent := v_sent + 1;\n EXCEPTION WHEN OTHERS THEN\n -- Um template ausente ou um canal bloqueado não derruba a régua inteira:\n -- avisa quem dá, conta quem não deu.\n v_skip := v_skip + 1;\n END;\n END LOOP;\n\n RETURN jsonb_build_object('sent', v_sent, 'skipped', v_skip, 'bucket', p_bucket);\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_dunning_run(text) IS\n 'Enfileira o aviso de cobrança por faixa de atraso, deduplicado por parcela + faixa. Sem o motor de notificações a idade continua valendo e o aviso não sai (016).';\n\nREVOKE ALL ON FUNCTION public.financial_dunning_run(text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_dunning_run(text) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_dunning_run(text) TO service_role;\n\n-- ── e o resumo que a tela mostra ────────────────────────────────────────────\nCREATE OR REPLACE VIEW public.v_financial_aging_summary\nWITH (security_invoker = 'true') AS\nSELECT tenant_id, unit_id, direction, bucket,\n count(*)::int AS installments,\n sum(balance)::numeric(14,2) AS balance\n FROM public.v_financial_aging\n GROUP BY tenant_id, unit_id, direction, bucket;\n\nCOMMENT ON VIEW public.v_financial_aging_summary IS\n 'Quanto há em cada faixa de atraso — a foto que abre a conversa sobre inadimplência (016).';\n\nGRANT SELECT ON public.v_financial_aging_summary TO authenticated;\nGRANT SELECT ON public.v_financial_aging_summary TO service_role;\n`\n\nexport const MIGRATION_017_THE_RECEIPT_IS_A_QUERY_NOT_A_LAYOUT = `-- 017_the_receipt_is_a_query_not_a_layout.sql\n--\n-- Imprimir fatura e recibo é pedido de tela, e a tela é a parte fácil. A parte\n-- que erra é a REUNIÃO DOS DADOS: quem emitiu (com CNPJ e endereço), quem\n-- pagou, o que foi comprado, quanto de imposto, o que já foi pago e quanto\n-- falta. Hoje isso é sete consultas espalhadas, e cada tela que imprime junta\n-- de um jeito — três telas, três recibos diferentes da mesma venda.\n--\n-- Então o recibo é uma CONSULTA, e o layout fica com quem desenha. HTML,\n-- cupom de 58mm, PDF, e-mail: todos leem a mesma linha e nenhum inventa um\n-- total próprio.\n--\n-- POR QUE JSON PARA AS LINHAS E OS PAGAMENTOS. Um recibo é UM documento com N\n-- itens; devolvê-lo como N linhas obrigaria toda tela a reagrupar, e é no\n-- reagrupamento que os totais divergem. Agregado uma vez, aqui, onde há um\n-- lugar só para consertar.\n--\n-- O RECIBO NÃO É DOCUMENTO FISCAL. É o comprovante que o cliente leva; a nota\n-- é \\`plg_financial_nfe\\`, com numeração, série e emissão próprias. Confundir os\n-- dois é como imprimir um cupom e achar que declarou.\n\nCREATE OR REPLACE VIEW public.v_financial_receipt\nWITH (security_invoker = 'true') AS\nSELECT f.id AS invoice_id,\n f.tenant_id,\n f.unit_id,\n f.number AS invoice_number,\n f.direction,\n f.issue_date,\n f.competence_date,\n f.total,\n f.description,\n f.order_id,\n o.reference_number AS order_number,\n\n -- Quem emitiu: sem CNPJ um recibo é um papel, e o cliente que pede \"com\n -- CNPJ\" está pedindo exatamente esta linha.\n e.id AS issuer_id,\n e.legal_name AS issuer_legal_name,\n e.trade_name AS issuer_trade_name,\n e.document_number AS issuer_document,\n e.tax_regime AS issuer_tax_regime,\n e.city AS issuer_city,\n e.state AS issuer_state,\n\n -- Quem pagou.\n p.id AS counterparty_id,\n p.name AS counterparty_name,\n p.email AS counterparty_email,\n p.phone AS counterparty_phone,\n\n -- O que foi comprado. Da linha da FATURA quando ela existe (é o que foi\n -- cobrado), caindo para a do pedido quando não — uma fatura antiga pode\n -- não ter linhas próprias, e um recibo sem itens é pior que um recibo\n -- com os itens da venda.\n coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'name', i.name, 'quantity', i.quantity,\n 'unit_price', i.unit_price, 'discount', i.discount, 'total', i.total)\n ORDER BY i.sort_order)\n FROM public.items i WHERE i.invoice_id = f.id\n ), (\n SELECT jsonb_agg(jsonb_build_object(\n 'name', i.name, 'quantity', i.quantity,\n 'unit_price', i.unit_price, 'discount', i.discount, 'total', i.total)\n ORDER BY i.sort_order)\n FROM public.items i WHERE i.order_id = f.order_id\n ), '[]'::jsonb) AS lines,\n\n -- Os tributos, por tributo. Vazio até alguém apurar, e vazio é honesto:\n -- imprimir \"impostos: R$ 0,00\" quando ninguém calculou é pior.\n coalesce((\n SELECT jsonb_agg(jsonb_build_object('tax', t.tax, 'base', t.base_amount,\n 'rate', t.rate, 'amount', t.amount)\n ORDER BY t.tax)\n FROM public.plg_financial_invoice_item_taxes t\n JOIN public.items i ON i.id = t.item_id\n WHERE i.invoice_id = f.id\n ), '[]'::jsonb) AS taxes,\n\n -- As parcelas, com o que cada uma já recebeu.\n coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'number', b.number, 'due_date', b.due_date,\n 'amount', b.amount, 'paid', b.paid, 'status', b.status)\n ORDER BY b.number)\n FROM public.v_financial_installment_balances b\n WHERE b.invoice_id = f.id\n ), '[]'::jsonb) AS installments,\n\n -- E como o dinheiro entrou, que é o que faz um recibo ser recibo.\n coalesce((\n SELECT jsonb_agg(jsonb_build_object(\n 'date', m.payment_date, 'amount', m.paid_amount,\n 'method', m.method, 'card_brand', m.card_brand,\n 'installments', m.card_installments)\n ORDER BY m.payment_date)\n FROM public.plg_financial_movements m\n WHERE m.financial_invoice_id = f.id\n AND m.movement_kind = 'payment'\n AND m.status = 'paid'\n AND m.reversed_by_id IS NULL\n ), '[]'::jsonb) AS payments,\n\n (SELECT coalesce(sum(b.paid), 0) FROM public.v_financial_installment_balances b\n WHERE b.invoice_id = f.id)::numeric(14,2) AS total_paid,\n (SELECT coalesce(sum(b.balance), 0) FROM public.v_financial_installment_balances b\n WHERE b.invoice_id = f.id)::numeric(14,2) AS total_due\n\n FROM public.plg_financial_invoices f\n LEFT JOIN public.orders o ON o.id = f.order_id\n LEFT JOIN public.company_legal_entities e ON e.id = f.company_legal_entity_id\n LEFT JOIN public.people p ON p.id = f.counterparty_person_id\n WHERE f.cancelled_at IS NULL;\n\nCOMMENT ON VIEW public.v_financial_receipt IS\n 'Tudo o que um recibo precisa, numa linha: emitente com CNPJ, cliente, itens, tributos, parcelas e pagamentos. O layout fica com quem desenha; o total é calculado UMA vez (017).';\n\nGRANT SELECT ON public.v_financial_receipt TO authenticated;\nGRANT SELECT ON public.v_financial_receipt TO service_role;\n`\n\nexport const MIGRATION_018_THE_CAIXA_SHOWS_WHAT_IT_SHOULD_HAVE = `-- 018_the_caixa_shows_what_it_should_have.sql\n--\n-- A tela de fechamento mostrava uma diferença INVENTADA:\n--\n-- difference = contado − saldo_de_abertura\n--\n-- Um caixa que abriu com 100, vendeu 500 em dinheiro e tem 600 na gaveta está\n-- CERTO, e a tela anunciava \"sobra de 500\". A conta de quebra é contra o\n-- ESPERADO — abertura mais o que entrou menos o que saiu —, e o esperado a tela\n-- não tinha como saber: \\`expected_cash\\` só é gravado no fechamento, pela RPC.\n--\n-- Ou seja: o operador conferia contra um número, confirmava, e o sistema\n-- gravava outro. A tela mentia justamente no momento em que alguém assume\n-- responsabilidade por dinheiro contado.\n--\n-- Esta migration não muda o fechamento — a RPC já apura certo, contra\n-- \\`v_financial_account_balances\\`, e lança a quebra em conta de falta/sobra.\n-- Ela dá à TELA o mesmo número, ANTES de fechar.\n--\n-- E POR FORMA DE PAGAMENTO, não só dinheiro e cartão. O fechamento resume em\n-- dois baldes e o Pix não está em nenhum: quem recebe Pix pelo caixa fecha o\n-- turno sem ver o que recebeu. Aqui a quebra por forma é uma linha por forma,\n-- então acrescentar uma não exige mexer em balde nenhum.\nCREATE OR REPLACE VIEW public.v_financial_cash_session_expected\nWITH (security_invoker = 'true') AS\nSELECT s.id AS session_id,\n s.tenant_id,\n s.unit_id,\n s.account_id,\n s.bank_account_id,\n s.status,\n s.opened_at,\n s.user_id,\n s.opened_by_name,\n s.opening_balance,\n\n -- O esperado em dinheiro é o SALDO DA CONTA do caixa, que é exatamente o\n -- que a RPC de fechamento lê. Um segundo jeito de calcular seria um\n -- segundo número para divergir.\n round(coalesce((SELECT b.balance FROM public.v_financial_account_balances b\n WHERE b.account_id = s.account_id), 0), 2) AS expected_cash,\n\n -- E o que passou pelo caixa, por forma. \\`payment\\` liquidado e não\n -- estornado: é o que o turno de fato recebeu.\n coalesce((\n SELECT jsonb_object_agg(x.method, x.total)\n FROM (SELECT m.method, round(sum(m.paid_amount), 2) AS total\n FROM public.plg_financial_movements m\n WHERE m.cash_session_id = s.id\n AND m.movement_kind = 'payment'\n AND m.status = 'paid'\n AND m.reversed_by_id IS NULL\n GROUP BY m.method) x\n ), '{}'::jsonb) AS received_by_method,\n\n (SELECT count(*) FROM public.plg_financial_movements m\n WHERE m.cash_session_id = s.id\n AND m.movement_kind = 'payment'\n AND m.status = 'paid'\n AND m.reversed_by_id IS NULL)::int AS payment_count\n FROM public.plg_financial_cash_register_sessions s;\n\nCOMMENT ON VIEW public.v_financial_cash_session_expected IS\n 'O que o caixa DEVERIA ter, do mesmo lugar que a RPC de fechamento lê, e o recebido por forma de pagamento. Existe para a tela conferir contra o número real em vez de contra o saldo de abertura (018).';\n\nGRANT SELECT ON public.v_financial_cash_session_expected TO authenticated;\nGRANT SELECT ON public.v_financial_cash_session_expected TO service_role;\n`\n\nexport const MIGRATION_019_WHO_DID_IT_IS_ALREADY_KNOWN = `-- 019_who_did_it_is_already_known.sql\n--\n-- \\`plg_financial_item_participants\\` (008) diz QUEM executou cada linha, e a\n-- apuração de comissão (009) lê exatamente essa tabela. Fora as fixtures de\n-- teste, NADA NUNCA ESCREVEU NELA.\n--\n-- O efeito é que o motor de comissão não podia apurar nada em operação real: a\n-- tela de Comissões diz, com honestidade, \"sem participante na linha não há a\n-- quem pagar\" — e não havia caminho nenhum para que existisse participante. Seis\n-- tabelas, uma RPC e duas vistas ficavam inalcançáveis por falta de um elo.\n--\n-- E O ELO JÁ EXISTIA. O agendamento nomeia o profissional (\\`assignee_id\\`), e as\n-- linhas da venda saem desse mesmo agendamento. Quem executou o serviço não é\n-- uma informação nova a ser digitada — é uma que a casa já tem no momento em que\n-- marca o horário.\n--\n-- Então o participante é DERIVADO, não cadastrado:\n--\n-- uma linha de pedido cuja venda veio de um agendamento recebe o profissional\n-- daquele agendamento como executor, com fração 1.\n--\n-- POR QUE \\`ON CONFLICT DO NOTHING\\`, E NÃO UPDATE. A fração é editável: um\n-- atendimento a quatro mãos divide 0,6/0,4, e isso é decisão de quem fecha a\n-- comanda. Uma vez que alguém tocou no rateio, o gatilho não pode desfazer —\n-- ele semeia o caso comum e sai do caminho.\n--\n-- DOIS SENTIDOS, porque a ordem varia: a linha pode nascer depois do\n-- agendamento (o de sempre) ou o profissional ser trocado depois de a linha\n-- existir (remarcação). Um gatilho só cobriria metade.\n\n-- ── a linha nasce: herda o profissional do agendamento ─────────────────────\nCREATE OR REPLACE FUNCTION public.trg_financial_seed_item_participant()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE v_person uuid; v_unit uuid;\nBEGIN\n IF NEW.order_id IS NULL THEN RETURN NEW; END IF;\n\n SELECT a.assignee_id, a.unit_id INTO v_person, v_unit\n FROM public.appointments a\n WHERE a.order_id = NEW.order_id\n AND a.assignee_id IS NOT NULL\n ORDER BY a.created_at\n LIMIT 1;\n\n IF v_person IS NULL THEN RETURN NEW; END IF;\n\n INSERT INTO public.plg_financial_item_participants\n (tenant_id, unit_id, item_id, person_id, role, share)\n VALUES (NEW.tenant_id, coalesce(NEW.unit_id, v_unit), NEW.id, v_person, 'executor', 1)\n ON CONFLICT (item_id, person_id, role) DO NOTHING;\n\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS items_financial_seed_participant ON public.items;\nCREATE TRIGGER items_financial_seed_participant\n AFTER INSERT ON public.items\n FOR EACH ROW\n EXECUTE FUNCTION public.trg_financial_seed_item_participant();\n\n-- ── o profissional muda: as linhas daquele pedido acompanham ───────────────\nCREATE OR REPLACE FUNCTION public.trg_financial_seed_participants_of_appointment()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nBEGIN\n IF NEW.order_id IS NULL OR NEW.assignee_id IS NULL THEN RETURN NEW; END IF;\n\n INSERT INTO public.plg_financial_item_participants\n (tenant_id, unit_id, item_id, person_id, role, share)\n SELECT i.tenant_id, coalesce(i.unit_id, NEW.unit_id), i.id, NEW.assignee_id, 'executor', 1\n FROM public.items i\n WHERE i.order_id = NEW.order_id\n ON CONFLICT (item_id, person_id, role) DO NOTHING;\n\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS appointments_financial_seed_participants ON public.appointments;\nCREATE TRIGGER appointments_financial_seed_participants\n AFTER INSERT OR UPDATE OF assignee_id, order_id ON public.appointments\n FOR EACH ROW\n EXECUTE FUNCTION public.trg_financial_seed_participants_of_appointment();\n\nCOMMENT ON FUNCTION public.trg_financial_seed_item_participant() IS\n 'Deriva o executor da linha do profissional do agendamento. Sem isto a apuração de comissão nunca tinha a quem pagar (019).';\n\n-- ── e o que já foi vendido antes deste arquivo ─────────────────────────────\nINSERT INTO public.plg_financial_item_participants\n (tenant_id, unit_id, item_id, person_id, role, share)\nSELECT i.tenant_id, coalesce(i.unit_id, a.unit_id), i.id, a.assignee_id, 'executor', 1\n FROM public.items i\n JOIN public.appointments a ON a.order_id = i.order_id AND a.assignee_id IS NOT NULL\n WHERE i.order_id IS NOT NULL\nON CONFLICT (item_id, person_id, role) DO NOTHING;\n`\n\nexport const MIGRATION_020_A_RULE_WITH_NOBODY_TO_PAY_PAYS_THE_EXECUTOR = `-- 020_a_rule_with_nobody_to_pay_pays_the_executor.sql\n--\n-- A semeadura da função default vive na APURAÇÃO (009): no AFTER INSERT da\n-- regra as funções ainda não existem, e um gatilho de cadastro entrava por\n-- cima de quem estava prestes a declarar as suas — foi assim que a suíte 008,\n-- que cadastra regra E funções, passou a apurar três lançamentos onde espera\n-- dois.\n--\n-- Aqui fica só o que a apuração não alcança sozinha: derrubar o gatilho que a\n-- primeira versão deste arquivo criou, e dar função às regras que já foram\n-- cadastradas sem nenhuma.\nDROP TRIGGER IF EXISTS commission_rules_default_function ON public.plg_financial_commission_rules;\nDROP FUNCTION IF EXISTS public.trg_financial_default_commission_function();\n\nINSERT INTO public.plg_financial_commission_functions\n (tenant_id, rule_id, function_name, account_type, value_type, value_amount,\n calculation_base, display_order, is_active)\nSELECT r.tenant_id, r.id, coalesce(nullif(r.name, ''), 'Executor'),\n 'professional_executor', r.value_type, r.value, 'net', 0, true\n FROM public.plg_financial_commission_rules r\n WHERE NOT EXISTS (SELECT 1 FROM public.plg_financial_commission_functions f\n WHERE f.rule_id = r.id);\n`\n\nexport const MIGRATION_021_THE_MOST_SPECIFIC_RULE_WINS = `-- 021_the_most_specific_rule_wins.sql\n--\n-- O V1 escreve a regra de resolução no subtítulo da própria tela:\n--\n-- \"A regra mais específica (mais critérios casados) vence; a padrão apanha o\n-- resto\"\n--\n-- e, no campo Prioridade, a legenda que completa a frase:\n--\n-- \"Desempate quando duas regras casam o mesmo número de critérios (maior\n-- vence)\"\n--\n-- Ou seja: ESPECIFICIDADE primeiro, prioridade só como desempate.\n--\n-- A 009 faz \\`ORDER BY r.priority DESC\\` — a prioridade É a ordenação. A\n-- diferença não é acadêmica: com a padrão em prioridade 99 (que é o que alguém\n-- faz quando acha que prioridade significa \"importância\") e \"Ultrassom 15%\" em\n-- 10, TODO atendimento de ultrassom comissiona 10% pela padrão, e a regra\n-- específica que a casa escreveu nunca pega. Ninguém descobre até o\n-- profissional reclamar do valor — e aí o dinheiro já saiu errado por um mês.\n--\n-- Como TODOS os critérios de uma regra têm de casar para ela se qualificar (é o\n-- \\`NOT EXISTS\\` de um critério que falha), a contagem de critérios da regra É a\n-- contagem de critérios casados. A especificidade sai de um \\`count(*)\\`, sem\n-- reavaliar nada.\n--\n-- E a padrão não precisa de desempate próprio: ela não tem critério, então\n-- pontua zero e cai naturalmente para o fim — que é a definição de \"apanha o\n-- resto\". O \\`is_default ASC\\` continua ali só para o caso de duas regras sem\n-- critério nenhum.\n--\n-- ── E \\`time_range\\` passa a restringir ───────────────────────────────────────\n--\n-- Dos nove critérios, quatro caíam num \\`ELSE true\\`. Três deles têm motivo que\n-- continua valendo: \\`profession\\` e \\`professional_group\\` não têm cadastro no V2\n-- para onde apontar, e \\`payment_method_type\\` só se conhece quando o pagamento\n-- acontece — e não quando a linha nasce, que é quando esta função roda.\n--\n-- \\`time_range\\` não tinha motivo nenhum: a hora está em \\`happened_at\\`, do lado\n-- do dia da semana que já restringe. \"Sábado à noite paga mais\" era metade\n-- verdade — o sábado pegava, a noite não.\n--\n-- A hora é lida como o dia da semana já é: no fuso da sessão. Coerência com o\n-- critério vizinho vale mais aqui do que exatidão isolada; o dia em que o\n-- \\`weekday\\` ganhar fuso de tenant, os dois ganham juntos.\nCREATE OR REPLACE FUNCTION public.plg_financial_commission_rule_for_item(p_item_id uuid, p_person_id uuid)\n RETURNS uuid\n LANGUAGE plpgsql\n STABLE\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_item record;\n v_rule uuid;\nBEGIN\n SELECT i.id, i.tenant_id, i.unit_id, i.product_id, i.order_id, i.invoice_id,\n coalesce(o.created_at, i.created_at) AS happened_at,\n p.category_id\n INTO v_item\n FROM public.items i\n LEFT JOIN public.orders o ON o.id = coalesce(i.order_id,\n (SELECT f2.order_id FROM public.plg_financial_invoices f2 WHERE f2.id = i.invoice_id))\n LEFT JOIN public.products p ON p.id = i.product_id\n WHERE i.id = p_item_id;\n IF v_item.id IS NULL THEN RETURN NULL; END IF;\n\n SELECT r.id INTO v_rule\n FROM public.plg_financial_commission_rules r\n WHERE r.tenant_id = v_item.tenant_id\n AND r.is_active\n AND (r.unit_id IS NULL OR r.unit_id = v_item.unit_id)\n AND NOT EXISTS (\n SELECT 1 FROM public.plg_financial_commission_rule_criteria c\n WHERE c.rule_id = r.id\n AND NOT CASE c.criteria_type\n WHEN 'service' THEN\n v_item.product_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'category' THEN\n v_item.category_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'professional' THEN\n p_person_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'unit' THEN\n v_item.unit_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'weekday' THEN\n (extract(dow FROM v_item.happened_at))::int IN (\n SELECT (jsonb_array_elements_text(coalesce(c.criteria_value -> 'days', '[]'::jsonb)))::int)\n -- Uma faixa que termina antes de começar atravessa a meia-noite\n -- (\"22:00 às 02:00\"), e é assim que se escreve o adicional\n -- noturno. Tratada como duas: até o fim do dia, e do começo dele.\n WHEN 'time_range' THEN\n EXISTS (\n SELECT 1\n FROM jsonb_array_elements(coalesce(c.criteria_value -> 'ranges', '[]'::jsonb)) rg\n WHERE (rg ->> 'from') IS NOT NULL AND (rg ->> 'to') IS NOT NULL\n AND CASE\n WHEN (rg ->> 'from')::time <= (rg ->> 'to')::time THEN\n v_item.happened_at::time >= (rg ->> 'from')::time\n AND v_item.happened_at::time <= (rg ->> 'to')::time\n ELSE\n v_item.happened_at::time >= (rg ->> 'from')::time\n OR v_item.happened_at::time <= (rg ->> 'to')::time\n END)\n -- Sem cadastro no V2 (\\`profession\\`, \\`professional_group\\`) ou\n -- desconhecido no nascimento da linha (\\`payment_method_type\\`).\n -- Deixar passar comissiona a mais e alguém vê; recusar em silêncio\n -- não comissiona e ninguém vê. A TELA diz quais são estes.\n ELSE true\n END)\n -- A especificidade primeiro, como no V1. Todos os critérios da regra casam\n -- (senão ela nem chega aqui), logo contá-los é contar os que casaram.\n ORDER BY (SELECT count(*) FROM public.plg_financial_commission_rule_criteria c2\n WHERE c2.rule_id = r.id) DESC,\n r.priority DESC,\n r.is_default ASC,\n r.created_at\n LIMIT 1;\n\n IF v_rule IS NOT NULL THEN RETURN v_rule; END IF;\n\n SELECT r.id INTO v_rule FROM public.plg_financial_commission_rules r\n WHERE r.tenant_id = v_item.tenant_id AND r.is_active AND r.is_default\n ORDER BY r.priority DESC LIMIT 1;\n RETURN v_rule;\nEND $$;\n\nCOMMENT ON FUNCTION public.plg_financial_commission_rule_for_item(uuid, uuid) IS\n 'A regra MAIS ESPECÍFICA (mais critérios casados) cujos critérios todos casam; prioridade só desempata; senão a padrão da casa. Critério ausente não restringe (021).';\n`\n\nexport const MIGRATION_022_AN_UNKNOWN_ATTRIBUTE_DOES_NOT_MATCH = `-- 022_an_unknown_attribute_does_not_match.sql\n--\n-- Uma linha chamada \"Hidratação\" estava comissionando pela regra \"Coloração\n-- 15%\". Não é erro de digitação: é \\`NULL IN (...)\\`.\n--\n-- As linhas que nascem da agenda não têm \\`product_id\\` — carregam só o nome do\n-- serviço. Sem produto não há categoria, então \\`v_item.category_id\\` é NULL, e a\n-- comparação do critério vira:\n--\n-- NULL::text IN ('91d80fff-…') --> NULL (não FALSE)\n-- NOT NULL --> NULL\n--\n-- e um \\`WHERE … AND NULL\\` não devolve linha. O \\`NOT EXISTS\\` que procura um\n-- critério REPROVADO não acha nenhum, e conclui que a regra passou em todos.\n--\n-- O efeito é o pior possível numa conta de comissão: **um atributo\n-- desconhecido faz TODO critério passar**. A linha sem produto casa com toda\n-- regra de categoria e toda regra de serviço da casa, e como a 021 ordena por\n-- especificidade, quem a captura é justamente a regra mais específica que\n-- existir — a que menos tem a ver com ela. Quanto melhor configurada a casa,\n-- mais errado o resultado.\n--\n-- O certo é o oposto e é o que qualquer pessoa diria em voz alta: se eu não sei\n-- a categoria da linha, ela NÃO casa com uma regra que exige categoria. Ela cai\n-- para uma regra sem aquele critério, ou para a padrão — que é a regra que\n-- existe exatamente para isso.\n--\n-- Um atributo desconhecido REPROVA o critério que depende dele. Nada de\n-- \\`coalesce\\` para um valor sentinela: o que se quer dizer é \"não sei\", e não \"é\n-- vazio\".\n--\n-- NÃO CONFUNDIR COM CRITÉRIO AUSENTE. Uma regra que não declara critério de\n-- categoria continua valendo para qualquer categoria — isso é a regra sendo\n-- ampla, e continua igual. O que muda é a LINHA que não sabe a sua categoria\n-- diante de uma regra que exige uma.\nCREATE OR REPLACE FUNCTION public.plg_financial_commission_rule_for_item(p_item_id uuid, p_person_id uuid)\n RETURNS uuid\n LANGUAGE plpgsql\n STABLE\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_item record;\n v_rule uuid;\nBEGIN\n SELECT i.id, i.tenant_id, i.unit_id, i.product_id, i.order_id, i.invoice_id,\n coalesce(o.created_at, i.created_at) AS happened_at,\n p.category_id\n INTO v_item\n FROM public.items i\n LEFT JOIN public.orders o ON o.id = coalesce(i.order_id,\n (SELECT f2.order_id FROM public.plg_financial_invoices f2 WHERE f2.id = i.invoice_id))\n LEFT JOIN public.products p ON p.id = i.product_id\n WHERE i.id = p_item_id;\n IF v_item.id IS NULL THEN RETURN NULL; END IF;\n\n SELECT r.id INTO v_rule\n FROM public.plg_financial_commission_rules r\n WHERE r.tenant_id = v_item.tenant_id\n AND r.is_active\n AND (r.unit_id IS NULL OR r.unit_id = v_item.unit_id)\n AND NOT EXISTS (\n SELECT 1 FROM public.plg_financial_commission_rule_criteria c\n WHERE c.rule_id = r.id\n -- \\`coalesce(…, false)\\`: o CASE pode devolver NULL quando o atributo da\n -- linha é desconhecido, e \\`NOT NULL\\` não reprova nada. Aqui,\n -- desconhecido reprova.\n AND NOT coalesce(CASE c.criteria_type\n WHEN 'service' THEN\n v_item.product_id IS NOT NULL AND v_item.product_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'category' THEN\n v_item.category_id IS NOT NULL AND v_item.category_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'professional' THEN\n p_person_id IS NOT NULL AND p_person_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'unit' THEN\n v_item.unit_id IS NOT NULL AND v_item.unit_id::text IN (\n SELECT jsonb_array_elements_text(coalesce(c.criteria_value -> 'ids', '[]'::jsonb)))\n WHEN 'weekday' THEN\n v_item.happened_at IS NOT NULL\n AND (extract(dow FROM v_item.happened_at))::int IN (\n SELECT (jsonb_array_elements_text(coalesce(c.criteria_value -> 'days', '[]'::jsonb)))::int)\n -- Uma faixa que termina antes de começar atravessa a meia-noite\n -- (\"22:00 às 02:00\"), e é assim que se escreve o adicional noturno.\n WHEN 'time_range' THEN\n v_item.happened_at IS NOT NULL AND EXISTS (\n SELECT 1\n FROM jsonb_array_elements(coalesce(c.criteria_value -> 'ranges', '[]'::jsonb)) rg\n WHERE (rg ->> 'from') IS NOT NULL AND (rg ->> 'to') IS NOT NULL\n AND CASE\n WHEN (rg ->> 'from')::time <= (rg ->> 'to')::time THEN\n v_item.happened_at::time >= (rg ->> 'from')::time\n AND v_item.happened_at::time <= (rg ->> 'to')::time\n ELSE\n v_item.happened_at::time >= (rg ->> 'from')::time\n OR v_item.happened_at::time <= (rg ->> 'to')::time\n END)\n -- Sem cadastro no V2 (\\`profession\\`, \\`professional_group\\`) ou\n -- desconhecido no nascimento da linha (\\`payment_method_type\\`).\n ELSE true\n END, false))\n -- Especificidade primeiro, prioridade só desempata (021).\n ORDER BY (SELECT count(*) FROM public.plg_financial_commission_rule_criteria c2\n WHERE c2.rule_id = r.id) DESC,\n r.priority DESC,\n r.is_default ASC,\n r.created_at\n LIMIT 1;\n\n IF v_rule IS NOT NULL THEN RETURN v_rule; END IF;\n\n SELECT r.id INTO v_rule FROM public.plg_financial_commission_rules r\n WHERE r.tenant_id = v_item.tenant_id AND r.is_active AND r.is_default\n ORDER BY r.priority DESC LIMIT 1;\n RETURN v_rule;\nEND $$;\n\nCOMMENT ON FUNCTION public.plg_financial_commission_rule_for_item(uuid, uuid) IS\n 'A regra MAIS ESPECÍFICA cujos critérios todos casam; prioridade desempata; senão a padrão. Atributo desconhecido na linha REPROVA o critério que depende dele — uma linha sem produto não casa com regra de categoria (022).';\n`\n\nexport const MIGRATION_023_REAPURAR_NAO_CRIA_UM_LOTE_ORFAO = `-- 023_reapurar_nao_cria_um_lote_orfao.sql\n--\n-- Apurar o mesmo período duas vezes criava DOIS lotes, e o segundo nascia\n-- mentindo: cabeçalho dizendo \"5 lançamentos · R$ 237,00\" com ZERO lançamentos\n-- apontando para ele.\n--\n-- A apuração é idempotente por linha + pessoa + função (009) — reapurar não\n-- duplica lançamento, e isso está certo. Mas o LOTE era criado antes, sempre\n-- novo, e os lançamentos que já existiam continuavam presos ao lote da primeira\n-- vez. O contador do lote conta o que a rodada ENCONTROU; o vínculo, só o que\n-- ela CRIOU. As duas contas divergem já na segunda execução.\n--\n-- E o estrago não é cosmético, porque os dois passos seguintes trabalham por\n-- \\`generation_id\\`:\n--\n-- \\`financial_commission_approve\\` aprova os lançamentos DAQUELE lote — zero\n-- deles — mas marca o lote como \\`approved\\` assim mesmo. A tela avança.\n-- \\`financial_commission_pay_run\\` recolhe o que está \\`approved\\` naquele lote —\n-- nada — e devolve \"0 contas a pagar · R$ 0,00\", sem erro nenhum.\n--\n-- Ou seja: quem apurasse duas vezes (o gesto mais natural do mundo, porque a\n-- primeira vez veio antes de a última venda fechar) não conseguia mais pagar\n-- comissão nenhuma, e a tela não dizia por quê. Só o silêncio de um borderô\n-- vazio.\n--\n-- O CONSERTO: um período aberto tem UM lote. Se já existe um lote \\`draft\\` para\n-- o mesmo tenant, unidade e período, a apuração REUSA — e adota os lançamentos\n-- pendentes daquele período que ficaram em lotes \\`draft\\` anteriores, que é o\n-- que o operador entende por \"reapurar\".\n--\n-- O que NÃO se mexe: lote aprovado ou pago é história, e história não se\n-- reescreve. Reapurar depois de aprovar abre um lote novo de verdade, para o\n-- que sobrou.\nCREATE OR REPLACE FUNCTION public.plg_financial_commission_open_generation(\n p_tenant uuid, p_unit_id uuid, p_period_start date, p_period_end date\n) RETURNS uuid\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE v_gen uuid;\nBEGIN\n -- \\`IS NOT DISTINCT FROM\\`: a unidade pode ser NULL (a casa toda), e \\`= NULL\\`\n -- nunca casa — era assim que um segundo lote nascia mesmo com esta busca.\n SELECT id INTO v_gen\n FROM public.plg_financial_commission_generations\n WHERE tenant_id = p_tenant\n AND unit_id IS NOT DISTINCT FROM p_unit_id\n AND period_start = p_period_start\n AND period_end = p_period_end\n AND status = 'draft'\n ORDER BY created_at\n LIMIT 1;\n\n IF v_gen IS NULL THEN\n INSERT INTO public.plg_financial_commission_generations\n (tenant_id, unit_id, period_start, period_end, status, generated_by_user_id)\n VALUES (p_tenant, p_unit_id, p_period_start, p_period_end, 'draft', auth.uid())\n RETURNING id INTO v_gen;\n END IF;\n\n -- Adota o que ficou para trás: lançamento ainda pendente, de um lote que\n -- também era rascunho do mesmo período. Sem isto o lote reusado continuaria\n -- contando lançamentos que não são dele.\n UPDATE public.plg_financial_commission_entries e\n SET generation_id = v_gen, updated_at = now()\n FROM public.plg_financial_commission_generations g\n WHERE e.generation_id = g.id\n AND e.tenant_id = p_tenant\n AND e.status = 'pending'\n AND e.generation_id <> v_gen\n AND g.status = 'draft'\n AND g.period_start = p_period_start\n AND g.period_end = p_period_end\n AND g.unit_id IS NOT DISTINCT FROM p_unit_id;\n\n -- E os rascunhos que ficaram vazios não são história de nada: some com eles,\n -- senão a lista de lotes vira uma pilha de cabeçalhos zerados.\n DELETE FROM public.plg_financial_commission_generations g\n WHERE g.tenant_id = p_tenant\n AND g.status = 'draft'\n AND g.id <> v_gen\n AND g.period_start = p_period_start\n AND g.period_end = p_period_end\n AND g.unit_id IS NOT DISTINCT FROM p_unit_id\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_commission_entries e\n WHERE e.generation_id = g.id);\n\n RETURN v_gen;\nEND $$;\n\nCOMMENT ON FUNCTION public.plg_financial_commission_open_generation(uuid, uuid, date, date) IS\n 'O lote ABERTO do período: reusa o rascunho que já existe, adota os lançamentos pendentes que ficaram em rascunhos irmãos e recolhe os vazios. Um período aberto tem um lote (023).';\n\nREVOKE ALL ON FUNCTION public.plg_financial_commission_open_generation(uuid, uuid, date, date) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.plg_financial_commission_open_generation(uuid, uuid, date, date) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_financial_commission_open_generation(uuid, uuid, date, date) TO service_role;\n\n-- ── e a apuração passa a pedir o lote em vez de criar um ────────────────────\nDO $$\nDECLARE v_src text;\nBEGIN\n SELECT pg_get_functiondef(p.oid) INTO v_src\n FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'public' AND p.proname = 'financial_commission_generate'\n LIMIT 1;\n\n IF v_src IS NULL THEN\n RAISE EXCEPTION '023: financial_commission_generate não existe — a 009 tem de vir antes';\n END IF;\n\n -- Troca cirúrgica do INSERT do lote pela chamada que o resolve. Reescrever a\n -- função inteira aqui a congelaria: qualquer correção futura na 009 seria\n -- desfeita por este arquivo ao ser replicado.\n v_src := replace(v_src,\n 'INSERT INTO public.plg_financial_commission_generations\n (tenant_id, unit_id, period_start, period_end, status, generated_by_user_id)\n VALUES (v_tenant, p_unit_id, p_period_start, p_period_end, ''draft'', auth.uid())\n RETURNING id INTO v_gen;',\n 'v_gen := public.plg_financial_commission_open_generation(\n v_tenant, p_unit_id, p_period_start, p_period_end);');\n\n IF position('plg_financial_commission_open_generation' in v_src) = 0 THEN\n RAISE EXCEPTION '023: não encontrei o INSERT do lote em financial_commission_generate — a 009 mudou de forma';\n END IF;\n\n EXECUTE v_src;\nEND $$;\n`\n\nexport const MIGRATION_024_O_BORDERO_PAGA_O_QUE_ELE_SOMA = `-- 024_o_bordero_paga_o_que_ele_soma.sql\n--\n-- O borderô dizia \"4 contas · R$ 237,00\" e levantava 3 contas somando R$ 141,00.\n-- A conta de uma profissional saiu R$ 24,00 carregando dois lançamentos de\n-- R$ 24,00 e R$ 96,00 — R$ 96,00 que ninguém ia pagar, e que some do relatório\n-- porque o lançamento CONSTA como faturado.\n--\n-- São três lugares com grãos diferentes dentro da mesma função (012):\n--\n-- o laço agrupa por (pessoa, unidade)\n-- a chave de idempotência é lote + pessoa\n-- o UPDATE que vincula filtra por lote + pessoa ← sem unidade\n--\n-- Para quem atende numa unidade só os três coincidem, e por isso os testes\n-- passavam. Para quem atende em duas:\n--\n-- 1ª volta (pessoa, unidade A): soma 24, cria a conta com a chave\n-- \\`run:lote:pessoa\\`, e o UPDATE — que não filtra unidade — vincula os DOIS\n-- lançamentos, o de A e o de B.\n-- 2ª volta (pessoa, unidade B): soma 96, chama a criação com A MESMA chave,\n-- recebe de volta a conta de R$ 24,00 sem alterá-la, e não acha mais nada\n-- para vincular. O contador soma assim mesmo.\n--\n-- Daí o relatório certo e o dinheiro errado: \\`v_total\\` acumula o que o laço\n-- somou, e o banco guarda o que a primeira volta gravou.\n--\n-- O CONSERTO é escolher UM grão e usá-lo nos três lugares. Fica **pessoa +\n-- unidade**: a despesa é da unidade que a gerou, é ela que resolve o emitente\n-- da conta (007), e juntar unidades numa conta só esconderia de qual delas o\n-- custo saiu. Quem atende numa unidade só continua vendo uma conta por pessoa,\n-- que é o que a 012 promete e o que os testes cobram.\n-- ── e o lote ganha o estado que faltava ─────────────────────────────────────\n--\n-- \\`status\\` ia só até \\`approved\\`: um lote faturado ficava indistinguível de um\n-- lote apenas autorizado, e a tela não tinha como parar de oferecer \"gerar\n-- contas a pagar\" para quem já as tinha. O borderô é uma transição de verdade,\n-- então ela existe.\nDO $$ BEGIN\n ALTER TABLE public.plg_financial_commission_generations\n DROP CONSTRAINT IF EXISTS plg_financial_commission_generations_status_check;\n ALTER TABLE public.plg_financial_commission_generations\n ADD CONSTRAINT plg_financial_commission_generations_status_check\n CHECK (status = ANY (ARRAY['draft', 'approved', 'paid', 'cancelled']));\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.financial_commission_pay_run(\n p_generation_id uuid,\n p_due_date date DEFAULT NULL\n) RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_chart uuid;\n v_row record;\n v_payable uuid;\n v_bills int := 0;\n v_total numeric := 0;\n v_person text;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_commission_pay_run: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.commissions.manage', NULL) THEN\n RAISE EXCEPTION 'financial_commission_pay_run: exige financial.commissions.manage' USING ERRCODE = '42501';\n END IF;\n\n SELECT id INTO v_chart FROM public.plg_financial_chart_of_accounts\n WHERE tenant_id = v_tenant AND code = '4.02' LIMIT 1;\n\n FOR v_row IN\n SELECT e.person_id, e.unit_id, sum(e.amount) AS total, count(*) AS n\n FROM public.plg_financial_commission_entries e\n WHERE e.tenant_id = v_tenant\n AND e.generation_id = p_generation_id\n AND e.status = 'approved'\n AND e.payable_invoice_id IS NULL\n GROUP BY e.person_id, e.unit_id\n HAVING sum(e.amount) > 0\n LOOP\n SELECT name INTO v_person FROM public.people WHERE id = v_row.person_id;\n\n -- A chave carrega a UNIDADE, como o laço. Sem ela a segunda unidade da\n -- mesma pessoa batia na conta da primeira e voltava sem alterar valor.\n -- \\`coalesce\\` porque unidade nula é um grupo legítimo (casa sem filial) e\n -- concatenar NULL apagaria a chave inteira.\n v_payable := (public.financial_create_payable(\n 'Comissões · ' || coalesce(v_person, 'profissional') || ' · ' || v_row.n || ' lançamento(s)',\n round(v_row.total, 2),\n coalesce(p_due_date, current_date),\n v_row.person_id,\n v_row.unit_id,\n v_chart,\n NULL,\n NULL,\n NULL,\n 'commission-run:' || p_generation_id::text || ':' || v_row.person_id::text\n || ':' || coalesce(v_row.unit_id::text, 'sem-unidade')\n ) ->> 'invoice_id')::uuid;\n\n -- E vincula só o que ESTA conta soma. Sem o filtro de unidade a primeira\n -- volta levava os lançamentos das outras unidades junto, e eles ficavam\n -- pendurados numa conta que não os somou.\n UPDATE public.plg_financial_commission_entries\n SET payable_invoice_id = v_payable, updated_at = now()\n WHERE tenant_id = v_tenant AND generation_id = p_generation_id\n AND person_id = v_row.person_id\n AND unit_id IS NOT DISTINCT FROM v_row.unit_id\n AND status = 'approved'\n AND payable_invoice_id IS NULL;\n\n v_bills := v_bills + 1;\n v_total := v_total + v_row.total;\n END LOOP;\n\n UPDATE public.plg_financial_commission_generations\n SET status = 'paid', updated_at = now()\n WHERE tenant_id = v_tenant AND id = p_generation_id;\n\n RETURN jsonb_build_object('generation_id', p_generation_id, 'payables', v_bills,\n 'total', round(v_total, 2), 'due_date', coalesce(p_due_date, current_date));\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_commission_pay_run(uuid, date) IS\n 'Junta os lançamentos aprovados por pessoa E unidade e levanta UMA conta a pagar para cada grupo — o mesmo grão no laço, na chave de idempotência e no vínculo, senão a conta soma menos do que carrega (024).';\n`\n\nexport const MIGRATION_025_A_META_DO_MES_TEM_TRES_DEGRAUS = `-- 025_a_meta_do_mes_tem_tres_degraus.sql\n--\n-- O V1 acompanha meta mensal de faturamento por unidade, e não tem UMA meta:\n-- tem três degraus no mesmo mês — **Meta**, **Super** e **Hiper**. É premiação\n-- escalonada, e quem porta isso com um único campo \"meta\" perde o modelo\n-- inteiro: some a diferença entre bater o alvo e superá-lo duas vezes, que é\n-- justamente o que a equipe persegue.\n--\n-- Um degrau é um VALOR ALVO, não um percentual: o percentual é o realizado\n-- sobre ele, e se calcula na leitura.\n--\n-- POR QUE OS TRÊS SÃO COLUNAS, e não linhas numa tabela de degraus. São\n-- exatamente três, fixos, e a casa fala deles pelo nome — \"bateu o Super\" é uma\n-- frase que se diz. Uma tabela de degraus arbitrários daria flexibilidade que\n-- ninguém pediu e obrigaria toda leitura a um pivot para desenhar a linha do\n-- mês. Se um dia forem quatro, aí sim vira tabela.\n--\n-- E O REALIZADO NÃO SE GUARDA. Ele é o que o livro já sabe: faturamento\n-- reconhecido no mês. Copiá-lo para cá criaria um segundo lugar onde o\n-- faturamento mora, e dois lugares divergem no primeiro estorno. A vista lê as\n-- faturas, sempre atual.\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_goals (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n -- A meta é SEMPRE de uma unidade: \"a meta da casa\" com três filiais não\n -- quer dizer nada para quem trabalha numa delas.\n unit_id uuid NOT NULL,\n year integer NOT NULL,\n month integer NOT NULL,\n target_amount numeric(14,2) DEFAULT 0 NOT NULL,\n super_amount numeric(14,2) DEFAULT 0 NOT NULL,\n hyper_amount numeric(14,2) DEFAULT 0 NOT NULL,\n notes text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'public.plg_financial_goals'::regclass AND contype = 'p') THEN\n ALTER TABLE ONLY public.plg_financial_goals ADD CONSTRAINT plg_financial_goals_pkey PRIMARY KEY (id);\n END IF;\n -- Um mês tem UMA meta por unidade. Duas seriam duas respostas para \"bati?\".\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_goals_unit_month_key') THEN\n ALTER TABLE ONLY public.plg_financial_goals\n ADD CONSTRAINT plg_financial_goals_unit_month_key UNIQUE (tenant_id, unit_id, year, month);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_goals_month_check') THEN\n ALTER TABLE ONLY public.plg_financial_goals\n ADD CONSTRAINT plg_financial_goals_month_check CHECK (month BETWEEN 1 AND 12);\n END IF;\n -- Os degraus SOBEM. Um Super abaixo da Meta não é um alvo mais difícil, é um\n -- erro de digitação — e um que só apareceria no fim do mês, na hora de pagar\n -- prêmio.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_goals_ladder_check') THEN\n ALTER TABLE ONLY public.plg_financial_goals\n ADD CONSTRAINT plg_financial_goals_ladder_check\n CHECK (super_amount = 0 OR super_amount >= target_amount);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_goals_ladder2_check') THEN\n ALTER TABLE ONLY public.plg_financial_goals\n ADD CONSTRAINT plg_financial_goals_ladder2_check\n CHECK (hyper_amount = 0 OR hyper_amount >= super_amount);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_goals_tenant_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_goals\n ADD CONSTRAINT plg_financial_goals_tenant_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_goals_unit_fk') THEN\n ALTER TABLE ONLY public.plg_financial_goals\n ADD CONSTRAINT plg_financial_goals_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_goals_tenant_year_idx\n ON public.plg_financial_goals USING btree (tenant_id, year, unit_id);\n\nDROP TRIGGER IF EXISTS plg_financial_goals_updated_at ON public.plg_financial_goals;\nCREATE TRIGGER plg_financial_goals_updated_at\n BEFORE UPDATE ON public.plg_financial_goals\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nALTER TABLE public.plg_financial_goals ENABLE ROW LEVEL SECURITY;\nALTER TABLE ONLY public.plg_financial_goals FORCE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_financial_goals_authz_select ON public.plg_financial_goals;\nCREATE POLICY plg_financial_goals_authz_select ON public.plg_financial_goals\n FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(unit_id))\n AND (SELECT app.has_permission('financial.read', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_goals_authz_insert ON public.plg_financial_goals;\nCREATE POLICY plg_financial_goals_authz_insert ON public.plg_financial_goals\n FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(unit_id))\n AND (SELECT app.has_permission('financial.create', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_goals_authz_update ON public.plg_financial_goals;\nCREATE POLICY plg_financial_goals_authz_update ON public.plg_financial_goals\n FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(unit_id))\n AND (SELECT app.has_permission('financial.edit', unit_id)));\n\nDROP POLICY IF EXISTS plg_financial_goals_authz_delete ON public.plg_financial_goals;\nCREATE POLICY plg_financial_goals_authz_delete ON public.plg_financial_goals\n FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_unit(unit_id))\n AND (SELECT app.has_permission('financial.delete', unit_id)));\n\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_financial_goals TO authenticated;\nGRANT ALL ON public.plg_financial_goals TO service_role;\n\n-- Toda tabela \\`plg_financial_*\\` declara a sua authz no template, em modo\n-- \\`enforce\\` — a suíte 004 recusa a que ficar de fora, e recusou esta. O escopo é\n-- \\`unit\\` e não \\`tenant\\`: a meta é sempre de UMA unidade, e quem enxerga uma\n-- filial não tem por que ver a meta da outra.\nINSERT INTO app.scaffold_registry\n (table_schema, table_name, resource_type, plugin, owner_scoping, mode, previous_policies, audit_hook, scope, distributable)\nVALUES\n ('public', 'plg_financial_goals', 'financial.goal', 'financial', false, 'enforce', ARRAY[]::text[], false, 'unit', false)\nON CONFLICT DO NOTHING;\n\nCOMMENT ON TABLE public.plg_financial_goals IS\n 'A meta mensal de faturamento de uma unidade, em três degraus: Meta, Super e Hiper. O realizado NÃO mora aqui — é o que o livro já sabe (025).';\n\n-- ── o ano inteiro, com o realizado ao lado ──────────────────────────────────\n--\n-- Doze linhas SEMPRE, mês com meta ou sem. O V1 mostra o realizado mesmo onde\n-- ninguém cadastrou alvo — foi assim que se viu Agosto com R$ 62.144,00\n-- realizados e os três degraus zerados —, e essa é a informação que faz alguém\n-- cadastrar a meta que falta.\nCREATE OR REPLACE VIEW public.v_financial_goal_year\nWITH (security_invoker = 'true') AS\nWITH meses AS (\n SELECT g.tenant_id, g.unit_id, y.year, m.month\n FROM (SELECT DISTINCT tenant_id, unit_id FROM public.plg_financial_goals) g\n CROSS JOIN generate_series(1, 12) AS m(month)\n CROSS JOIN (SELECT DISTINCT year FROM public.plg_financial_goals) y\n UNION\n -- E as unidades que ainda não têm meta nenhuma também aparecem: uma tela de\n -- metas que só mostra quem já cadastrou não ajuda quem nunca cadastrou.\n SELECT u.tenant_id, u.id, extract(year FROM current_date)::int, m.month\n FROM app.units u CROSS JOIN generate_series(1, 12) AS m(month)\n),\nrealizado AS (\n -- O faturamento reconhecido do mês: fatura a receber, não cancelada, pela\n -- competência. É a mesma base do fechamento (006) — a meta tem de ser medida\n -- com a régua que o livro usa, senão o mês \"bate\" aqui e não bate lá.\n SELECT f.tenant_id, f.unit_id,\n extract(year FROM coalesce(f.competence_date, f.issue_date))::int AS year,\n extract(month FROM coalesce(f.competence_date, f.issue_date))::int AS month,\n sum(f.total)::numeric(14,2) AS total\n FROM public.plg_financial_invoices f\n WHERE f.direction = 'receivable' AND f.cancelled_at IS NULL\n GROUP BY 1, 2, 3, 4\n)\nSELECT ms.tenant_id,\n ms.unit_id,\n ms.year,\n ms.month,\n g.id AS goal_id,\n coalesce(g.target_amount, 0)::numeric(14,2) AS target_amount,\n coalesce(g.super_amount, 0)::numeric(14,2) AS super_amount,\n coalesce(g.hyper_amount, 0)::numeric(14,2) AS hyper_amount,\n coalesce(r.total, 0)::numeric(14,2) AS achieved_amount,\n -- Percentual só onde há alvo. Dividir por zero devolveria NULL e a tela\n -- mostraria um traço onde o V1 mostra 0,00%.\n CASE WHEN coalesce(g.target_amount, 0) > 0\n THEN round(coalesce(r.total, 0) / g.target_amount * 100, 2) ELSE 0 END AS target_percent,\n CASE WHEN coalesce(g.super_amount, 0) > 0\n THEN round(coalesce(r.total, 0) / g.super_amount * 100, 2) ELSE 0 END AS super_percent,\n CASE WHEN coalesce(g.hyper_amount, 0) > 0\n THEN round(coalesce(r.total, 0) / g.hyper_amount * 100, 2) ELSE 0 END AS hyper_percent,\n g.notes\n FROM meses ms\n LEFT JOIN public.plg_financial_goals g\n ON g.tenant_id = ms.tenant_id AND g.unit_id = ms.unit_id\n AND g.year = ms.year AND g.month = ms.month\n LEFT JOIN realizado r\n ON r.tenant_id = ms.tenant_id AND r.unit_id = ms.unit_id\n AND r.year = ms.year AND r.month = ms.month;\n\nCOMMENT ON VIEW public.v_financial_goal_year IS\n 'Os doze meses de uma unidade num ano, com os três degraus e o realizado do livro ao lado. Mês sem meta aparece do mesmo jeito, com o realizado — é o que faz alguém cadastrar a meta que falta (025).';\n\nGRANT SELECT ON public.v_financial_goal_year TO authenticated;\nGRANT SELECT ON public.v_financial_goal_year TO service_role;\n`\n\nexport const MIGRATION_026_QUEM_FECHA_O_CAIXA_NAO_E_QUEM_CONFERE = `-- 026_quem_fecha_o_caixa_nao_e_quem_confere.sql\n--\n-- No V1: \"Recepção abre e fecha; gerência e financeiro conferem. A aba\n-- Conferência é de quem aprova — não de quem opera.\" Cada fechamento pendente\n-- traz duas ações, **Aprovar** e **Rejeitar**.\n--\n-- É separação de funções sobre dinheiro contado à mão, que é o único controle\n-- que sobra quando a conferência é de cédula: quem conta não pode ser quem\n-- assevera que a conta está certa. O V2 tinha a tabela\n-- \\`plg_financial_cash_session_reviewers\\` — com \\`verdict\\`, \\`reviewer_id\\`,\n-- \\`reviewed_at\\` e \\`notes\\` — e NENHUMA linha de código a usando. Faltava o\n-- verbo.\n--\n-- (A tela \"Conferentes de Caixa\" do V1 não se porta: ela quebra o módulo\n-- inteiro por um import que falta, e está documentada como stub. O que se porta\n-- é a CONFERÊNCIA, que é a aba de Caixas — o conceito, não o defeito.)\n\nCREATE OR REPLACE FUNCTION public.financial_cash_session_review(\n p_session_id uuid,\n p_verdict text,\n p_notes text DEFAULT NULL\n) RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_session record;\n v_review uuid;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_cash_session_review: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF p_verdict NOT IN ('approved', 'rejected') THEN\n RAISE EXCEPTION 'financial_cash_session_review: veredito inválido (%)', p_verdict USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO v_session\n FROM public.plg_financial_cash_register_sessions\n WHERE id = p_session_id AND tenant_id = v_tenant;\n\n IF v_session.id IS NULL THEN\n RAISE EXCEPTION 'financial_cash_session_review: sessão não encontrada' USING ERRCODE = 'P0002';\n END IF;\n\n -- Conferir é sobre o que já foi contado: uma sessão ABERTA ainda vai receber\n -- dinheiro, e aprovar o que ainda se move não quer dizer nada.\n IF v_session.status <> 'closed' THEN\n RAISE EXCEPTION 'financial_cash_session_review: só sessão fechada se confere (status atual: %)', v_session.status\n USING ERRCODE = '22023';\n END IF;\n\n -- \\`financial.manage\\` na unidade da sessão, e não \\`financial.read\\`: conferir é\n -- assinar embaixo. Quem só enxerga o caixa não o aprova.\n IF NOT app.has_permission('financial.manage', v_session.unit_id) THEN\n RAISE EXCEPTION 'financial_cash_session_review: exige financial.manage na unidade' USING ERRCODE = '42501';\n END IF;\n\n -- QUEM FECHOU NÃO CONFERE. É a regra inteira desta migration: sem isto, a\n -- separação de funções vira um botão que o próprio operador aperta.\n IF v_session.closed_by_user_id IS NOT NULL AND v_session.closed_by_user_id = auth.uid() THEN\n RAISE EXCEPTION 'financial_cash_session_review: quem fechou o caixa não pode conferi-lo'\n USING ERRCODE = '42501';\n END IF;\n\n INSERT INTO public.plg_financial_cash_session_reviewers\n (tenant_id, unit_id, session_id, reviewer_id, verdict, notes, reviewed_at)\n VALUES (v_tenant, v_session.unit_id, p_session_id, auth.uid(), p_verdict, p_notes, now())\n RETURNING id INTO v_review;\n\n RETURN jsonb_build_object('review_id', v_review, 'session_id', p_session_id, 'verdict', p_verdict);\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_cash_session_review(uuid, text, text) IS\n 'Aprova ou rejeita o fechamento de um caixa. Só sessão fechada, só com financial.manage na unidade, e NUNCA por quem fechou — conferir é assinar embaixo (026).';\n\nREVOKE ALL ON FUNCTION public.financial_cash_session_review(uuid, text, text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_cash_session_review(uuid, text, text) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_cash_session_review(uuid, text, text) TO service_role;\n\n-- ── a fila de conferência ───────────────────────────────────────────────────\n--\n-- O que espera aprovação é o que fechou e ainda não tem veredito. O V1 põe um\n-- contador nesta aba (20, na verificação) porque é uma FILA: enquanto houver\n-- número ali, alguém tem trabalho a fazer.\nCREATE OR REPLACE VIEW public.v_financial_cash_session_review\nWITH (security_invoker = 'true') AS\nSELECT s.id AS session_id,\n s.tenant_id,\n s.unit_id,\n s.bank_account_id,\n s.opened_at,\n s.closed_at,\n s.opened_by_name,\n s.closed_by_name,\n s.closed_by_user_id,\n s.opening_balance,\n s.closing_balance,\n s.difference,\n r.id AS review_id,\n r.verdict,\n r.reviewed_at,\n r.notes AS review_notes,\n p.name AS reviewer_name,\n -- \\`pending\\` é o estado que a fila mostra; os outros são história.\n coalesce(r.verdict, 'pending') AS review_status\n FROM public.plg_financial_cash_register_sessions s\n LEFT JOIN LATERAL (\n -- O ÚLTIMO veredito vale. Rejeitar e depois aprovar é um caminho real —\n -- a gaveta foi recontada — e a fila não pode ficar presa no primeiro \"não\".\n SELECT r2.* FROM public.plg_financial_cash_session_reviewers r2\n WHERE r2.session_id = s.id\n ORDER BY r2.reviewed_at DESC NULLS LAST, r2.created_at DESC\n LIMIT 1\n ) r ON true\n LEFT JOIN public.people p ON p.id = r.reviewer_id\n WHERE s.status = 'closed';\n\nCOMMENT ON VIEW public.v_financial_cash_session_review IS\n 'Os fechamentos de caixa e o seu veredito. Sem veredito é \\`pending\\` — a fila de quem confere (026).';\n\nGRANT SELECT ON public.v_financial_cash_session_review TO authenticated;\nGRANT SELECT ON public.v_financial_cash_session_review TO service_role;\n`\n\nexport const MIGRATION_027_O_VALE_PRESENTE_NASCE_EM_LOTE_E_E_UM_PASSIVO = `-- 027_o_vale_presente_nasce_em_lote_e_e_um_passivo.sql\n--\n-- Um vale-presente é a única coisa que o Financeiro vende antes de existir: a\n-- casa recebe hoje por um serviço que vai prestar depois. Isso não é receita, é\n-- PASSIVO — e é por isso que a tela mora no Financeiro e não na loja.\n--\n-- O CARTÃO JÁ EXISTE, e mora na loja: \\`plg_shop_gift_cards\\` guarda código,\n-- valor de face, saldo, validade e status desde o baseline do @fayz-ai/shop.\n-- Esta migration não cria tabela nenhuma e não copia uma linha: o Financeiro\n-- LÊ a tabela da loja e escreve nela por uma porta própria. Duplicar o cartão\n-- daria dois lugares onde o saldo mora, e dois lugares divergem no primeiro\n-- resgate.\n--\n-- O QUE FALTAVA ERA A EMISSÃO EM LOTE. O provider da loja cria um cartão por\n-- vez, e não é assim que se emite vale-presente: emite-se um LOTE uniforme —\n-- quantidade × valor unitário — e manda-se imprimir. Vinte cartões de R$ 100\n-- são vinte chamadas no shop e uma só aqui, com o total do lote conferido antes\n-- de gravar.\n--\n-- A LOJA PODE NÃO ESTAR INSTALADA. \\`packages/shop\\` é opcional na corrente e o\n-- Financeiro é aplicado ANTES dele — a tabela não existe nem no momento em que\n-- este arquivo roda. É a mesma armadilha da 015: uma vista criada em duas\n-- formas conforme a tabela existisse APLICA e MENTE, porque o dia em que a loja\n-- chegasse não a corrigiria. Então o nome se resolve em TEMPO DE EXECUÇÃO, aqui\n-- também: sem loja as funções devolvem \"não instalado\" e a tela se explica; com\n-- loja, as mesmas funções passam a ler, sem republicar nada.\n--\n-- VALOR NÃO É SALDO, e o traço não é zero. Valor é o de face, gravado na\n-- emissão e imutável. Saldo é o que resta — e um cartão que nunca foi ativado\n-- NÃO TEM saldo: tem traço. Zero é um cartão gasto, que é outra história e\n-- outra conversa com o cliente. A tabela da loja não sabe dizer a diferença\n-- (\\`balance\\` é NOT NULL), então quem lê é que a diz: \\`balance_known\\`.\n\n-- ── a loja está aí? ─────────────────────────────────────────────────────────\n--\n-- Uma pergunta e não uma exceção: a tela precisa dizer \"instale a loja\" em vez\n-- de mostrar um erro de SQL a quem nunca ouviu falar de \\`plg_shop_gift_cards\\`.\nCREATE OR REPLACE FUNCTION public.financial_gift_cards_enabled()\n RETURNS boolean\n LANGUAGE sql\n STABLE\n SET search_path TO ''\nAS $$ SELECT to_regclass('public.plg_shop_gift_cards') IS NOT NULL $$;\n\nCOMMENT ON FUNCTION public.financial_gift_cards_enabled() IS\n 'A loja que guarda os vales-presente está instalada? Sem ela a tela do Financeiro se explica em vez de quebrar (027).';\n\n-- ── a lista ─────────────────────────────────────────────────────────────────\n--\n-- SECURITY DEFINER porque a tabela é de OUTRO plugin e a política dela é a da\n-- loja; quem entra por esta porta é filtrado aqui, pelo tenant da sessão e pela\n-- permissão do Financeiro. Sem isso, ligar o Financeiro daria de brinde a\n-- leitura da loja a quem só tem \\`financial.read\\` — e o contrário também.\nCREATE OR REPLACE FUNCTION public.financial_gift_cards(\n p_search text DEFAULT NULL,\n p_state text DEFAULT NULL,\n p_min numeric DEFAULT NULL,\n p_max numeric DEFAULT NULL,\n p_from date DEFAULT NULL,\n p_to date DEFAULT NULL,\n p_limit integer DEFAULT 500\n) RETURNS TABLE (\n id uuid,\n code text,\n face_value numeric,\n balance numeric,\n balance_known boolean,\n state text,\n currency text,\n issued_at timestamptz,\n expires_at timestamptz,\n activated_at timestamptz,\n note text,\n customer_email text\n)\n LANGUAGE plpgsql\n STABLE\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_activated text;\n v_sql text;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_gift_cards: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.read', NULL) THEN\n RAISE EXCEPTION 'financial_gift_cards: exige financial.read' USING ERRCODE = '42501';\n END IF;\n IF to_regclass('public.plg_shop_gift_cards') IS NULL THEN\n RETURN; -- sem loja, nenhuma linha: quem chama já perguntou pelo _enabled\n END IF;\n\n -- QUANDO O CARTÃO FOI ATIVADO. A tabela da loja não tem essa coluna, e criá-la\n -- daqui não é opção: esta corrente roda antes da dela, então o ALTER cairia no\n -- vazio e nunca mais voltaria. O melhor que a linha sabe dizer é a última vez\n -- que mudou — aproximação, e assumida. No dia em que a loja ganhar\n -- \\`activated_at\\` de verdade, esta mesma função passa a lê-la sem republicar\n -- nada, que é o motivo de a expressão ser resolvida aqui e não estar fixa.\n IF EXISTS (SELECT 1 FROM information_schema.columns c\n WHERE c.table_schema = 'public' AND c.table_name = 'plg_shop_gift_cards'\n AND c.column_name = 'activated_at') THEN\n v_activated := 'g.activated_at';\n ELSE\n v_activated :=\n 'CASE WHEN g.status = ''disabled'' AND g.balance = 0 THEN NULL'\n || ' WHEN g.updated_at > g.created_at THEN g.updated_at'\n || ' ELSE g.created_at END';\n END IF;\n\n v_sql :=\n 'SELECT * FROM ('\n || 'SELECT r.id, r.code, r.initial_balance,'\n -- O traço vira NULL aqui, não na tela: assim quem quer que leia esta função\n -- — relatório, agente, planilha — recebe a mesma distinção.\n || 'CASE WHEN r.never THEN NULL ELSE r.balance END,'\n || '(NOT r.never),'\n -- O estado que a casa fala, e não o enum da loja. \\`issued\\` é o cartão\n -- impresso e ainda não vendido: o lote do V1 inteiro estava assim.\n || 'CASE WHEN r.never THEN ''issued'''\n || ' WHEN r.balance = 0 OR r.status = ''redeemed'' THEN ''redeemed'''\n || ' WHEN r.expires_at IS NOT NULL AND r.expires_at < now() THEN ''expired'''\n || ' WHEN r.status = ''disabled'' THEN ''revoked'''\n || ' ELSE ''active'' END,'\n || 'r.currency, r.created_at, r.expires_at, r.activated_at, r.note, r.customer_email '\n || 'FROM (SELECT g.id, g.code, g.initial_balance, g.balance, g.currency, g.status,'\n || 'g.created_at, g.expires_at, g.note, g.customer_email,'\n -- Emitido e nunca ativado: saldo zerado E desabilitado. É exatamente o que a\n -- emissão abaixo grava, e a loja nunca cria essa combinação (ela nasce ativa\n -- com o saldo cheio) — então a distinção não depende de adivinhação.\n || '(g.status = ''disabled'' AND g.balance = 0) AS never,'\n || v_activated || ' AS activated_at '\n || 'FROM public.plg_shop_gift_cards g '\n || 'WHERE g.tenant_id = $1'\n || ' AND ($2::text IS NULL OR g.code ILIKE ''%'' || $2 || ''%'')'\n || ' AND ($3::numeric IS NULL OR g.initial_balance >= $3)'\n || ' AND ($4::numeric IS NULL OR g.initial_balance <= $4)'\n || ' AND ($5::date IS NULL OR g.created_at::date >= $5)'\n || ' AND ($6::date IS NULL OR g.created_at::date <= $6)) r'\n || ') s(id, code, face_value, balance, balance_known, state, currency,'\n || ' issued_at, expires_at, activated_at, note, customer_email) '\n || 'WHERE ($7::text IS NULL OR s.state = $7) '\n || 'ORDER BY s.issued_at DESC, s.code '\n || 'LIMIT $8';\n\n RETURN QUERY EXECUTE v_sql\n USING v_tenant, p_search, p_min, p_max, p_from, p_to, p_state,\n greatest(1, least(coalesce(p_limit, 500), 2000));\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_gift_cards(text, text, numeric, numeric, date, date, integer) IS\n 'Os vales-presente do tenant, lidos da loja pela porta do Financeiro. Devolve valor de face e saldo como coisas SEPARADAS: saldo NULL é \"nunca ativado\", zero é \"gasto\" (027).';\n\n-- ── a emissão em lote ───────────────────────────────────────────────────────\n--\n-- Um lote é uniforme: uma quantidade e UM valor unitário. Não existe emitir\n-- cartões de valores diferentes numa tacada — quem precisa disso emite dois\n-- lotes, e os dois totais ficam separados, que é como o dinheiro entrou.\n--\n-- O cartão nasce DESABILITADO com saldo zero: impresso, ainda não vendido.\n-- Ninguém recebeu nada por ele, então ele ainda não é passivo — vira, no dia em\n-- que for ativado. Emitir e ativar na mesma tacada faria o balanço da casa\n-- crescer por causa de uma impressão.\nCREATE OR REPLACE FUNCTION public.financial_issue_gift_cards(\n p_quantity integer,\n p_amount numeric,\n p_expires_at timestamptz DEFAULT NULL,\n p_note text DEFAULT NULL,\n p_currency text DEFAULT 'BRL'\n) RETURNS TABLE (code text)\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_code text;\n v_out text;\n i integer;\n a integer;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_issue_gift_cards: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.create', NULL) THEN\n RAISE EXCEPTION 'financial_issue_gift_cards: exige financial.create' USING ERRCODE = '42501';\n END IF;\n IF to_regclass('public.plg_shop_gift_cards') IS NULL THEN\n RAISE EXCEPTION 'financial_issue_gift_cards: os vales-presente vivem na loja, e ela não está instalada neste banco'\n USING ERRCODE = '0A000';\n END IF;\n IF coalesce(p_quantity, 0) < 1 OR p_quantity > 500 THEN\n RAISE EXCEPTION 'financial_issue_gift_cards: quantidade fora de 1..500 (%)', p_quantity\n USING ERRCODE = '22023';\n END IF;\n -- Um vale de zero não é uma cortesia, é um cartão que não paga nada — e o\n -- cliente descobre isso no caixa.\n IF coalesce(p_amount, 0) <= 0 THEN\n RAISE EXCEPTION 'financial_issue_gift_cards: o valor unitário tem de ser maior que zero'\n USING ERRCODE = '22023';\n END IF;\n -- Uma validade que já passou emite um lote nascido vencido.\n IF p_expires_at IS NOT NULL AND p_expires_at <= now() THEN\n RAISE EXCEPTION 'financial_issue_gift_cards: a validade já passou' USING ERRCODE = '22023';\n END IF;\n\n FOR i IN 1..p_quantity LOOP\n v_out := NULL;\n -- Dezesseis dígitos, como cartão. O índice único da loja é (tenant, upper(code)),\n -- então a colisão é recusada pelo banco e não por uma checagem que corre\n -- antes — e o retry é o que resolve, não um SELECT anterior que mente sob\n -- concorrência.\n FOR a IN 1..25 LOOP\n v_code := lpad((floor(random() * 100000000))::bigint::text, 8, '0')\n || lpad((floor(random() * 100000000))::bigint::text, 8, '0');\n EXECUTE\n 'INSERT INTO public.plg_shop_gift_cards'\n || ' (tenant_id, code, initial_balance, balance, currency, status, expires_at, note)'\n || ' VALUES ($1, $2, $3, 0, $4, ''disabled'', $5, $6)'\n || ' ON CONFLICT DO NOTHING RETURNING code'\n INTO v_out\n USING v_tenant, v_code, p_amount, coalesce(p_currency, 'BRL'), p_expires_at, p_note;\n EXIT WHEN v_out IS NOT NULL;\n END LOOP;\n IF v_out IS NULL THEN\n RAISE EXCEPTION 'financial_issue_gift_cards: não consegui um código livre em 25 tentativas'\n USING ERRCODE = '23505';\n END IF;\n code := v_out;\n RETURN NEXT;\n END LOOP;\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_issue_gift_cards(integer, numeric, timestamptz, text, text) IS\n 'Emite um LOTE uniforme de vales-presente na tabela da loja: quantidade × valor unitário, validade opcional. Nascem impressos e não ativados — passivo só quando vendidos (027).';\n\n-- ── a ativação ──────────────────────────────────────────────────────────────\n--\n-- Aqui é onde o dinheiro entra e o passivo nasce: o cartão é vendido, o saldo\n-- passa a valer o valor de face, e a partir de agora a casa DEVE aquele\n-- atendimento. É também o único momento em que \"saldo\" deixa de ser um traço.\nCREATE OR REPLACE FUNCTION public.financial_activate_gift_card(p_id uuid)\n RETURNS void\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_n integer;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_activate_gift_card: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.edit', NULL) THEN\n RAISE EXCEPTION 'financial_activate_gift_card: exige financial.edit' USING ERRCODE = '42501';\n END IF;\n IF to_regclass('public.plg_shop_gift_cards') IS NULL THEN\n RAISE EXCEPTION 'financial_activate_gift_card: a loja não está instalada neste banco'\n USING ERRCODE = '0A000';\n END IF;\n\n -- Só ativa o que ainda não foi ativado. Reativar um cartão em uso repõe o\n -- saldo gasto — seria imprimir dinheiro por um clique repetido.\n EXECUTE\n 'UPDATE public.plg_shop_gift_cards'\n || ' SET status = ''active'', balance = initial_balance, updated_at = now()'\n || ' WHERE id = $1 AND tenant_id = $2 AND status = ''disabled'' AND balance = 0'\n USING p_id, v_tenant;\n GET DIAGNOSTICS v_n = ROW_COUNT;\n IF v_n = 0 THEN\n RAISE EXCEPTION 'financial_activate_gift_card: cartão inexistente ou já ativado'\n USING ERRCODE = '22023';\n END IF;\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_activate_gift_card(uuid) IS\n 'Vende o vale impresso: saldo passa a valer o valor de face e o passivo nasce. Recusa reativar um cartão em uso — repor saldo gasto é imprimir dinheiro (027).';\n\nREVOKE ALL ON FUNCTION public.financial_gift_cards_enabled() FROM PUBLIC;\nREVOKE ALL ON FUNCTION public.financial_gift_cards(text, text, numeric, numeric, date, date, integer) FROM PUBLIC;\nREVOKE ALL ON FUNCTION public.financial_issue_gift_cards(integer, numeric, timestamptz, text, text) FROM PUBLIC;\nREVOKE ALL ON FUNCTION public.financial_activate_gift_card(uuid) FROM PUBLIC;\n\nGRANT EXECUTE ON FUNCTION public.financial_gift_cards_enabled() TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_gift_cards(text, text, numeric, numeric, date, date, integer) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_issue_gift_cards(integer, numeric, timestamptz, text, text) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_activate_gift_card(uuid) TO authenticated;\n\nGRANT EXECUTE ON FUNCTION public.financial_gift_cards_enabled() TO service_role;\nGRANT EXECUTE ON FUNCTION public.financial_gift_cards(text, text, numeric, numeric, date, date, integer) TO service_role;\nGRANT EXECUTE ON FUNCTION public.financial_issue_gift_cards(integer, numeric, timestamptz, text, text) TO service_role;\nGRANT EXECUTE ON FUNCTION public.financial_activate_gift_card(uuid) TO service_role;\n`\n\nexport const MIGRATION_028_O_CARTAO_TEM_COR = `-- 028_o_cartao_tem_cor.sql\n--\n-- Quem tem quatro cartões não os distingue pelo nome que digitou seis meses\n-- atrás: distingue pela COR e pelos últimos dígitos, que é como o cartão está\n-- na carteira. Uma tela de despesa que só diz \"Cartão Empresarial\" obriga a\n-- pessoa a lembrar qual é — e a errar de vez em quando, o que no fim do mês é\n-- uma fatura conciliada contra o cartão errado.\n--\n-- A cor é do CADASTRO da conta, não derivada do id: derivar dá cores estáveis e\n-- sem sentido (o roxo do Nubank não sai de um hash), e o dono quer poder pintar\n-- o cartão como ele é de verdade.\n--\n-- \\`color\\` é texto livre e não um enum: é um valor de CSS que a tela pinta. Um\n-- enum aqui obrigaria uma migration por cor nova, e a paleta é escolha de quem\n-- desenha, não do banco. NULL é resposta legítima — cartão sem cor é cartão\n-- sem cor, e a tela usa o cinza padrão em vez de inventar uma.\nALTER TABLE public.plg_financial_accounts\n ADD COLUMN IF NOT EXISTS color text;\n\nCOMMENT ON COLUMN public.plg_financial_accounts.color IS\n 'A cor com que o cartão aparece na tela — é assim que quem tem quatro cartões sabe qual é. Texto (valor CSS), nulo quando não escolheram (028).';\n`\n\nexport const MIGRATION_029_THE_KIOSK_SALE_IS_A_SALE_TOO = `-- ---------------------------------------------------------------------------\n-- 029_the_kiosk_sale_is_a_sale_too.sql\n--\n-- Um pedido fechado levanta seu recebível — não só o do salão e o da agenda.\n--\n-- O gate da 001 nomeava só \\`dine_in\\`; a 005 acrescentou \\`appointment\\` e\n-- \\`service_order\\`. Nenhuma das três cobre o que o quiosque e a entrega fazem:\n-- um \\`takeout\\` fechado no totem ia para \\`completed\\` sem nada atrás dele —\n-- dinheiro cobrado na maquininha e nenhuma conta a receber nos livros. O mesmo\n-- valia para \\`delivery\\` e para a venda de balcão do PDV.\n--\n-- ESTA MIGRATION É UMA UNIÃO, NÃO UMA TROCA. Ela nasceu numa branch que só\n-- conhecia o gate da 001 e, escrita como estava, teria removido \\`appointment\\`\n-- e \\`service_order\\` — desfazendo a 005 sem ruído, porque o gatilho engole a\n-- falha num WARNING e o pedido fecha calado. As seis espécies convivem: as\n-- três da 001/005 mais \\`takeout\\`, \\`delivery\\` e \\`sale\\`.\n--\n-- SOBRE \\`sale\\`, que a 005 recusou de propósito (\"a loja fatura no checkout,\n-- pela extensão dela\"): a recusa continua valendo para a loja e nada muda para\n-- ela. O pedido de loja NASCE fechado, e este gatilho é \\`AFTER UPDATE OF\n-- status\\` — um pedido que já entra \\`completed\\` nunca o dispara. Quem \\`sale\\`\n-- alcança aqui é a venda de balcão do PDV, que abre e depois fecha. E se as\n-- duas portas se cruzarem, \\`order_raise_receivable\\` devolve a fatura que já\n-- existe em vez de emitir a segunda.\n--\n-- A lista sai daqui e vira UMA função, porque ela tem dois leitores que não\n-- podem discordar: este gatilho, que decide quando levantar, e\n-- \\`v_orders_sem_invoice\\` (030), que audita quem fechou sem levantar. Se os dois\n-- guardassem a própria cópia, a que audita poderia ficar cega justamente para a\n-- espécie que a outra esqueceu — e foi exatamente essa cegueira que deixou o\n-- buraco do quiosque passar despercebido.\n--\n-- CUIDADO ao editar \\`order_raise_receivable\\` (010): recriar este gatilho junto\n-- com ela desfaz o alargamento, e a falha não aparece em lugar nenhum. Esse\n-- silêncio é de propósito (ver 001: o garçom tem de conseguir fechar a mesa), e\n-- só é seguro porque a 030 enxerga o estrago depois.\n--\n-- Idempotente e replay-safe.\n-- ---------------------------------------------------------------------------\n\n-- IMMUTABLE: é uma constante com nome. O planejador dobra a chamada no plano,\n-- então usá-la no WHERE de uma view não custa nada por linha.\nCREATE OR REPLACE FUNCTION public.order_selling_kinds()\nRETURNS text[]\nLANGUAGE sql\nIMMUTABLE\nPARALLEL SAFE\nAS $function$\n SELECT ARRAY['dine_in', 'takeout', 'delivery', 'sale', 'appointment', 'service_order']::text[];\n$function$;\n\nCOMMENT ON FUNCTION public.order_selling_kinds() IS\n 'As especies de pedido cuja CONCLUSAO levanta o recebivel (gatilho orders_raise_receivable_on_close, 001/005/029) e cuja conclusao sem levantar e um defeito (v_orders_sem_invoice, 030). Uma lista so, para que o gatilho e a auditoria nao possam discordar. \\`sale\\` esta dentro pela venda de balcao do PDV; o pedido de loja nasce fechado e nunca dispara um AFTER UPDATE.';\n\nREVOKE ALL ON FUNCTION public.order_selling_kinds() FROM public, anon;\nGRANT EXECUTE ON FUNCTION public.order_selling_kinds() TO authenticated, service_role;\n\nCREATE OR REPLACE FUNCTION public.trg_order_raise_receivable_on_close()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = ''\nAS $function$\nBEGIN\n IF NEW.kind = ANY (public.order_selling_kinds())\n AND NEW.status = 'completed'\n AND coalesce(OLD.status, '') IS DISTINCT FROM 'completed' THEN\n -- Nunca deixa o fechamento falhar por causa da escrituração (001).\n BEGIN\n PERFORM public.order_raise_receivable(NEW.id);\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'order_raise_receivable falhou para o pedido %: %', NEW.id, SQLERRM;\n END;\n END IF;\n RETURN NEW;\nEND $function$;\n\nCOMMENT ON FUNCTION public.trg_order_raise_receivable_on_close() IS\n 'A comanda, o atendimento, a ordem de servico, o balcao, o quiosque e a entrega levantam o recebivel ao concluir (001, 005, 029). A lista vive em order_selling_kinds().';\n\nDROP TRIGGER IF EXISTS orders_raise_receivable_on_close ON public.orders;\nCREATE TRIGGER orders_raise_receivable_on_close\n AFTER UPDATE OF status ON public.orders\n FOR EACH ROW EXECUTE FUNCTION public.trg_order_raise_receivable_on_close();\n\n-- ── o que já fechou pelas espécies novas e nunca foi faturado ──────────────\n--\n-- Só as três que este arquivo acrescenta: a 005 já varreu \\`appointment\\` e\n-- \\`service_order\\`, e \\`dine_in\\` nunca esteve fora do gate.\nDO $$\nDECLARE r record; v_n integer := 0;\nBEGIN\n FOR r IN\n SELECT o.id FROM public.orders o\n WHERE o.kind IN ('takeout', 'delivery', 'sale')\n AND o.status = 'completed'\n AND coalesce(o.total, 0) > 0\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_invoices f\n WHERE f.order_id = o.id AND f.cancelled_at IS NULL)\n LOOP\n BEGIN\n PERFORM public.order_raise_receivable(r.id);\n v_n := v_n + 1;\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'backfill 029: order_raise_receivable falhou para %: %', r.id, SQLERRM;\n END;\n END LOOP;\n IF v_n > 0 THEN\n RAISE NOTICE '029: % venda(s) de balcao/quiosque/entrega faturadas com atraso', v_n;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_030_THE_CENSUS_CAN_SEE_THE_KIOSK = `-- ---------------------------------------------------------------------------\n-- 030_the_census_can_see_the_kiosk.sql\n--\n-- O censo enxerga as espécies que realmente vendem.\n--\n-- \\`v_orders_sem_invoice\\` responde \"quais pedidos fecharam sem levantar\n-- recebível\". A lista dela era \\`sale, purchase, course\\` — e essas três não\n-- incluem NENHUMA das que o caixa, o quiosque e a entrega usam de verdade.\n-- Então a view voltava zero linha para um pool com o defeito dentro, e zero\n-- linha lê como \"tudo certo\", não como \"não estou olhando\".\n--\n-- Isso importa mais que um furo de relatório por causa da escolha embutida no\n-- gatilho da 001: ele engole a falha num RAISE WARNING de propósito, para que a\n-- conta sempre feche com o cliente na frente. Essa escolha está certa — recusar\n-- o fechamento prenderia o cliente no salão —, mas ela só é SEGURA se alguém\n-- rio abaixo tornar o silêncio visível. Esta view é esse alguém, e era cega\n-- exatamente para o caso que aconteceu: um \\`takeout\\` do totem que fechou sem\n-- fatura porque o gate ainda nomeava só \\`dine_in\\`.\n--\n-- A lista de espécies vem de \\`public.order_selling_kinds()\\` (029), a MESMA que\n-- o gatilho consulta para decidir quando levantar. É o ponto todo: quem audita\n-- não pode ter uma cópia própria da regra que audita. Com a união da 029 ela\n-- carrega também \\`appointment\\` e \\`service_order\\` — e é o que se quer: a 005\n-- passou a faturar os dois ao concluir e fez o backfill, então um atendimento\n-- concluído sem fatura deixou de ser legítimo e passou a ser exatamente o tipo\n-- de silêncio que esta view existe para mostrar.\n--\n-- \\`purchase\\` e \\`course\\` continuam: são documentos que também deveriam ter\n-- fatura, só não passam pelo gatilho de fechamento. A view alarga, não troca.\n-- ---------------------------------------------------------------------------\n\nCREATE OR REPLACE VIEW public.v_orders_sem_invoice\nWITH (security_invoker = true) AS\nSELECT o.id AS order_id, o.tenant_id, o.unit_id, o.kind, o.channel, o.status,\n o.total, o.created_at\n FROM public.orders o\n WHERE (o.kind = ANY (public.order_selling_kinds()) OR o.kind IN ('purchase', 'course'))\n -- Um pedido ainda aberto não é buraco: ele é faturado quando fecha. O que\n -- não é legítimo é o que CONCLUIU e não levantou nada — a mesma distinção\n -- que o PRD 06 faz para o atendimento.\n AND o.status IN ('completed', 'invoiced', 'paid')\n AND NOT EXISTS (\n SELECT 1 FROM public.plg_financial_invoices f\n WHERE f.order_id = o.id AND f.cancelled_at IS NULL);\n\nCOMMENT ON VIEW public.v_orders_sem_invoice IS\n 'Pedidos que FECHARAM e nao produziram fatura (aceite do PRD 02/04). Vazio e a barra. As especies vem de order_selling_kinds(), a mesma lista que o gatilho de fechamento consulta, mais purchase e course. Um pedido ainda aberto fica de fora: ele e faturado quando fecha.';\n\nGRANT SELECT ON public.v_orders_sem_invoice TO authenticated, service_role;\n`\n\nexport const MIGRATION_031_THE_BANK_LINE_IS_EVIDENCE_NOT_A_SECOND_PAYMENT = `-- A bank statement line is evidence of cash movement until reconciliation.\n-- If matched, the app-native payment remains the sole economic event. If the\n-- user accepts the line without a match, that line becomes the economic event.\nCREATE OR REPLACE VIEW public.v_financial_cash_movements\nWITH (security_invoker = true) AS\nSELECT movement.*\n FROM public.plg_financial_movements movement\n LEFT JOIN public.plg_financial_movements reversed\n ON reversed.id = movement.reverses_id\n WHERE CASE\n -- A reversal has exactly the economic nature of the row it reverses. This\n -- keeps provider corrections neutral both for matched evidence and for an\n -- imported line that had previously been accepted as standalone.\n WHEN reversed.id IS NOT NULL THEN\n reversed.external_source IS NULL\n OR (reversed.reconciled_at IS NOT NULL AND reversed.matched_movement_id IS NULL)\n ELSE\n movement.external_source IS NULL\n OR (movement.reconciled_at IS NOT NULL AND movement.matched_movement_id IS NULL)\n END;\n\nCOMMENT ON VIEW public.v_financial_cash_movements IS\n 'Economic cash events. Native movements always count; imported statement lines count only after standalone acceptance, never when matched to a native movement. Reversals inherit the economic nature of the row they reverse.';\n\nGRANT SELECT ON public.v_financial_cash_movements TO authenticated, service_role;\n\nCREATE OR REPLACE VIEW public.v_financial_account_balances\nWITH (security_invoker = true) AS\nWITH debits AS (\n SELECT movement.debit_account_id AS account_id,\n sum(movement.amount) AS total,\n count(*) AS n\n FROM public.v_financial_cash_movements movement\n WHERE movement.status <> 'cancelled'\n AND movement.debit_account_id IS NOT NULL\n AND movement.movement_kind IN ('payment', 'transfer')\n GROUP BY movement.debit_account_id\n), credits AS (\n SELECT movement.credit_account_id AS account_id,\n sum(movement.amount) AS total,\n count(*) AS n\n FROM public.v_financial_cash_movements movement\n WHERE movement.status <> 'cancelled'\n AND movement.credit_account_id IS NOT NULL\n AND movement.movement_kind IN ('payment', 'transfer')\n GROUP BY movement.credit_account_id\n)\nSELECT account.id AS account_id,\n account.tenant_id,\n account.unit_id,\n account.kind,\n account.code,\n account.name,\n account.person_id,\n account.is_active,\n account.account_type,\n account.bank_name,\n account.agency_number,\n account.account_number,\n account.metadata,\n coalesce(debits.total, 0)::numeric(14,2) AS debit_total,\n coalesce(credits.total, 0)::numeric(14,2) AS credit_total,\n (coalesce(credits.total, 0) - coalesce(debits.total, 0))::numeric(14,2) AS net_credit,\n CASE\n WHEN account.kind IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit', 'card_receivable_debit')\n THEN (coalesce(debits.total, 0) - coalesce(credits.total, 0))::numeric(14,2)\n ELSE (coalesce(credits.total, 0) - coalesce(debits.total, 0))::numeric(14,2)\n END AS balance,\n (coalesce(debits.n, 0) + coalesce(credits.n, 0))::integer AS movement_count,\n account.created_at,\n account.updated_at\n FROM public.plg_financial_accounts account\n LEFT JOIN debits ON debits.account_id = account.id\n LEFT JOIN credits ON credits.account_id = account.id;\n\nCOMMENT ON VIEW public.v_financial_account_balances IS\n 'Balances from economic cash events; matched bank evidence never duplicates its app-native payment.';\n\nGRANT SELECT ON public.v_financial_account_balances TO authenticated, service_role;\n`\n\nexport const MIGRATION_032_CONTAS_A_PAGAR_GANHAM_ONDE_MORAR = `-- Contas a pagar ganham onde morar.\n--\n-- \\`recurring_bills\\`, \\`recurring_bill_items\\` e \\`recurring_bill_occurrences\\` do V1\n-- estavam em \\`migration.excluded_tables\\` com este motivo: *\"no destination\n-- contract … plg_financial_recurring_bills nunca foi construída (#188)\"*.\n--\n-- **A nota está vencida.** \\`public.plg_financial_recurring_bills\\` existe, com\n-- forma própria e diferente da do V1 — e é essa forma que manda:\n--\n-- V1 V2\n-- recurring_bills plg_financial_recurring_bills\n-- recurring_bill_items (tabela) .items (jsonb na própria conta)\n-- recurring_bill_occurrences (tabela) — não existe\n-- interval_unit + interval_count frequency (weekly|monthly|quarterly|yearly)\n-- first_due_date starts_on + due_day (1..31)\n-- account_central_id → contacts counterparty_person_id → public.people\n-- (soma dos itens) amount NOT NULL, CHECK (amount > 0)\n-- (primeira descrição de item) description NOT NULL\n--\n-- Este arquivo faz três coisas, e só a terceira cria tabela:\n--\n-- 1. escreve os writers que levam a conta e os itens para a forma do V2\n-- 2. escreve o de-para e tira as três tabelas da lista de adiadas\n-- 3. cria \\`plg_financial_recurring_bill_occurrences\\`, que é a única lacuna\n-- DE VERDADE\n--\n-- ── Por que a ocorrência precisa existir ─────────────────────────────────────\n--\n-- O V2 deriva os vencimentos de \\`frequency\\` + \\`due_day\\`: a conta é a regra, e o\n-- calendário sai dela. Isso funciona enquanto ninguém mexe num vencimento — e o\n-- V1 deixa mexer. \\`recurring_bill_occurrences.overridden\\` marca o vencimento que\n-- alguém editou à mão (mudou o valor de março sem mexer na regra), e\n-- \\`paid_value\\` guarda o que de fato foi pago. Nenhum dos dois é derivável: a\n-- fórmula devolve o valor da regra, não o que aconteceu.\n--\n-- Medido na origem em 18/09/2026 (licença 8, Pertinho do Céu): 35 contas, 35\n-- itens, 833 ocorrências, R$ 1.937.827,67 entre 25/05/2026 e 02/09/2028, 111 já\n-- consolidadas. Descartar as ocorrências apagaria o histórico de pagamento e\n-- toda edição manual junto.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. A única tabela que falta\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_recurring_bill_occurrences (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid REFERENCES app.units(id),\n recurring_bill_id uuid NOT NULL\n REFERENCES public.plg_financial_recurring_bills(id) ON DELETE CASCADE,\n seq integer NOT NULL,\n competence_date date NOT NULL,\n due_date date NOT NULL,\n amount numeric NOT NULL,\n paid_amount numeric NOT NULL DEFAULT 0,\n status text NOT NULL DEFAULT 'provisioned'\n CHECK (status IN ('provisioned', 'consolidated', 'cancelled')),\n -- Alguém editou ESTE vencimento, fora da regra. É a razão de a ocorrência ser\n -- uma linha e não um cálculo.\n overridden boolean NOT NULL DEFAULT false,\n -- O título que o vencimento virou, quando virou. Fica nulo até consolidar, e\n -- não vem do V1: lá o campo aponta para \\`invoices\\`, cuja contraparte no V2 é\n -- uma VENDA, e ligar uma conta a pagar a uma venda inverteria o sentido do\n -- dinheiro para ganhar um link. A referência crua do V1 fica em metadata.\n invoice_id uuid REFERENCES public.plg_financial_invoices(id) ON DELETE SET NULL,\n notes text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (recurring_bill_id, seq)\n);\n\n-- A pergunta que a tela faz: \"o que vence neste período?\". Sem este índice ela\n-- varre as ocorrências de todos os tenants.\nCREATE INDEX IF NOT EXISTS plg_financial_recurring_bill_occurrences_due_idx\n ON public.plg_financial_recurring_bill_occurrences (tenant_id, due_date, status);\n\nDROP TRIGGER IF EXISTS plg_financial_recurring_bill_occurrences_updated_at\n ON public.plg_financial_recurring_bill_occurrences;\nCREATE TRIGGER plg_financial_recurring_bill_occurrences_updated_at\n BEFORE UPDATE ON public.plg_financial_recurring_bill_occurrences\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nGRANT SELECT, INSERT, UPDATE, DELETE\n ON public.plg_financial_recurring_bill_occurrences TO authenticated, service_role;\n\n-- O scaffold é quem sabe desenhar a política deste cluster (tenant, unidade,\n-- permissão do plugin, escotilhas de compartilhamento). Escrever a policy à mão\n-- criaria a segunda regra de autorização do financeiro, que diverge da primeira\n-- na próxima mudança do scaffold.\n--\n-- \\`unit_optional\\`, e não \\`unit\\`: \\`unit_id\\` aceita NULL aqui porque a conta pode\n-- não ter unidade. Declarar \\`unit\\` numa coluna anulável é o erro que já escondeu\n-- todo movimento financeiro sem unidade e zerou um relatório inteiro.\nSELECT app.scaffold_table('public.plg_financial_recurring_bill_occurrences'::regclass,\n 'financial.recurring_bill_occurrence', 'financial', false, 'enforce');\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. Os writers\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- O vocabulário do de-para é lista fechada de propósito: uma linha que nomeia um\n-- writer inexistente falha na hora, não na terceira hora da onda.\nDO $$\nDECLARE v_def text;\nBEGIN\n -- Replay-safe, e agora também MERGE-safe.\n --\n -- Este bloco reescrevia a lista inteira a partir do instantâneo daqui. Isso\n -- sobrevive a um replay (o guarda abaixo sai cedo), mas não sobrevive a um\n -- writer que tenha entrado ANTES deste arquivo por outro caminho: a\n -- reescrita apaga o nome e a linha que o usa passa a violar a constraint.\n --\n -- Foi o que aconteceu: \\`082\\` do spine (funil do V1) acrescenta\n -- \\`crm_pipeline\\`, o spine roda antes das cadeias de plugin, e este arquivo\n -- devolvia a lista sem ele — \\`db-from-zero\\` parou aqui com \"check constraint\n -- allowlist_writer_check is violated by some row\".\n --\n -- O padrão certo já estava escrito na 062 e é o que a própria 082 usa: ler a\n -- definição vigente e INJETAR os nomes ao lado de um termo que sabidamente\n -- está lá. Cirúrgico, não substitutivo.\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n\n -- Já sabe destes writers: nada a fazer.\n IF v_def IS NOT NULL AND strpos(v_def, 'fin_recurring_bill_occurrence') > 0 THEN\n RETURN;\n END IF;\n\n IF v_def IS NULL THEN\n -- Não existe ainda: cria com a lista deste momento.\n ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check\n CHECK (writer IS NULL OR writer = ANY (ARRAY[\n 'person', 'unit', 'category', 'product', 'order', 'invoice', 'price_table',\n 'price_table_item', 'package', 'package_item', 'field_rule', 'holiday', 'facet',\n 'fin_account', 'fin_payment_method_type', 'fin_payment_method', 'fin_card_brand',\n 'fin_chart_account', 'fin_cost_center', 'inv_measurement_unit', 'inv_unit_conversion',\n 'inv_location', 'inv_movement', 'order_item', 'fin_movement', 'authz_role',\n 'authz_role_permission', 'person_registration',\n 'fin_recurring_bill', 'fin_recurring_bill_item', 'fin_recurring_bill_occurrence']));\n RETURN;\n END IF;\n\n -- Existe: preserva o que está lá e acrescenta os três desta onda.\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''person''::text',\n '''person''::text, ''fin_recurring_bill''::text, '\n || '''fin_recurring_bill_item''::text, ''fin_recurring_bill_occurrence''::text');\nEND $$;\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_dir_v1 text := nullif(btrim(upper(coalesce(p_row ->> 'direction', ''))), '');\n v_dir text;\n v_unit_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'interval_unit', ''))), '');\n v_count integer := coalesce(nullif(btrim(coalesce(p_row ->> 'interval_count', '')), '')::integer, 1);\n v_freq text;\n v_amount numeric := (p_row ->> 'amount')::numeric;\n v_desc text := nullif(btrim(coalesce(p_row ->> 'description', '')), '');\n v_due_day integer;\n v_starts date := (p_row ->> 'starts_on')::date;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_starts IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma conta recorrente precisa de id e de primeiro vencimento');\n END IF;\n\n -- Sem correção silenciosa: o V1 grava 'D' (despesa) e 'R' (receita) em \\`type\\`.\n -- Um vencimento com o sentido do dinheiro trocado some do contas a pagar e\n -- aparece no contas a receber, e ninguém confere um número que parece plausível.\n v_dir := CASE v_dir_v1 WHEN 'D' THEN 'payable' WHEN 'R' THEN 'receivable' ELSE NULL END;\n IF v_dir IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('sentido do dinheiro desconhecido: type=%L (o V1 usa D=despesa, R=receita)', v_dir_v1));\n END IF;\n\n -- Duas colunas do V1 (unidade + quantidade) viram uma palavra no V2. A tradução\n -- é exaustiva de propósito: \"a cada 2 meses\" não tem nome no vocabulário do V2,\n -- e arredondar para 'monthly' dobraria a previsão de caixa daquela conta.\n v_freq := CASE\n WHEN v_unit_v1 = 'week' AND v_count = 1 THEN 'weekly'\n WHEN v_unit_v1 = 'month' AND v_count = 1 THEN 'monthly'\n WHEN v_unit_v1 = 'month' AND v_count = 3 THEN 'quarterly'\n WHEN v_unit_v1 = 'month' AND v_count = 12 THEN 'yearly'\n WHEN v_unit_v1 = 'year' AND v_count = 1 THEN 'yearly'\n ELSE NULL END;\n IF v_freq IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('periodicidade sem equivalente no V2: a cada %s %s (o V2 conhece '\n 'weekly, monthly, quarterly e yearly)', v_count, v_unit_v1));\n END IF;\n\n -- \\`description\\` e \\`amount\\` são NOT NULL no V2 e não existem na conta do V1:\n -- lá eles moram nos itens. O plano de extração os reconstrói a partir da\n -- própria origem (Step.augment) — se vierem vazios, a conta não tem item, e\n -- isso é uma linha quebrada na origem, não um default a inventar.\n IF v_desc IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'conta recorrente sem descrição: no V1 ela vem do item, e esta conta não tem item');\n END IF;\n IF v_amount IS NULL OR v_amount <= 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('valor da conta recorrente é %s: o V2 exige amount > 0, e no V1 ele é a '\n 'soma dos itens', coalesce(v_amount::text, 'nulo')));\n END IF;\n\n v_due_day := extract(day from v_starts)::integer;\n\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bills WHERE id = p_id) THEN\n INSERT INTO public.plg_financial_recurring_bills\n (id, tenant_id, unit_id, description, direction, counterparty_person_id, amount,\n chart_account_id, cost_center_id, frequency, due_day, starts_on, ends_on,\n is_active, notes, items, metadata)\n VALUES (p_id, p_tenant, (p_row ->> 'unit_id')::uuid, v_desc, v_dir,\n (p_row ->> 'counterparty_person_id')::uuid, v_amount,\n (p_row ->> 'chart_account_id')::uuid, (p_row ->> 'cost_center_id')::uuid,\n v_freq, v_due_day, v_starts, (p_row ->> 'ends_on')::date,\n coalesce((p_row ->> 'is_active')::boolean, true), p_row ->> 'notes',\n '[]'::jsonb, v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_recurring_bills SET\n unit_id = CASE WHEN p_row ? 'unit_id' THEN (p_row ->> 'unit_id')::uuid ELSE unit_id END,\n description = v_desc,\n direction = v_dir,\n counterparty_person_id = CASE WHEN p_row ? 'counterparty_person_id' THEN (p_row ->> 'counterparty_person_id')::uuid ELSE counterparty_person_id END,\n amount = v_amount,\n chart_account_id = CASE WHEN p_row ? 'chart_account_id' THEN (p_row ->> 'chart_account_id')::uuid ELSE chart_account_id END,\n cost_center_id = CASE WHEN p_row ? 'cost_center_id' THEN (p_row ->> 'cost_center_id')::uuid ELSE cost_center_id END,\n frequency = v_freq,\n due_day = v_due_day,\n starts_on = v_starts,\n ends_on = CASE WHEN p_row ? 'ends_on' THEN (p_row ->> 'ends_on')::date ELSE ends_on END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bills', p_id, '{}'::jsonb); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- O item do V1 é uma LINHA; no V2 ele é um elemento do jsonb \\`items\\` da conta.\n-- Por isso este writer não tem tabela própria e devolve o id da CONTA: o ledger\n-- passa a apontar a linha de item do V1 para a conta que a contém, que é onde o\n-- dado foi parar de verdade.\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill_item(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_bill uuid := (p_row ->> 'recurring_bill_id')::uuid;\n v_item jsonb;\n v_items jsonb;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_bill IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'um item precisa de id e da conta recorrente a que pertence');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bills\n WHERE id = v_bill AND tenant_id = p_tenant) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a conta recorrente %s não existe neste tenant', v_bill));\n END IF;\n\n v_item := jsonb_strip_nulls(jsonb_build_object(\n 'id', p_id,\n 'description', p_row ->> 'description',\n 'quantity', (p_row ->> 'quantity')::numeric,\n 'unit_price', (p_row ->> 'unit_price')::numeric,\n 'discount', (p_row ->> 'discount')::numeric,\n 'addition', (p_row ->> 'addition')::numeric,\n 'total_amount', (p_row ->> 'total_amount')::numeric,\n 'chart_account_id', p_row ->> 'chart_account_id',\n 'cost_center_id', p_row ->> 'cost_center_id',\n -- No V2 um serviço É um produto: as duas colunas do V1 desaguam aqui.\n 'product_id', coalesce(p_row ->> 'product_id', p_row ->> 'service_product_id')\n ));\n\n -- Substitui o item de mesmo id em vez de acrescentar: reoferecer a mesma linha\n -- numa onda seguinte tem que atualizar, não duplicar o valor da conta.\n SELECT coalesce(jsonb_agg(x), '[]'::jsonb) INTO v_items\n FROM (SELECT value AS x FROM jsonb_array_elements(\n (SELECT items FROM public.plg_financial_recurring_bills WHERE id = v_bill))\n WHERE value ->> 'id' IS DISTINCT FROM p_id::text) s;\n\n UPDATE public.plg_financial_recurring_bills\n SET items = v_items || jsonb_build_array(v_item)\n WHERE id = v_bill;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bills', v_bill, '{}'::jsonb); END IF;\n -- target_id é a CONTA: é lá que o item mora no V2.\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_bill,\n 'reason', 'item gravado em plg_financial_recurring_bills.items');\nEND $function$;\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill_occurrence(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_bill uuid := (p_row ->> 'recurring_bill_id')::uuid;\n v_status_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text;\n v_unit uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_bill IS NULL OR p_row ->> 'due_date' IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma ocorrência precisa de id, da conta recorrente e de vencimento');\n END IF;\n SELECT unit_id INTO v_unit FROM public.plg_financial_recurring_bills\n WHERE id = v_bill AND tenant_id = p_tenant;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a conta recorrente %s não existe neste tenant', v_bill));\n END IF;\n\n -- O V1 usa provisioned | consolidated; 'cancelled' existe do lado do V2 para o\n -- vencimento cancelado DEPOIS da virada. Estado desconhecido vira quarentena\n -- com a palavra à vista, não um estado aproximado.\n v_status := CASE v_status_v1\n WHEN 'provisioned' THEN 'provisioned'\n WHEN 'consolidated' THEN 'consolidated'\n WHEN 'cancelled' THEN 'cancelled'\n WHEN 'canceled' THEN 'cancelled'\n ELSE NULL END;\n IF v_status IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('estado de vencimento desconhecido: %L', v_status_v1));\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bill_occurrences WHERE id = p_id) THEN\n INSERT INTO public.plg_financial_recurring_bill_occurrences\n (id, tenant_id, unit_id, recurring_bill_id, seq, competence_date, due_date,\n amount, paid_amount, status, overridden, notes, metadata)\n VALUES (p_id, p_tenant, v_unit, v_bill,\n coalesce((p_row ->> 'seq')::integer, 1),\n coalesce((p_row ->> 'competence_date')::date, (p_row ->> 'due_date')::date),\n (p_row ->> 'due_date')::date,\n coalesce((p_row ->> 'amount')::numeric, 0),\n coalesce((p_row ->> 'paid_amount')::numeric, 0),\n v_status, coalesce((p_row ->> 'overridden')::boolean, false),\n p_row ->> 'notes', v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_recurring_bill_occurrences SET\n unit_id = v_unit,\n seq = CASE WHEN p_row ? 'seq' THEN coalesce((p_row ->> 'seq')::integer, seq) ELSE seq END,\n competence_date = CASE WHEN p_row ? 'competence_date' THEN coalesce((p_row ->> 'competence_date')::date, competence_date) ELSE competence_date END,\n due_date = (p_row ->> 'due_date')::date,\n amount = CASE WHEN p_row ? 'amount' THEN coalesce((p_row ->> 'amount')::numeric, amount) ELSE amount END,\n paid_amount = CASE WHEN p_row ? 'paid_amount' THEN coalesce((p_row ->> 'paid_amount')::numeric, paid_amount) ELSE paid_amount END,\n status = v_status,\n overridden = CASE WHEN p_row ? 'overridden' THEN coalesce((p_row ->> 'overridden')::boolean, overridden) ELSE overridden END,\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bill_occurrences', p_id, '{}'::jsonb); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 3. O de-para\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- Saem da lista de adiadas: a nota delas dizia que o destino não existia, e ele\n-- existe (duas das três) ou passou a existir (a terceira, acima).\nDELETE FROM migration.excluded_tables\n WHERE source_table IN ('recurring_bills', 'recurring_bill_items', 'recurring_bill_occurrences');\n\n-- \\`_allow\\` é o construtor oficial: a allowlist tem gatilho de congelamento, e um\n-- INSERT direto por cima de uma linha \\`frozen\\` seria recusado.\n--\n-- As quatro colunas que o V2 exige e o V1 não tem na conta — description, amount,\n-- starts_on e o comprador — são reconstruídas a partir da PRÓPRIA origem pelo\n-- plano de extração (\\`Step.augment\\` em fayz_etl/sources/beautysoft_v1.py), que é\n-- a ponte declarada para isso. Nada aqui inventa valor.\n-- A replay finds these rows already frozen, and trg_allowlist_frozen refuses a\n-- plain write; a migration file is the writer the hatch exists for (033/035).\n-- SET LOCAL so the unlock dies with this transaction.\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'recurring_bills', 'public.plg_financial_recurring_bills', 'financial',\n 'fin_recurring_bill', '{}'::jsonb, ARRAY['amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'unit_id', 'unit_id@companies:app.units',\n 'type', 'direction',\n 'first_due_date', 'starts_on',\n 'end_date', 'ends_on',\n 'interval_count', 'interval_count',\n 'interval_unit', 'interval_unit',\n 'active', 'is_active',\n 'observations', 'notes',\n -- Derivadas pelo plano, a partir dos itens e do account_central da conta.\n 'description', 'description',\n 'amount', 'amount',\n 'counterparty_contact_id','counterparty_person_id@contacts:public.people',\n 'chart_account_id', 'chart_account_id@chart_of_accounts',\n 'cost_center_id', 'cost_center_id@cost_centers',\n -- A conta financeira da contraparte continua registrada, ainda que o V2\n -- prefira a PESSOA: é por ela que o V1 alcança quem recebe.\n 'account_central_id', 'jsonb:v1.account_central_id',\n -- O autor é um login do V1, e nada liga login do V1 a usuário do V2 (#115).\n -- A referência crua vale mais que perdê-la e mais que fingir que resolve.\n 'created_by', 'jsonb:v1.created_by',\n 'payment_method_id', 'jsonb:v1.payment_method_id'\n ),\n 'Regra de conta recorrente. description/amount/contraparte são derivados dos itens e do account_central pelo plano de extração.'\n);\n\nSELECT migration._allow(\n 'recurring_bill_items', 'public.plg_financial_recurring_bills', 'financial',\n 'fin_recurring_bill_item', '{}'::jsonb, ARRAY[]::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'recurring_bill_id', 'recurring_bill_id@recurring_bills',\n 'description', 'description',\n 'quantity', 'quantity',\n 'unit_price', 'unit_price',\n 'discount', 'discount',\n 'additional', 'addition',\n 'total_amount', 'total_amount',\n 'account_id', 'chart_account_id@chart_of_accounts',\n 'cost_center_id', 'cost_center_id@cost_centers',\n 'product_id', 'product_id@products',\n 'service_id', 'service_product_id@services:public.products',\n -- Único valor medido: 'other'. Traduzi-lo exigiria um vocabulário de destino\n -- que não existe; no metadata ele fica recuperável sem inventar nada.\n 'item_type', 'jsonb:v1.item_type'\n ),\n 'Item da conta. No V2 ele é elemento do jsonb \\`items\\` da própria conta — não tem tabela.'\n);\n\nSELECT migration._allow(\n 'recurring_bill_occurrences', 'public.plg_financial_recurring_bill_occurrences',\n 'financial',\n 'fin_recurring_bill_occurrence', '{}'::jsonb, ARRAY['amount', 'paid_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'recurring_bill_id', 'recurring_bill_id@recurring_bills',\n 'seq', 'seq',\n 'competence_date', 'competence_date',\n 'due_date', 'due_date',\n 'value', 'amount',\n 'paid_value', 'paid_amount',\n 'status', 'status',\n 'overridden', 'overridden',\n 'observations', 'notes',\n -- NÃO vira FK: no V1 aponta para \\`invoices\\`, cuja contraparte no V2 é uma\n -- VENDA. Ligar uma conta a pagar a uma venda inverteria o sentido do\n -- dinheiro para ganhar um link. O estado \\`consolidated\\` já diz o que importa.\n 'invoice_id', 'jsonb:v1.invoice_id'\n ),\n 'O vencimento. \\`overridden\\` marca o que alguém editou fora da regra — não é derivável.'\n);\n`\n\nexport const MIGRATION_033_O_VENCIMENTO_APONTA_PARA_O_TITULO = `-- O vencimento aponta para o título.\n--\n-- Correção de um mapeamento da 046. Lá, \\`recurring_bill_occurrences.invoice_id\\`\n-- foi jogado em \\`metadata\\` com esta justificativa:\n--\n-- \"NÃO vira FK: no V1 aponta para \\`invoices\\`, cuja contraparte no V2 é uma\n-- VENDA. Ligar uma conta a pagar a uma venda inverteria o sentido do\n-- dinheiro para ganhar um link.\"\n--\n-- A primeira metade está certa e a segunda é falsa. \\`invoices\\` do V1 tem DOIS\n-- destinos no V2, não um: o writer \\`order\\` grava a Venda em \\`public.orders\\` e\n-- devolve, em \\`extra\\`, o TÍTULO em \\`public.plg_financial_invoices\\`. O ledger\n-- guarda as duas linhas. Medido no tenant 8: 2.365 para \\`public.orders\\` e 2.365\n-- para \\`public.plg_financial_invoices\\`.\n--\n-- E o título é exatamente o objeto certo: das 111 faturas que as ocorrências\n-- consolidadas referenciam, as 111 têm título no V2, e **todas com\n-- \\`direction = 'payable'\\`**. Não há inversão nenhuma a evitar.\n--\n-- ── O que o erro custava ─────────────────────────────────────────────────────\n--\n-- \\`recurring_bill_occurrences.paid_value\\` é 0 nas 833 linhas da origem — no V1 o\n-- pagamento não mora no vencimento, mora no movimento financeiro ligado ao\n-- título. Com \\`invoice_id\\` em metadata, a ocorrência consolidada chegava ao V2\n-- sem nenhum caminho até o dinheiro: 123 vencimentos marcados \\`consolidated\\` e\n-- indistinguíveis, na tela, de 710 em aberto. A tela dizia a verdade sobre o\n-- campo e mentia sobre o fato.\n--\n-- Com a FK, o caminho existe:\n--\n-- ocorrência → plg_financial_invoices → plg_financial_movements (paid_amount)\n--\n-- São R$ 311.006,00 pagos em 181 movimentos, que passam a ser alcançáveis a\n-- partir do vencimento.\n--\n-- Idempotente.\n\n-- A linha já está \\`frozen\\`, e o gatilho \\`trg_allowlist_frozen\\` recusa a escrita\n-- com a frase certa: \"core-A allowlist changes are a migration file, not a\n-- write\". Este É o arquivo de migration, e a escotilha existe exatamente para\n-- ele — \\`SET LOCAL\\`, para a destravada morrer com a transação e não vazar para a\n-- sessão seguinte.\nSET LOCAL migration.allowlist_unlock = 'on';\n\n-- \\`_allow\\` reescreve a linha inteira (ON CONFLICT DO UPDATE), então o mapa vai\n-- completo — inclusive as colunas que não mudaram.\nSELECT migration._allow(\n 'recurring_bill_occurrences', 'public.plg_financial_recurring_bill_occurrences',\n 'financial',\n 'fin_recurring_bill_occurrence', '{}'::jsonb, ARRAY['amount', 'paid_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'recurring_bill_id', 'recurring_bill_id@recurring_bills',\n 'seq', 'seq',\n 'competence_date', 'competence_date',\n 'due_date', 'due_date',\n 'value', 'amount',\n 'paid_value', 'paid_amount',\n 'status', 'status',\n 'overridden', 'overridden',\n 'observations', 'notes',\n -- O TÍTULO, não a venda. \\`invoices\\` tem dois destinos no V2 e este é o\n -- financeiro; o \\`:public.plg_financial_invoices\\` escolhe qual dos dois o\n -- ledger deve resolver.\n 'invoice_id', 'invoice_id@invoices:public.plg_financial_invoices'\n ),\n 'O vencimento. \\`overridden\\` marca o que alguém editou fora da regra — não é '\n 'derivável. \\`invoice_id\\` liga o vencimento consolidado ao título, que é onde o '\n 'pagamento mora: no V1 paid_value é sempre 0.'\n);\n\n-- O writer precisa gravar a coluna que o de-para agora entrega. Sem isto o mapa\n-- resolveria a referência e o writer a jogaria fora em silêncio — que é a forma\n-- mais cara de errar aqui, porque tudo parece ter funcionado.\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill_occurrence(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_bill uuid := (p_row ->> 'recurring_bill_id')::uuid;\n v_invoice uuid := (p_row ->> 'invoice_id')::uuid;\n v_status_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text;\n v_unit uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_bill IS NULL OR p_row ->> 'due_date' IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma ocorrência precisa de id, da conta recorrente e de vencimento');\n END IF;\n SELECT unit_id INTO v_unit FROM public.plg_financial_recurring_bills\n WHERE id = v_bill AND tenant_id = p_tenant;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a conta recorrente %s não existe neste tenant', v_bill));\n END IF;\n\n -- O V1 usa provisioned | consolidated; 'cancelled' existe do lado do V2 para o\n -- vencimento cancelado DEPOIS da virada. Estado desconhecido vira quarentena\n -- com a palavra à vista, não um estado aproximado.\n v_status := CASE v_status_v1\n WHEN 'provisioned' THEN 'provisioned'\n WHEN 'consolidated' THEN 'consolidated'\n WHEN 'cancelled' THEN 'cancelled'\n WHEN 'canceled' THEN 'cancelled'\n ELSE NULL END;\n IF v_status IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('estado de vencimento desconhecido: %L', v_status_v1));\n END IF;\n\n -- Um título que não existe (ou é de outro tenant) não derruba o vencimento: o\n -- vencimento é o dado, o vínculo é o enriquecimento. Perder o vínculo é ruim;\n -- perder o vencimento por causa dele seria pior.\n IF v_invoice IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.plg_financial_invoices\n WHERE id = v_invoice AND tenant_id = p_tenant) THEN\n v_meta := migration._deep_set(v_meta, ARRAY['v1', 'invoice_unresolved'], to_jsonb(v_invoice));\n v_invoice := NULL;\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bill_occurrences WHERE id = p_id) THEN\n INSERT INTO public.plg_financial_recurring_bill_occurrences\n (id, tenant_id, unit_id, recurring_bill_id, seq, competence_date, due_date,\n amount, paid_amount, status, overridden, invoice_id, notes, metadata)\n VALUES (p_id, p_tenant, v_unit, v_bill,\n coalesce((p_row ->> 'seq')::integer, 1),\n coalesce((p_row ->> 'competence_date')::date, (p_row ->> 'due_date')::date),\n (p_row ->> 'due_date')::date,\n coalesce((p_row ->> 'amount')::numeric, 0),\n coalesce((p_row ->> 'paid_amount')::numeric, 0),\n v_status, coalesce((p_row ->> 'overridden')::boolean, false),\n v_invoice, p_row ->> 'notes', v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_recurring_bill_occurrences SET\n unit_id = v_unit,\n seq = CASE WHEN p_row ? 'seq' THEN coalesce((p_row ->> 'seq')::integer, seq) ELSE seq END,\n competence_date = CASE WHEN p_row ? 'competence_date' THEN coalesce((p_row ->> 'competence_date')::date, competence_date) ELSE competence_date END,\n due_date = (p_row ->> 'due_date')::date,\n amount = CASE WHEN p_row ? 'amount' THEN coalesce((p_row ->> 'amount')::numeric, amount) ELSE amount END,\n paid_amount = CASE WHEN p_row ? 'paid_amount' THEN coalesce((p_row ->> 'paid_amount')::numeric, paid_amount) ELSE paid_amount END,\n status = v_status,\n overridden = CASE WHEN p_row ? 'overridden' THEN coalesce((p_row ->> 'overridden')::boolean, overridden) ELSE overridden END,\n invoice_id = coalesce(v_invoice, invoice_id),\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bill_occurrences', p_id, '{}'::jsonb); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- A leitura de contas a pagar, num lugar só\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- A pergunta que a tela faz é \"o que vence, e o que já foi pago?\", e respondê-la\n-- exige três junções e o conhecimento de que o pagamento mora no movimento, não\n-- no vencimento. Uma view põe esse conhecimento no banco em vez de em cada tela\n-- que vier a perguntar.\nCREATE OR REPLACE VIEW public.v_financial_payables AS\nSELECT\n o.tenant_id,\n o.unit_id,\n o.id AS occurrence_id,\n b.id AS recurring_bill_id,\n b.description,\n b.counterparty_person_id,\n p.name AS counterparty_name,\n b.frequency,\n o.seq,\n o.competence_date,\n o.due_date,\n o.amount,\n -- O pagamento vem do movimento do título; \\`o.paid_amount\\` é 0 em tudo que veio\n -- do V1, e somar os dois contaria duas vezes o dia em que o V2 começar a\n -- escrever no vencimento.\n coalesce(o.paid_amount, 0) + coalesce(m.pago, 0) AS paid_amount,\n o.amount - (coalesce(o.paid_amount, 0) + coalesce(m.pago, 0)) AS open_amount,\n o.status,\n o.overridden,\n o.invoice_id,\n CASE\n WHEN o.status = 'cancelled' THEN 'cancelado'\n WHEN o.amount <= coalesce(o.paid_amount, 0) + coalesce(m.pago, 0) THEN 'pago'\n WHEN coalesce(m.pago, 0) > 0 THEN 'parcial'\n WHEN o.due_date < current_date THEN 'vencido'\n ELSE 'em aberto'\n END AS situacao\nFROM public.plg_financial_recurring_bill_occurrences o\nJOIN public.plg_financial_recurring_bills b ON b.id = o.recurring_bill_id\nLEFT JOIN public.people p ON p.id = b.counterparty_person_id\nLEFT JOIN LATERAL (\n SELECT sum(mv.paid_amount) AS pago\n FROM public.plg_financial_movements mv\n WHERE mv.financial_invoice_id = o.invoice_id\n) m ON o.invoice_id IS NOT NULL;\n\nCOMMENT ON VIEW public.v_financial_payables IS\n 'Contas a pagar por vencimento, com o pago somado do movimento do título. '\n 'No V1 recurring_bill_occurrences.paid_value é sempre 0: o pagamento mora no '\n 'movimento, e sem esta junção todo vencimento consolidado parece em aberto.';\n\nGRANT SELECT ON public.v_financial_payables TO authenticated, service_role;\n`\n\nexport const MIGRATION_034_A_UNIDADE_TEM_CONTA_E_A_PESSOA_TEM_UMA_SO = `-- A unidade tem conta, e a pessoa tem uma só.\n--\n-- Duas decisões sobre o razão financeiro, tomadas depois de medir o que cada uma\n-- custava. As duas estavam registradas como \"adiar\" com uma estimativa de custo,\n-- e a estimativa veio do cliente errado.\n--\n-- ── 1. \\`account_type = 'unit'\\` ───────────────────────────────────────────────\n--\n-- O plano de extração do V1 traduz o vocabulário de conta do V1 para as oito\n-- espécies do V2, e deixava \\`unit\\` passar sem tradução de propósito, com este\n-- comentário:\n--\n-- \"conta da unidade não é pessoa, nem banco, nem caixa. Encaixar em 'system'\n-- seria escolher a semântica no lugar de quem é dono do contrato\n-- financeiro. Custa 6 movimentos na onda 1.\"\n--\n-- A recusa em escolher estava certa. A estimativa não: os 6 movimentos são do\n-- Espaço Facial, uma rede de estética onde o dinheiro entra pela venda. Num\n-- restaurante o caixa passa pela conta da própria unidade, e no Pertinho do Céu\n-- (licença 8) DUAS contas travam **1.359 movimentos** — 30% do razão:\n--\n-- account_central #195 \"Pertinho do Céu\" → 777 movimentos\n-- account_central #654100 \"PC Serviços\" → 582 movimentos\n--\n-- E não é só a contagem: sem elas, \\`v_financial_payables\\` mostra 833 vencimentos\n-- em aberto num cliente que pagou R$ 311.006. O número certo some junto com a\n-- conta.\n--\n-- A escolha foi **criar a espécie**, não encaixar em \\`system\\`. \\`system\\` já tem\n-- dono semântico (\\`CHECK (kind <> 'system' OR code IS NOT NULL)\\` — conta de\n-- plano de contas, identificada por código) e jogar a conta da casa lá dentro\n-- faria os dois conceitos compartilharem um balde que nenhum relatório consegue\n-- separar depois.\n--\n-- ── 2. Uma conta por pessoa ──────────────────────────────────────────────────\n--\n-- \\`plg_financial_accounts_person_uq\\` diz que uma pessoa tem UMA conta no tenant.\n-- O V1 permite várias (48 casos no Pertinho do Céu), e cada colisão derrubava a\n-- conta E todos os movimentos que a referenciavam.\n--\n-- A constraint FICA. A decisão foi **fundir**: da segunda conta em diante, o\n-- writer devolve o id da conta que já existe, o ledger amarra a linha do V1 a\n-- ela, e todo movimento daquela conta passa a apontar para a conta única da\n-- pessoa. O saldo da pessoa fica certo; o que se perde é a separação que o V1\n-- fazia entre as contas dela, e isso fica escrito em\n-- \\`metadata.migration.merged_from\\`.\n--\n-- Fundir é o mesmo mecanismo que \\`upsert_person\\` já usa para pessoa duplicada, e\n-- é reversível pela leitura do ledger: \\`source_numeric_id\\` distintos apontando\n-- para o mesmo \\`target_id\\` é a marca da fusão.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. A espécie nova\n-- ─────────────────────────────────────────────────────────────────────────────\n\nDO $$\nBEGIN\n -- Replay-safe: once the CHECK knows \\`unit\\`, leave it alone. 037 extends the\n -- same list with \\`unidentified\\`, and rewriting it from this snapshot on a\n -- replay fails on every account 037's kind already holds.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n WHERE conname = 'plg_financial_accounts_kind_check'\n AND strpos(pg_get_constraintdef(oid), '''unit''') > 0) THEN\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_kind_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_kind_check\n CHECK (kind = ANY (ARRAY['bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit',\n 'card_payable', 'person', 'system', 'unit']));\n END IF;\n\n -- Uma conta de unidade sem unidade seria a conta de ninguém. O par espécie ↔\n -- coluna obrigatória é a mesma disciplina que \\`person\\`/\\`person_id\\` já tem.\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_unit_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_unit_check\n CHECK (kind <> 'unit' OR unit_id IS NOT NULL);\nEND $$;\n\nCOMMENT ON CONSTRAINT plg_financial_accounts_unit_check ON public.plg_financial_accounts IS\n 'A conta da própria unidade (a conta da casa) nomeia a unidade a que pertence. '\n 'Sem isso ela seria indistinguível de uma conta de sistema sem dono.';\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O writer\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L — V1 account_type does not map onto the ledger''s nine', v_kind));\n END IF;\n\n -- A pessoa da conta polimórfica. O ledger é a única fonte que sabe o de-para,\n -- e ele guarda a tabela de origem — então \\`clients\\`, \\`contacts\\` e\n -- \\`professionals\\` resolvem pelo mesmo caminho, sem o writer conhecer nenhuma.\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = (v_meta #>> '{v1,reference_table}')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n -- Recusar é o certo, e é temporário: a pessoa entra na mesma onda, e a\n -- segunda passada oferece a conta de novo. Inventar uma pessoa aqui seria\n -- pendurar dinheiro em quem não existe.\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda não migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', '<sem tabela>'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n\n -- FUSÃO. O V2 admite uma conta por pessoa e o V1 admite várias; da segunda\n -- em diante a conta do V1 passa a APONTAR para a que já existe, em vez de\n -- derrubar a si mesma e a todo movimento que a referencia.\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n -- A unidade da conta da casa, pelo mesmo caminho da pessoa: o ledger sabe qual\n -- \\`app.units\\` a \\`companies\\` do V1 virou.\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_meta #>> '{v1,reference_table}', 'companies')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda não migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', 'companies'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n -- Enriquece a conta que fica e devolve O ID DELA. O motor grava esse\n -- target_id no ledger, e a partir daí todo movimento que referenciava a\n -- conta do V1 resolve para a conta única da pessoa no V2.\n UPDATE public.plg_financial_accounts SET\n is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true),\n metadata = migration._merge_meta(\n metadata,\n migration._deep_set(v_meta, ARRAY['migration', 'merged_from'],\n coalesce(metadata #> '{migration,merged_from}', '[]'::jsonb)\n || jsonb_build_array(jsonb_build_object(\n 'v1_account', v_meta #> '{v1,reference_id}',\n 'name', p_row ->> 'name', 'account_id', p_id))))\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta única da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code',\n p_row ->> 'name',\n p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true),\n v_person,\n v_meta);\n v_created := true;\n ELSE\n -- kind e person_id andam JUNTOS: a constraint amarra os dois, então mudar\n -- um sem o outro derruba a linha que já estava lá.\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n`\n\nexport const MIGRATION_035_A_CLASSIFICACAO_CONTABIL_CHEGA_INTEIRA = `-- A classificação contábil chega inteira.\n--\n-- Sem esta migration não existe DRE no V2 — e o motivo não é a tela. O DRE do V1\n-- é montado sobre DOIS eixos, e os dois se perdiam no caminho:\n--\n-- 1. a NATUREZA da conta (receita × despesa)\n-- 2. a CONTA CONTÁBIL DO ITEM (qual linha do DRE aquele lançamento é)\n--\n-- ── Eixo 1: a natureza estava na coluna errada ───────────────────────────────\n--\n-- O de-para mapeava \\`chart_of_accounts.account_type → node_type\\`. São coisas\n-- diferentes: no V1 \\`account_type\\` é a NATUREZA (asset|liability|equity|revenue|\n-- expense); no V2 \\`node_type\\` é a FORMA DO NÓ (group|leaf) e \\`account_type\\` é a\n-- natureza. O resultado, medido no cluster em 18/09/2026:\n--\n-- node_type = 'expense' → 302 linhas account_type = NULL\n-- node_type = 'revenue' → 38 linhas account_type = NULL\n-- node_type = 'leaf' → 244 linhas account_type preenchido\n--\n-- As 244 corretas são as que alguém criou pelo app; as 340 erradas são as que o\n-- porte escreveu. E \\`upsert_fin_chart_account\\` **nunca gravou \\`account_type\\`** —\n-- a coluna não aparece no INSERT nem no UPDATE dele.\n--\n-- Um DRE sem natureza não separa receita de despesa. Não erra por pouco: não\n-- monta.\n--\n-- ── Eixo 2: a conta do item virava texto opaco ───────────────────────────────\n--\n-- \\`invoice_items.account_id → jsonb:financial.v1_account_ref\\`. A classificação\n-- contábil, que no V1 é do ITEM e não do movimento, chegava ao V2 como o id\n-- numérico cru do V1 dentro de \\`metadata\\` — sem FK, sem join possível. E não\n-- havia para onde resolver: \\`public.items\\` não tinha coluna de conta contábil.\n--\n-- (\\`plg_financial_invoice_items.chart_account_id\\` existe e tem ZERO linhas: o\n-- item do V1 cai só em \\`public.items\\`; não há split financeiro para ele.)\n--\n-- ── O terceiro eixo NÃO precisou de nada ─────────────────────────────────────\n--\n-- A direção (receita × despesa do documento) parecia perdida também —\n-- \\`invoices.type → jsonb:financial.v1_type\\`. Não está: o writer \\`order\\` devolve\n-- em \\`extra\\` o TÍTULO em \\`plg_financial_invoices\\`, e lá \\`direction\\` já chega\n-- certa. Medido: 8.175 \\`receivable\\` + 2.503 \\`payable\\`. O DRE lê o título, não o\n-- pedido, e o eixo já existe.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. O item ganha onde guardar a classificação\n-- ─────────────────────────────────────────────────────────────────────────────\n\nALTER TABLE public.items\n ADD COLUMN IF NOT EXISTS chart_account_id uuid REFERENCES public.plg_financial_chart_of_accounts(id),\n ADD COLUMN IF NOT EXISTS cost_center_id uuid REFERENCES public.plg_financial_cost_centers(id);\n\nCOMMENT ON COLUMN public.items.chart_account_id IS\n 'Conta contábil DESTE item. A classificação é da linha, não do documento: no '\n 'V1 dois itens da mesma venda podem cair em ramos diferentes do DRE, e é essa '\n 'granularidade que o relatório precisa.';\nCOMMENT ON COLUMN public.items.cost_center_id IS\n 'Centro de custo do item. O DRE não usa — é a segunda dimensão, para análise '\n 'de custo — mas perder o dado na entrada seria irreversível.';\n\n-- A pergunta do DRE é \"some os itens por conta, no período\". Sem índice ela\n-- varre \\`items\\` inteira por linha do relatório.\nCREATE INDEX IF NOT EXISTS items_chart_account_idx\n ON public.items (tenant_id, chart_account_id) WHERE chart_account_id IS NOT NULL;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. Conserto do dado já migrado\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- Devolve a natureza para a coluna certa nas linhas que o porte escreveu. O\n-- teste é o próprio vocabulário: \\`node_type\\` só pode ser group|leaf|account, e\n-- qualquer outra coisa ali é natureza que foi parar no lugar errado.\nUPDATE public.plg_financial_chart_of_accounts\n SET account_type = node_type,\n node_type = 'leaf'\n WHERE node_type IN ('asset', 'liability', 'equity', 'revenue', 'expense')\n AND account_type IS NULL;\n\n-- \\`node_type\\` de verdade: é grupo quem tem filho. O V1 não tem essa coluna, e\n-- derivá-la da própria árvore é mais honesto que herdar um default.\nUPDATE public.plg_financial_chart_of_accounts p\n SET node_type = 'group'\n WHERE node_type <> 'group'\n AND EXISTS (SELECT 1 FROM public.plg_financial_chart_of_accounts c WHERE c.parent_id = p.id);\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 3. Os writers\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_chart_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_type text := nullif(btrim(lower(coalesce(p_row ->> 'account_type', ''))), '');\n v_parent uuid := (p_row ->> 'parent_id')::uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n\n -- Sem correção silenciosa: uma natureza fora do vocabulário do razão faria a\n -- conta sumir do DRE sem aparecer em lugar nenhum — nem na receita, nem na\n -- despesa, nem num aviso.\n IF v_type IS NOT NULL AND v_type NOT IN ('asset', 'liability', 'equity', 'revenue', 'expense') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('natureza de conta desconhecida: %L (o razão conhece asset, liability, equity, revenue e expense)', v_type));\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_chart_of_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_chart_of_accounts\n (id, tenant_id, code, name, node_type, account_type, parent_id, is_active)\n VALUES (p_id, p_tenant, p_row ->> 'code', p_row ->> 'name',\n -- Nasce folha; vira grupo abaixo, quando o primeiro filho chegar.\n coalesce(p_row ->> 'node_type', 'leaf'),\n v_type, v_parent,\n coalesce((p_row ->> 'is_active')::boolean, true));\n v_created := true;\n ELSE\n UPDATE public.plg_financial_chart_of_accounts SET\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n node_type = CASE WHEN p_row ? 'node_type' THEN coalesce(p_row ->> 'node_type', node_type) ELSE node_type END,\n account_type = coalesce(v_type, account_type),\n parent_id = CASE WHEN p_row ? 'parent_id' THEN v_parent ELSE parent_id END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END\n WHERE id = p_id;\n END IF;\n\n -- Quem tem filho é grupo. Marcar aqui, e não num passe no fim, faz a árvore\n -- ficar correta mesmo se a onda parar no meio.\n IF v_parent IS NOT NULL THEN\n UPDATE public.plg_financial_chart_of_accounts\n SET node_type = 'group'\n WHERE id = v_parent AND tenant_id = p_tenant AND node_type <> 'group';\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_chart_of_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- O writer do item passa a gravar as duas colunas novas. O resto do corpo é o\n-- que já estava lá — está reemitido inteiro porque é assim que se troca uma\n-- função no Postgres, não porque mudou.\nCREATE OR REPLACE FUNCTION migration.upsert_order_item(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_order uuid := (p_row ->> 'order_id')::uuid;\n v_chart uuid := (p_row ->> 'chart_account_id')::uuid;\n v_center uuid := (p_row ->> 'cost_center_id')::uuid;\n v_order_tenant uuid;\n v_existing uuid;\n v_cancelled timestamptz;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_order IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an order line needs an id and an order');\n END IF;\n SELECT tenant_id INTO v_order_tenant FROM public.orders WHERE id = v_order;\n IF v_order_tenant IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk', 'reason', format('order %s is not in the destination yet', v_order));\n END IF;\n IF v_order_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order %s belongs to another tenant', v_order));\n END IF;\n\n v_cancelled := nullif(v_meta #>> '{cancellation,at}', '')::timestamptz;\n\n SELECT tenant_id INTO v_existing FROM public.items WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order line %s already exists in another tenant', p_id));\n END IF;\n IF v_existing IS NULL THEN\n -- \\`order_id\\` e SÓ ele: a fatura alcança a linha pelo pedido (items_one_parent).\n INSERT INTO public.items (id, tenant_id, order_id, product_id, name, description,\n quantity, unit_price, discount, total, sort_order,\n cancelled_at, cancel_reason, chart_account_id, cost_center_id, metadata)\n VALUES (p_id, p_tenant, v_order, (p_row ->> 'product_id')::uuid,\n coalesce(nullif(btrim(coalesce(p_row ->> 'name', '')), ''), 'Item'),\n coalesce(p_row ->> 'description', nullif(btrim(coalesce(p_row ->> 'name', '')), '')),\n greatest(coalesce((p_row ->> 'quantity')::numeric, 1), 0.0001),\n round(coalesce((p_row ->> 'unit_price')::numeric, 0), 2),\n round(coalesce((p_row ->> 'discount')::numeric, 0), 2),\n round(coalesce((p_row ->> 'total')::numeric, 0), 2),\n coalesce((p_row ->> 'sort_order')::integer, 0),\n v_cancelled, v_meta #>> '{cancellation,reason}',\n v_chart, v_center, v_meta);\n v_created := true;\n ELSE\n UPDATE public.items SET\n order_id = v_order,\n product_id = CASE WHEN p_row ? 'product_id' THEN (p_row ->> 'product_id')::uuid ELSE product_id END,\n name = CASE WHEN p_row ? 'name' THEN coalesce(nullif(btrim(p_row ->> 'name'), ''), name) ELSE name END,\n quantity = CASE WHEN p_row ? 'quantity' THEN greatest(coalesce((p_row ->> 'quantity')::numeric, 1), 0.0001) ELSE quantity END,\n unit_price = CASE WHEN p_row ? 'unit_price' THEN round(coalesce((p_row ->> 'unit_price')::numeric, 0), 2) ELSE unit_price END,\n discount = CASE WHEN p_row ? 'discount' THEN round(coalesce((p_row ->> 'discount')::numeric, 0), 2) ELSE discount END,\n total = CASE WHEN p_row ? 'total' THEN round(coalesce((p_row ->> 'total')::numeric, 0), 2) ELSE total END,\n cancelled_at = coalesce(v_cancelled, cancelled_at),\n cancel_reason = coalesce(v_meta #>> '{cancellation,reason}', cancel_reason),\n -- \\`coalesce\\` e não CASE: a classificação é enriquecimento. Uma reoferta\n -- sem a coluna não deve apagar a conta que já está lá.\n chart_account_id = coalesce(v_chart, chart_account_id),\n cost_center_id = coalesce(v_center, cost_center_id),\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.items', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 4. O de-para\n-- ─────────────────────────────────────────────────────────────────────────────\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\n-- \\`account_type\\` para \\`account_type\\`. O mapa anterior mandava a natureza para\n-- \\`node_type\\`, e é a correção que faz o DRE existir.\nSELECT migration._allow(\n 'chart_of_accounts', 'public.plg_financial_chart_of_accounts', 'financial',\n 'fin_chart_account', '{}'::jsonb, ARRAY[]::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'code', 'code',\n 'name', 'name',\n 'account_type', 'account_type',\n 'parent_id', 'parent_id@chart_of_accounts',\n 'active', 'is_active'\n ),\n 'Plano de contas. \\`account_type\\` é a NATUREZA (o eixo do DRE); \\`node_type\\` é '\n 'derivado da árvore pelo writer — quem tem filho é grupo.'\n);\n\n-- O item passa a resolver a conta contábil e o centro de custo em vez de\n-- carregá-los como id cru do V1 em metadata.\nSELECT migration._allow(\n 'invoice_items', 'public.items', 'financial',\n 'order_item', '{}'::jsonb, ARRAY['unit_price', 'total_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'invoice_id', 'order_id@invoices:public.orders',\n 'description', 'name',\n 'quantity', 'quantity',\n 'unit_price', 'unit_price',\n 'discount', 'discount',\n 'total_amount', 'total',\n 'additional', 'jsonb:financial.additional',\n 'product_id', 'product_id@products',\n 'service_id', 'product_id@services',\n 'account_id', 'chart_account_id@chart_of_accounts',\n 'cost_center_id', 'cost_center_id@cost_centers',\n 'item_type', 'jsonb:item_type',\n 'canceled_at', 'jsonb:cancellation.at',\n 'cancel_reason', 'jsonb:cancellation.reason',\n 'appointment_id', 'jsonb:agenda.v1_appointment_ref',\n 'execution_date', 'jsonb:execution.date',\n 'execution_start_time', 'jsonb:execution.start_time',\n 'execution_end_time', 'jsonb:execution.end_time',\n 'execution_photos', '-',\n 'executed_by_professional_id', 'jsonb:execution.v1_professional_ref',\n 'is_executed', 'jsonb:execution.is_executed',\n 'gift_card_id', 'jsonb:v1_gift_card_ref',\n 'service_package_id', 'jsonb:v1_package_ref'\n ),\n 'A linha da venda. \\`account_id\\` é a classificação contábil DO ITEM — o eixo '\n 'que o DRE agrupa; antes ela vinha como id cru do V1 em metadata.'\n);\n`\n\nexport const MIGRATION_036_A_FUSAO_PARA_DE_DOBRAR_A_PROPRIA_MEMORIA = `-- A fusão para de dobrar a própria memória.\n--\n-- Correção de um defeito que eu introduzi na 048 e que derrubou o cluster quatro\n-- vezes em 18/09/2026.\n--\n-- ── O que eu escrevi ─────────────────────────────────────────────────────────\n--\n-- Na fusão de conta de pessoa, para registrar de onde a conta veio:\n--\n-- metadata = migration._merge_meta(\n-- metadata,\n-- migration._deep_set(v_meta, ARRAY['migration','merged_from'],\n-- coalesce(metadata #> '{migration,merged_from}', '[]'::jsonb)\n-- || jsonb_build_array(...)))\n--\n-- Leio o array que já está na linha, acrescento um elemento — e entrego o\n-- resultado a \\`_merge_meta\\`, que **concatena com o array que já está na linha de\n-- novo**. Cada oferta não acrescenta um: ela DOBRA.\n--\n-- oferta 1 → 1 elemento\n-- oferta 5 → 16\n-- oferta 10 → 512\n-- oferta 18 → 262.143 ← medido\n--\n-- ── O que isso custou ────────────────────────────────────────────────────────\n--\n-- Medido antes deste conserto, no tenant Pertinho do Céu:\n--\n-- maior merged_from 262.143 elementos (2¹⁸ − 1)\n-- maior metadata 1.378 kB numa única linha\n--\n-- Montar e mesclar um jsonb desse tamanho, uma vez por linha, dentro de um\n-- statement, num cluster com \\`shared_buffers\\` de 224 MB, mata o backend. E o\n-- sintoma não apontava para cá: a conexão caía no meio de \\`account_central\\`, o\n-- transporte reportava \"server closed the connection unexpectedly\", e eu passei\n-- duas rodadas reduzindo o tamanho do lote — de 2.000 para 250, depois para 100.\n-- Nenhuma ajudou, porque 417 linhas nunca foram o problema.\n--\n-- ── O conserto ───────────────────────────────────────────────────────────────\n--\n-- \\`merged_from\\` sai. Não vira \"append idempotente\", não vira \"array com limite\":\n-- sai inteiro, porque ele nunca deveria ter existido — **o ledger já registra a\n-- fusão**, e melhor: dois \\`source_numeric_id\\` distintos apontando para o mesmo\n-- \\`target_id\\` É a definição de fusão, é consultável, e é o lugar onde o resto\n-- deste schema já procura essa resposta.\n--\n-- select source_numeric_id, target_id\n-- from migration.ledger\n-- where source_table = 'account_central' and target_table = 'public.plg_financial_accounts'\n-- order by target_id;\n--\n-- Guardar a mesma informação num jsonb ao lado era a segunda cópia da regra —\n-- exatamente o que o cabeçalho do \\`sdkmig.py\\` diz para não fazer. A segunda\n-- cópia divergiu no primeiro dia, e de forma exponencial.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. Desarmar o que já explodiu\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- Primeiro o dado, depois a função: enquanto as linhas gordas estiverem lá,\n-- qualquer rodada que as toque derruba o banco de novo — inclusive a que\n-- aplicaria esta migration mais adiante.\nUPDATE public.plg_financial_accounts\n SET metadata = (coalesce(metadata, '{}'::jsonb) #- '{migration,merged_from}')\n WHERE metadata #> '{migration,merged_from}' IS NOT NULL;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O writer para de acumular\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L — V1 account_type does not map onto the ledger''s nine', v_kind));\n END IF;\n\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = (v_meta #>> '{v1,reference_table}')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda não migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', '<sem tabela>'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_meta #>> '{v1,reference_table}', 'companies')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda não migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', 'companies'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n -- A fusão NÃO escreve histórico aqui. Quem registra que duas contas do V1\n -- viraram uma no V2 é o ledger: dois \\`source_numeric_id\\` apontando para o\n -- mesmo \\`target_id\\`. Acumular a mesma informação num array de metadata foi o\n -- defeito que dobrava de tamanho a cada oferta e derrubou o cluster.\n UPDATE public.plg_financial_accounts\n SET is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true)\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta única da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code', p_row ->> 'name', p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true), v_person, v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n`\n\nexport const MIGRATION_037_A_VENDA_DE_BALCAO_TEM_CONTA = `-- A venda de balcão tem conta.\n--\n-- Terceira espécie de conta que o V1 tem e o razão do V2 não tinha — depois de\n-- \\`unit\\` (a conta da casa, migration 048) vem \\`unidentified\\`: a conta da venda\n-- para quem não deixou cadastro.\n--\n-- No V1 ela é uma linha de \\`account_central\\` com \\`account_type = 'client'\\` e\n-- \\`reference_id = 0\\`. O tipo diz \"pessoa\"; a referência é o sentinela de \"não\n-- há pessoa\". O V2 não aceita a combinação, e com razão:\n--\n-- CHECK ((kind = 'person') = (person_id IS NOT NULL))\n--\n-- Uma conta de pessoa sem pessoa é dinheiro pendurado em ninguém. O CHECK está\n-- certo; o que faltava era a palavra para o caso em que **não há contraparte e\n-- isso é o fato**, não uma falha de cadastro.\n--\n-- ── Por que espécie própria, e não \\`system\\` ──────────────────────────────────\n--\n-- \\`system\\` já tem dono: é a conta do plano de contas, identificada por código\n-- (\\`CHECK (kind <> 'system' OR code IS NOT NULL)\\`). Jogar a venda de balcão lá\n-- dentro faria dois conceitos dividirem um balde que nenhum relatório separa\n-- depois — e o de balcão não é detalhe: num restaurante ele é a maior parte do\n-- movimento.\n--\n-- \\`unidentified\\` diz a verdade: existe contraparte, ela comprou, e ela não se\n-- identificou. É diferente de \\`system\\` (não há contraparte) e de \\`person\\` (há, e\n-- sabemos quem).\n--\n-- ── O que estava preso ───────────────────────────────────────────────────────\n--\n-- Medido em 18/09/2026 no tenant Pertinho do Céu, antes deste arquivo:\n--\n-- account_central em quarentena por \\`clients #0\\` 5\n-- financial_movements travados atrás delas 110\n-- valor R$ 15.102,78\n--\n-- A asserção de paridade (\\`make parity\\`) apontava exatamente isso: contas a\n-- pagar com Δ −10.218,30 contra a origem, e a cadeia inteira levava a uma conta.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. A espécie\n-- ─────────────────────────────────────────────────────────────────────────────\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_kind_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_kind_check\n CHECK (kind = ANY (ARRAY['bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit',\n 'card_payable', 'person', 'system', 'unit',\n 'unidentified']));\nEND $$;\n\nCOMMENT ON COLUMN public.plg_financial_accounts.kind IS\n 'bank | cash_physical | cash_session | card_receivable_credit | '\n 'card_receivable_debit | card_payable | person | system | unit | unidentified. '\n '\\`unidentified\\` é a contraparte que comprou e não se identificou (venda de '\n 'balcão): diferente de \\`system\\`, onde não há contraparte, e de \\`person\\`, onde '\n 'há e sabemos quem.';\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O writer reconhece o sentinela\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- Só a perna que muda: quando a conta é de pessoa e a referência da origem é o\n-- sentinela (\\`reference_id = 0\\`) ou aponta para linha que não existe mais, ela\n-- deixa de ser \\`person\\` e passa a ser \\`unidentified\\` — em vez de recusar a conta\n-- e, com ela, todo movimento que a cita.\n--\n-- A distinção importa e está no código: referência AUSENTE (0) vira\n-- \\`unidentified\\`; referência PRESENTE cuja pessoa ainda não migrou continua\n-- caindo em quarentena de \\`fk\\`, porque aí a pessoa existe e a ordem é que está\n-- errada — e a segunda passada resolve.\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_ref_id bigint := nullif(v_meta #>> '{v1,reference_id}', '')::bigint;\n v_ref_table text := v_meta #>> '{v1,reference_table}';\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n\n -- O sentinela ANTES da checagem de vocabulário: \\`clients#0\\` não é uma conta de\n -- pessoa mal formada, é uma conta sem contraparte.\n IF v_kind = 'person' AND coalesce(v_ref_id, 0) = 0 THEN\n v_kind := 'unidentified';\n END IF;\n\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit',\n 'unidentified') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L — V1 account_type does not map onto the ledger''s ten', v_kind));\n END IF;\n\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = v_ref_table\n AND l.source_numeric_id = v_ref_id\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda não migrou (%s #%s)',\n coalesce(v_ref_table, '<sem tabela>'), coalesce(v_ref_id::text, '<sem id>')));\n END IF;\n\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_ref_table, 'companies')\n AND l.source_numeric_id = v_ref_id\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda não migrou (%s #%s)',\n coalesce(v_ref_table, 'companies'), coalesce(v_ref_id::text, '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n UPDATE public.plg_financial_accounts\n SET is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true)\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta única da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code', p_row ->> 'name', p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true), v_person, v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n`\n\nexport const MIGRATION_038_A_LINHA_DA_FATURA_ALCANCA_QUEM_VEIO_PELO_PEDIDO = `-- A linha da fatura alcança quem veio pelo pedido.\n--\n-- \\`v_financial_invoice_lines\\` liga o item ao título por \\`items.invoice_id\\`. Só\n-- que \\`items_one_parent\\` — \\`CHECK (num_nonnulls(order_id, invoice_id,\n-- fulfillment_id, quote_id) = 1)\\` — diz que um item tem EXATAMENTE UM pai. Um\n-- item que nasceu numa venda tem \\`order_id\\` preenchido e \\`invoice_id\\` **nulo**,\n-- por construção, e a view nunca o enxerga.\n--\n-- Isso não é um caso de borda: é todo item que veio de venda, e é 100% do que o\n-- porte do V1 traz. Medido em 18/09/2026 no tenant Pertinho do Céu:\n--\n-- public.items do tenant 5.420\n-- …com invoice_id preenchido 0\n-- linhas em v_financial_invoice_lines 0\n--\n-- Cinco mil e quatrocentas linhas de fatura, e a view do financeiro devolvia\n-- zero. Qualquer tela ou relatório construído sobre ela — análise de custo,\n-- encargos por conta contábil, DRE por natureza de linha — nasce vazio e parece\n-- \"cliente sem dado\" em vez de \"consulta pelo caminho errado\".\n--\n-- ── O conserto ───────────────────────────────────────────────────────────────\n--\n-- O título alcança a linha por DOIS caminhos, e a view passa a cobrir os dois:\n--\n-- direto items.invoice_id = f.id (linha criada no título)\n-- pelo pedido items.order_id = f.order_id (linha criada na venda)\n--\n-- É \\`UNION ALL\\` e não um \\`JOIN ... ON a OR b\\` de propósito: a forma com OR faz o\n-- planner cair em nested loop sobre as duas tabelas inteiras, e esta view é lida\n-- por tela. Dois ramos disjuntos (o segundo exige \\`invoice_id IS NULL\\`) dão dois\n-- index scans e nenhuma linha contada duas vezes.\n--\n-- \\`chart_account_id\\` e \\`cost_center_id\\` passam a vir com \\`coalesce\\`: a extensão\n-- financeira (\\`plg_financial_invoice_items\\`) vence quando existe, e o item do\n-- núcleo responde quando não — que é onde o porte grava a classificação desde a\n-- migration 049 do spine.\n--\n-- Idempotente. Mesma lista de colunas, mesma ordem — \\`CREATE OR REPLACE VIEW\\`\n-- exige, e manter a assinatura é o que deixa esta correção ser um replace e não\n-- um drop com dependência quebrada.\n\n-- Replay guard. financial/120 appended item_cancelled_at, item_cancel_reason and\n-- item_metadata to this view; re-running this narrower list over that shape\n-- fails with «cannot drop columns from view». Once the view has them it is left\n-- as 120 made it. The invoker setting and the grant below are idempotent.\nDO $guard$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_attribute\n WHERE attrelid = to_regclass('public.v_financial_invoice_lines')\n AND attname = 'item_cancelled_at' AND NOT attisdropped) THEN\n RAISE NOTICE 'financial 038: v_financial_invoice_lines already has the 120 shape; left as it is';\n RETURN;\n END IF;\n\n EXECUTE $view$\nCREATE OR REPLACE VIEW public.v_financial_invoice_lines AS\n-- ── caminho 1: a linha nasceu no próprio título ──────────────────────────────\nSELECT i.id AS item_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n f.number AS invoice_number,\n f.direction,\n f.issue_date,\n f.cancelled_at,\n i.product_id,\n i.name,\n i.description,\n i.quantity,\n i.unit_price,\n i.discount,\n i.total,\n COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,\n COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,\n x.tax_amount,\n x.cfop,\n x.ncm,\n a.from_item_id AS billed_from_order_item_id,\n a.quantity AS billed_quantity\n FROM public.items i\n JOIN public.plg_financial_invoices f ON f.id = i.invoice_id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n LEFT JOIN public.item_associations a ON a.to_item_id = i.id\n WHERE i.invoice_id IS NOT NULL\n\nUNION ALL\n\n-- ── caminho 2: a linha nasceu na venda, e o título veio dela ─────────────────\n-- \\`i.invoice_id IS NULL\\` mantém os dois ramos disjuntos: sem isso, um item com\n-- os dois preenchidos apareceria duas vezes e dobraria o valor da conta. Hoje o\n-- CHECK impede que isso exista, e a condição é o cinto que sobrevive a um dia em\n-- que o CHECK mude.\nSELECT i.id AS item_id,\n i.tenant_id,\n i.unit_id,\n f.id AS invoice_id,\n f.number AS invoice_number,\n f.direction,\n f.issue_date,\n f.cancelled_at,\n i.product_id,\n i.name,\n i.description,\n i.quantity,\n i.unit_price,\n i.discount,\n i.total,\n COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,\n COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,\n x.tax_amount,\n x.cfop,\n x.ncm,\n a.from_item_id AS billed_from_order_item_id,\n a.quantity AS billed_quantity\n FROM public.items i\n JOIN public.plg_financial_invoices f ON f.order_id = i.order_id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n LEFT JOIN public.item_associations a ON a.to_item_id = i.id\n WHERE i.invoice_id IS NULL AND i.order_id IS NOT NULL\n -- Only for an invoice with no lines of its own. An invoice billed from\n -- PART of an order (financial_invoice_from_lines) has its own lines, and\n -- without this guard it also listed every line of the order — the whole\n -- sale reported as billed, and summed twice.\n AND NOT EXISTS (SELECT 1 FROM public.items own WHERE own.invoice_id = f.id)\n $view$;\n\n EXECUTE $comment$\nCOMMENT ON VIEW public.v_financial_invoice_lines IS\n 'Linha de fatura com classificação contábil, pelos DOIS caminhos: a linha '\n 'criada no título (items.invoice_id) e a criada na venda (items.order_id = '\n 'plg_financial_invoices.order_id). O segundo é 100% do que o porte do V1 traz '\n '— items_one_parent proíbe um item de ter os dois pais.'\n $comment$;\nEND $guard$;\n\n-- \\`security_invoker\\` precisa sobreviver ao replace: sem ele a view passaria a\n-- rodar com os direitos do dono e furaria a RLS de todo mundo que a lê.\nALTER VIEW public.v_financial_invoice_lines SET (security_invoker = true);\n\nGRANT SELECT ON public.v_financial_invoice_lines TO authenticated, service_role;\n`\n\nexport const MIGRATION_039_O_TITULO_DE_UMA_VENDA_CANCELADA_NASCE_CANCELADO = `-- O título de uma venda cancelada nasce cancelado.\n--\n-- Uma venda do V1 tem DOIS destinos no V2: a Venda em \\`public.orders\\` e o TÍTULO\n-- em \\`public.plg_financial_invoices\\`, criado pelo \\`extra\\` do writer \\`order\\`. O\n-- cancelamento atravessa para o primeiro e **não** para o segundo:\n--\n-- V1 invoices #177530 status = 'canceled'\n-- → public.orders status = 'cancelled' ✅\n-- → plg_financial_invoices cancelled_at = NULL ❌\n--\n-- O resultado é pior que perder a linha: o título cancelado **conta como\n-- ativo**. Medido em 18/09/2026 no tenant Pertinho do Céu: 8 títulos de venda\n-- cancelada, **R$ 24.362,90** somando no faturamento e no DRE como se fossem\n-- receita viva. Uma linha ausente alguém procura; uma linha a mais ninguém\n-- confere.\n--\n-- Foi a asserção de paridade que achou: o DRE do V2 dava R$ 12.000 A MAIS que o\n-- do V1 na conta 8.2.5 de agosto, e o destino ter mais que a origem só pode ser\n-- mês deslocado ou dado que não deveria estar lá.\n--\n-- ── Por que um gatilho, e não um conserto no writer ──────────────────────────\n--\n-- O caminho óbvio seria fazer \\`upsert_order\\` carimbar o título junto. Não basta,\n-- e por uma razão que vale além da migração: o cancelamento de uma venda também\n-- acontece DEPOIS, pelo app, e nesse caminho o writer da migração nem roda. A\n-- regra \"título de venda cancelada está cancelado\" é do modelo, não do\n-- transporte — então ela mora onde o modelo mora.\n--\n-- O gatilho é \\`AFTER UPDATE OF status\\`, não \\`AFTER UPDATE\\`: uma venda recebe\n-- escrita o tempo todo (total, notas, unidade) e disparar em todas faria dele um\n-- custo em cada uma delas.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. O que já entrou errado\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- \\`updated_at\\` da venda, e não \\`now()\\`: o carimbo tem que dizer quando a venda\n-- foi cancelada, não quando esta migration rodou. Um \\`now()\\` aqui colocaria\n-- todos os cancelamentos históricos na data do deploy, e qualquer relatório por\n-- período passaria a mentir a partir de hoje.\nUPDATE public.plg_financial_invoices fi\n SET cancelled_at = coalesce(\n nullif(o.metadata #>> '{cancellation,at}', '')::timestamptz,\n o.updated_at),\n cancel_reason = coalesce(fi.cancel_reason, o.metadata #>> '{cancellation,reason}')\n FROM public.orders o\n WHERE o.id = fi.order_id\n AND o.status = 'cancelled'\n AND fi.cancelled_at IS NULL;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O que entrar daqui para a frente\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION public.trg_order_cancels_its_invoice()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\nBEGIN\n IF NEW.status = 'cancelled' AND coalesce(OLD.status, '') <> 'cancelled' THEN\n UPDATE public.plg_financial_invoices\n SET cancelled_at = coalesce(\n nullif(NEW.metadata #>> '{cancellation,at}', '')::timestamptz, now()),\n cancel_reason = coalesce(cancel_reason, NEW.metadata #>> '{cancellation,reason}')\n WHERE order_id = NEW.id AND cancelled_at IS NULL;\n -- Descancelar é reabrir: se a venda volta, o título volta com ela. Sem esta\n -- perna o estorno de um cancelamento deixaria o título morto para sempre, e o\n -- dinheiro sumiria do relatório sem ninguém ter pedido.\n ELSIF coalesce(OLD.status, '') = 'cancelled' AND NEW.status <> 'cancelled' THEN\n UPDATE public.plg_financial_invoices\n SET cancelled_at = NULL, cancel_reason = NULL\n WHERE order_id = NEW.id;\n END IF;\n RETURN NULL;\nEND $function$;\n\nDROP TRIGGER IF EXISTS orders_cancel_reaches_the_invoice ON public.orders;\nCREATE TRIGGER orders_cancel_reaches_the_invoice\n AFTER UPDATE OF status ON public.orders\n FOR EACH ROW EXECUTE FUNCTION public.trg_order_cancels_its_invoice();\n\nCOMMENT ON FUNCTION public.trg_order_cancels_its_invoice() IS\n 'Cancelar a venda cancela o título dela, e descancelar reabre. O porte do V1 '\n 'levava o cancelamento para public.orders e não para plg_financial_invoices — '\n '8 títulos, R$ 24.362,90, contavam como receita viva.';\n`\n\nexport const MIGRATION_040_A_DRE_LE_A_LINHA_DA_VENDA = `-- A DRE lê a linha da venda.\n--\n-- O relatório que o V1 tem e o V2 não tinha. Ele existe agora porque o DADO\n-- passou a bater: \\`make parity CLIENT=pertinho-do-ceu DE=2026-01-01\n-- ATE=2026-12-31\\` fecha em 376 de 376 chaves (conta contábil × mês), com\n-- A = R$ 2.181.543,30 e B = R$ 2.181.543,29. Construir a DRE antes disso seria\n-- dar forma bonita a um número errado.\n--\n-- ── De onde cada número sai, e por quê ───────────────────────────────────────\n--\n-- A DRE do V1 é montada sobre \\`invoices × invoice_items\\`, classificando pelo\n-- **item** e não pelo movimento — dois itens da mesma venda podem cair em ramos\n-- diferentes. Ela NUNCA lê \\`financial_movements\\`: parcela e baixa não importam,\n-- o lançamento conta inteiro no mês da competência.\n--\n-- O caminho equivalente no V2 é este, e não outro:\n--\n-- public.items → public.orders → public.plg_financial_invoices\n--\n-- **Não** é \\`items.invoice_id\\`. O CHECK \\`items_one_parent\\` exige exatamente um\n-- pai (\\`order_id\\` XOR \\`invoice_id\\` XOR …), e o item que nasce numa venda tem\n-- \\`order_id\\`. Ligar pelo \\`invoice_id\\` devolve zero linha para todo dado migrado\n-- — foi o que aconteceu com \\`v_financial_invoice_lines\\` antes da migration 032.\n--\n-- A competência é \\`plg_financial_invoices.competence_date\\`, que é onde a\n-- \\`invoices.invoice_date\\` do V1 foi parar. A direção é \\`fi.direction\\`\n-- (receivable | payable), que o writer \\`order\\` já grava certo — não é preciso\n-- deduzir nada de \\`orders.direction\\`, que vem quase sempre nula.\n--\n-- ── O que esta view NÃO reproduz ─────────────────────────────────────────────\n--\n-- Três defeitos do DRE do V1, provados no código dele:\n--\n-- 1. subtrai o desconto DUAS vezes — \\`invoice_items.total_amount\\` já é líquido\n-- e a linha \"(−) Descontos\" desconta de novo;\n-- 2. deixa passar nota \\`'canceled'\\` (1 L) pelo filtro de \\`'cancelled'\\` (2 L);\n-- 3. soma item cancelado individualmente (não filtra \\`canceled_at\\`).\n--\n-- Aqui \\`cancelled_at\\` é filtrado nos dois níveis e o desconto não é subtraído\n-- duas vezes. Ou seja: a view bate com o DADO do V1, e vai divergir da TELA do\n-- V1 exatamente na medida desses três. É de propósito, e é o motivo de a\n-- paridade comparar dado e não tela.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.plg_financial_rep_dre;\n\nCREATE VIEW public.plg_financial_rep_dre AS\nSELECT\n fi.tenant_id,\n fi.unit_id,\n -- Primeiro dia do mês: é assim que \\`v_financial_trial_balance\\` já expressa\n -- competência, e duas convenções de mês no mesmo plugin fariam um relatório\n -- não casar com o outro por um dia.\n date_trunc('month', fi.competence_date)::date AS competence_month,\n fi.direction,\n coa.id AS account_id,\n coa.code AS account_code,\n coa.name AS account_name,\n coa.account_type,\n coa.parent_id AS account_parent_id,\n -- A linha da DRE que não tem conta. O relatório do V1 chama de \"sem\n -- classificação\" e ela domina a tela dele; mantê-la como uma linha de\n -- verdade — em vez de simplesmente não aparecer — é o que deixa a soma da\n -- árvore fechar com o cabeçalho.\n (coa.id IS NULL) AS unclassified,\n count(*) AS line_count,\n sum(it.total) AS amount,\n sum(it.discount) AS discount,\n sum(it.quantity) AS quantity\n FROM public.items it\n JOIN public.orders o ON o.id = it.order_id\n JOIN public.plg_financial_invoices fi ON fi.order_id = o.id\n LEFT JOIN public.plg_financial_chart_of_accounts coa\n ON coa.id = it.chart_account_id\n AND coa.tenant_id = fi.tenant_id\n WHERE fi.cancelled_at IS NULL\n AND it.cancelled_at IS NULL\n GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10;\n\nCOMMENT ON VIEW public.plg_financial_rep_dre IS\n 'DRE: soma das linhas de venda por conta contábil × mês de competência × '\n 'direção. Liga o item ao título PELO PEDIDO (items_one_parent proíbe o item '\n 'de ter os dois pais). Confere com o V1 em 376 de 376 chaves.';\n\nALTER VIEW public.plg_financial_rep_dre SET (security_invoker = true);\nGRANT SELECT ON public.plg_financial_rep_dre TO authenticated, service_role;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- O motor de analytics só alcança o que está na allowlist\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- Uma view fora de \\`analytics_read_models\\` é inalcançável pelo \\`analytics_run\\`,\n-- por melhor que seja o card. Registrar aqui, junto com a view, é o que impede\n-- as duas coisas de divergirem — a view sem registro é um relatório que abre\n-- vazio e não diz por quê.\nDO $$\nBEGIN\n IF to_regclass('public.analytics_read_models') IS NULL THEN\n RAISE NOTICE 'financial 034: motor de analytics não instalado — read model não registrado';\n RETURN;\n END IF;\n INSERT INTO public.analytics_read_models (name, date_column, tenant_column, description)\n VALUES ('plg_financial_rep_dre', 'competence_month', 'tenant_id',\n 'Receitas e despesas por plano de contas e mês de competência, da linha da venda.')\n ON CONFLICT (name) DO UPDATE\n SET date_column = EXCLUDED.date_column,\n tenant_column = EXCLUDED.tenant_column,\n description = EXCLUDED.description;\nEND $$;\n`\n\nexport const MIGRATION_041_O_CAIXA_DO_V1_NAO_SEGURA_O_MOVIMENTO = `-- O caixa do V1 não segura o movimento.\n--\n-- \\`financial_movements.cash_session_id\\` estava no de-para como REFERÊNCIA DURA:\n--\n-- 'cash_session_id' → 'cash_session_id@cash_register_sessions'\n--\n-- \\`cash_register_sessions\\` nunca entrou na allowlist. Toda linha que cita uma\n-- sessão cai em quarentena \\`fk\\` — e não é a sessão que fica de fora, é o\n-- DINHEIRO. Medido em 19/09/2026 no tenant Pertinho do Céu:\n--\n-- movimentos presos por \\`cash_session_id\\` 24\n-- contas a pagar que sumiam da lista por isso ≈ R$ 2.319,46\n-- sessões que os prendiam 2\n--\n-- Duas sessões de conferência de gaveta seguravam dois mil reais de contas a\n-- pagar fora do relatório. E, medido na origem, essas duas sessões não são\n-- operação: uma abriu em 27/05 e fechou em 06/08 com diferença de R$ 20, a\n-- outra está aberta desde 18/09. Não há fechamento de caixa acontecendo nesse\n-- tenant — há uma tela que alguém abriu duas vezes.\n--\n-- ── Por que metadata, e não portar a sessão ──────────────────────────────────\n--\n-- Portar \\`cash_register_sessions\\` resolveria a quarentena e criaria dois\n-- registros de conferência de caixa que ninguém conferiu, num plugin onde\n-- \\`v_financial_cash_session_expected\\` e \\`v_financial_cash_session_review\\` os\n-- mostrariam como operação real. O V2 passaria a ter um caixa que o negócio não\n-- tem.\n--\n-- A sessão vira o que ela é aqui: um FATO da linha de origem, guardado onde os\n-- outros fatos sem destino já moram —\n--\n-- 'cash_session_id' → 'jsonb:v1_cash_session_ref'\n--\n-- O id do V1 continua na linha, consultável, e quando o fechamento de caixa for\n-- portar de verdade ele é a chave que liga os dois lados. O que muda é que ele\n-- deixa de ser uma condição para o movimento existir.\n--\n-- Esta é a mesma decisão já tomada para \\`bank_account_id\\`\n-- (\\`jsonb:v1_bank_account_ref\\`) na mesma tabela: referência que o V2 ainda não\n-- alcança é registro, não trava.\n--\n-- Idempotente.\n\n-- The row is already frozen, and trg_allowlist_frozen refuses a plain write.\n-- A migration file is the one writer the hatch exists for (same idiom as\n-- 033/035); SET LOCAL so the unlock dies with this transaction.\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'financial_movements', 'public.plg_financial_movements', 'financial',\n 'fin_movement', '{}'::jsonb, ARRAY['amount', 'paid_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'type', 'jsonb:v1.type',\n 'notes', 'notes',\n 'amount', 'amount',\n 'status', 'status',\n 'barcode', 'jsonb:barcode',\n 'due_date', 'due_date',\n 'direction', 'direction',\n 'card_brand', 'jsonb:card.brand',\n 'invoice_id', 'financial_invoice_id@invoices:public.plg_financial_invoices',\n 'external_id', 'external_id',\n 'paid_amount', 'paid_amount',\n 'confirmed_at', 'confirmed_at',\n 'is_confirmed', 'jsonb:confirmed',\n 'payment_date', 'paid_at',\n 'movement_type', 'movement_kind',\n 'order_guest_id', '-',\n 'bank_account_id', 'jsonb:v1_bank_account_ref',\n -- A mudança desta migration, e a única.\n 'cash_session_id', 'jsonb:v1_cash_session_ref',\n 'external_source', 'external_source',\n 'debit_account_id', 'debit_account_id@account_central',\n 'card_installments', 'jsonb:card.installments',\n 'credit_account_id', 'credit_account_id@account_central',\n 'payment_method_id', 'payment_method_id@payment_methods',\n 'installment_number', 'installment_number',\n 'parent_movement_id', 'parent_movement_id@financial_movements',\n 'payment_method_type_id','payment_method_type_id@payment_method_types'\n ),\n 'PRD 05 R7/R13: append-only two-leg movements (debit + credit account). Never '\n 'overwritten. Paid invoices without movements receive an identifiable synthetic '\n 'payment (Crédito Legado, R14) — the extractor emits it, the destination records '\n 'it as a normal row. \\`cash_session_id\\` é REGISTRO (v1_cash_session_ref), não '\n 'referência: o fechamento de caixa do V1 não é operação real neste porte e não '\n 'pode segurar o dinheiro fora do relatório.'\n);\n`\n\nexport const MIGRATION_042_O_CANCELADO_DO_V1_E_O_CANCELLED_DO_V2 = `-- O \\`canceled\\` do V1 é o \\`cancelled\\` do V2.\n--\n-- \\`upsert_fin_movement\\` traduz o vocabulário de \\`movement_kind\\` com um CASE de\n-- dez linhas — conta, título, pagamento, recebimento — e entrega \\`status\\` CRU:\n--\n-- coalesce(p_row ->> 'status', 'pending')\n--\n-- O V1 escreve \\`canceled\\` (1 L). O plugin inteiro do V2 pergunta\n-- \\`status <> 'cancelled'\\` (2 L) — em \\`v_financial_cash_movements\\`, no balancete,\n-- no saldo da sessão, no extrato. Uma letra, e o movimento cancelado passa por\n-- vivo em toda tela que o filtra pelo status.\n--\n-- Medido em 19/09/2026 no tenant Pertinho do Céu:\n--\n-- contas canceladas contando como vivas 8 R$ 24.362,90\n-- pagamentos cancelados contando como vivos 6 R$ 14.905,67\n--\n-- São as mesmas 8 vendas canceladas da migration 033 — lá o que faltava era o\n-- carimbo no TÍTULO, aqui é a grafia no MOVIMENTO. O mesmo dinheiro achou dois\n-- caminhos diferentes para contar duas vezes, e nenhum dos dois aparecia numa\n-- conferência de contagem de linhas: as 14 linhas atravessaram, inteiras, com o\n-- valor certo.\n--\n-- ── O que esta migration NÃO consegue consertar, e por quê ───────────────────\n--\n-- Os 6 PAGAMENTOS ficam como estão. \\`trg_plg_financial_movements_append_only\\`\n-- deixa a conta (\\`bill\\`) mover colunas derivadas e recusa qualquer mudança de\n-- status num evento de dinheiro:\n--\n-- the status of money event % never changes — a reversal is a new movement\n--\n-- A regra está certa e não é para ser contornada aqui: no modelo do V2, um\n-- pagamento que não aconteceu se desfaz com um ESTORNO — outro movimento, em\n-- sentido contrário — e não apagando o primeiro. Fazer isso pela migração\n-- criaria 6 lançamentos que o V1 não tem, e o espelho deixaria de ser espelho.\n-- Fica registrado aqui, com o valor, para decidir com quem é do negócio.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. O que já entrou com a grafia da origem\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- Só \\`bill\\` e \\`commission\\`: é exatamente o que o gatilho admite, e tentar o\n-- resto abortaria a migration inteira por causa das linhas que ele protege.\nUPDATE public.plg_financial_movements\n SET status = 'cancelled'\n WHERE status IN ('canceled', 'cancelado', 'cancelada')\n AND movement_kind IN ('bill', 'commission');\n\nDO $$\nDECLARE v_n integer; v_soma numeric;\nBEGIN\n SELECT count(*), coalesce(sum(amount), 0) INTO v_n, v_soma\n FROM public.plg_financial_movements\n WHERE status IN ('canceled', 'cancelado', 'cancelada');\n IF v_n > 0 THEN\n RAISE NOTICE 'financial 036: % evento(s) de dinheiro (R$ %) seguem com a grafia da origem — o append-only recusa mudar status de pagamento; o desfazimento deles e um ESTORNO, nao uma edicao', v_n, v_soma;\n END IF;\nEND $$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O que entrar daqui para a frente\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- A tradução mora ao lado da que já existe para \\`movement_kind\\`, e pela mesma\n-- razão: o de-para transporta a palavra da origem, e quem sabe o vocabulário do\n-- destino é o destino. Valor desconhecido passa INTEIRO — status não é\n-- identidade, e recusar o movimento por causa de uma palavra nova perderia o\n-- dinheiro para proteger um rótulo.\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_movement(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'movement_kind', ''))), '');\n v_kind text := CASE v_kind_raw\n WHEN 'bill' THEN 'bill' WHEN 'payment' THEN 'payment'\n WHEN 'transfer' THEN 'transfer' WHEN 'commission' THEN 'commission'\n -- V1's vocabulary\n WHEN 'conta' THEN 'bill' WHEN 'titulo' THEN 'bill' WHEN 'título' THEN 'bill'\n WHEN 'pagamento' THEN 'payment' WHEN 'recebimento' THEN 'payment'\n WHEN 'transferencia' THEN 'transfer' WHEN 'transferência' THEN 'transfer'\n WHEN 'comissao' THEN 'commission' WHEN 'comissão' THEN 'commission'\n END;\n v_status_raw text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n -- A mudança desta migration. Mesma forma do CASE de \\`movement_kind\\`, três\n -- linhas acima: o V1 fala uma língua, o V2 fala outra, e a tradução é aqui.\n v_status text := CASE v_status_raw\n WHEN 'canceled' THEN 'cancelled'\n WHEN 'cancelado' THEN 'cancelled'\n WHEN 'cancelada' THEN 'cancelled'\n WHEN 'pago' THEN 'paid'\n WHEN 'paga' THEN 'paid'\n WHEN 'pendente' THEN 'pending'\n WHEN 'vencido' THEN 'overdue'\n WHEN 'vencida' THEN 'overdue'\n WHEN 'atrasado' THEN 'overdue'\n WHEN 'parcial' THEN 'partial'\n ELSE coalesce(v_status_raw, 'pending')\n END;\n v_dir text := nullif(btrim(lower(coalesce(p_row ->> 'direction', ''))), '');\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_legs record;\n v_debit uuid := (p_row ->> 'debit_account_id')::uuid;\n v_credit uuid := (p_row ->> 'credit_account_id')::uuid;\n v_exists boolean;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a movement needs an id');\n END IF;\n IF v_kind IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown movement kind %L (expected bill|payment|transfer|commission, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));\n END IF;\n IF v_dir IS NOT NULL AND v_dir NOT IN ('debit', 'credit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown direction %L (expected debit|credit)', v_dir));\n END IF;\n -- A V1 pool that does not carry the column still knows the direction, through\n -- the invoice the movement settles: a receivable is money in, a payable money\n -- out. That is a derivation, not a guess. When there is no invoice either,\n -- refuse — \"which way did this money go\" is not something to default.\n IF v_dir IS NULL AND (p_row ->> 'financial_invoice_id') IS NOT NULL THEN\n SELECT CASE t.direction WHEN 'receivable' THEN 'credit' WHEN 'payable' THEN 'debit' END INTO v_dir\n FROM public.plg_financial_invoices t WHERE t.id = (p_row ->> 'financial_invoice_id')::uuid;\n END IF;\n IF v_dir IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'the movement carries no direction and settles no invoice — which way the money went cannot be derived, and must not be defaulted');\n END IF;\n IF v_amount < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a money event cannot carry a negative amount — direction is the direction, not the sign');\n END IF;\n\n SELECT EXISTS (SELECT 1 FROM public.plg_financial_movements WHERE id = p_id) INTO v_exists;\n IF v_exists THEN\n -- append-only: a second offer of the same money event is a replay\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'already in the ledger (append-only)');\n END IF;\n\n -- Legs, resolved exactly as the in-place backfill resolves them, so a migrated\n -- pool and an upgraded pool end up with the same postings.\n IF v_debit IS NULL OR v_credit IS NULL THEN\n SELECT * INTO v_legs FROM public.plg_financial_legacy_legs(\n p_tenant, v_kind, v_dir, (p_row ->> 'bank_account_id')::uuid, (p_row ->> 'cash_session_id')::uuid,\n (p_row ->> 'financial_invoice_id')::uuid, v_meta);\n IF v_legs.o_mirror THEN\n -- an obligation mirror, not a money event: legs_check allows it legless\n v_meta := v_meta || jsonb_build_object('legacy_mirror', true);\n v_debit := NULL; v_credit := NULL;\n ELSE\n v_debit := coalesce(v_debit, v_legs.o_debit);\n v_credit := coalesce(v_credit, v_legs.o_credit);\n END IF;\n END IF;\n\n INSERT INTO public.plg_financial_movements\n (id, tenant_id, financial_invoice_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, notes, metadata, debit_account_id, credit_account_id)\n VALUES (p_id, p_tenant, (p_row ->> 'financial_invoice_id')::uuid, v_dir, v_kind, v_amount,\n round(coalesce((p_row ->> 'paid_amount')::numeric, 0), 2),\n v_status,\n -- due_date is NOT NULL on the ledger. A V1 payment has no due date —\n -- it is not an obligation — so it takes the date the money moved, which\n -- is the only date that means anything for it. A bill keeps its own.\n coalesce((p_row ->> 'due_date')::date, (p_row ->> 'paid_at')::date, current_date),\n (p_row ->> 'paid_at')::date,\n (p_row ->> 'installment_number')::integer, p_row ->> 'notes', v_meta, v_debit, v_credit);\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_movements', p_id, jsonb_build_object('reason', 'created')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 3. A baixa de um movimento apagado no V1\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- \\`migration.retire_row\\` desce uma escada — \\`deleted_at\\`, \\`cancelled_at\\`,\n-- \\`is_active\\`, \\`active\\` — e, não achando nenhuma, faz DELETE. Em\n-- \\`plg_financial_movements\\` não há nenhuma das quatro, o DELETE bate no\n-- append-only, e a linha apagada no V1 fica VIVA no V2 para sempre.\n--\n-- O caso concreto: \\`financial_movements #359960\\`, apagado no V1 em 18/09 às\n-- 20:10, R$ 5.439,05 vencendo em 17/09 — aparecia como a maior diferença de\n-- setembro na asserção de contas a pagar, com B maior que A. Dinheiro que o V1\n-- não tem mais e o V2 continuava cobrando.\n--\n-- A política diz onde a escada deve parar nesta tabela: o status é o desligamento.\nINSERT INTO migration.retire_policy (target_table, mode, column_name, value, reason)\nVALUES ('public.plg_financial_movements', 'soft', 'status', 'cancelled',\n 'append-only: o movimento nunca é apagado. A baixa é o status — e vale '\n 'para a conta (bill/commission), que o gatilho deixa mover. Para um '\n 'evento de dinheiro o gatilho recusa, e a recusa vira quarentena com a '\n 'frase certa: o desfazimento de um pagamento é um estorno.')\nON CONFLICT (target_table) DO UPDATE\n SET mode = EXCLUDED.mode, column_name = EXCLUDED.column_name,\n value = EXCLUDED.value, reason = EXCLUDED.reason;\n`\n\nexport const MIGRATION_043_O_MOVIMENTO_SABE_DE_QUE_UNIDADE_ELE_E = `-- O movimento sabe de que unidade ele é.\n--\n-- A tela de Contas a Pagar do V1 filtra por unidade, e o filtro é a primeira\n-- coisa que a pessoa mexe. Está escrito no componente dele\n-- (\\`PayablesList.tsx:341\\`):\n--\n-- const unitColumn = type === 'payable' ? 'debit_unit_id' : 'credit_unit_id'\n-- query = query.eq(unitColumn, Number(filterUnitId))\n--\n-- O de-para não levava nenhuma das duas. Medido em 19/09/2026 no tenant\n-- Pertinho do Céu:\n--\n-- movimentos de setembro no V2 259\n-- com \\`unit_id\\` preenchido 0\n--\n-- O dado atravessou inteiro — o total do tenant bate, e \\`make parity\\` fecha em\n-- R$ 2.186.212,02 dos dois lados. O que não atravessou foi a capacidade de\n-- SEPARAR, e por isso a tela do V2 não conseguia reproduzir a do V1:\n--\n-- V1, setembro, unidade Pertinho do Céu 189 mov R$ 283.507,97\n-- V1, setembro, unidade PC Serviços 70 mov R$ 71.422,29\n-- V2, setembro, sem como separar 259 mov R$ 354.930,26\n--\n-- Um total certo que não se deixa recortar é um relatório que ninguém confere:\n-- quem paga as contas paga as da SUA unidade.\n--\n-- ── Onde a escolha é feita, e por quê lá ─────────────────────────────────────\n--\n-- O V1 tem duas colunas e escolhe pela direção; o V2 tem uma. A ponte é\n-- declarada no extrator (\\`Step.augment\\` de \\`financial_movements\\`), com a mesma\n-- regra do componente:\n--\n-- case when t.type = 'D' then t.debit_unit_id else t.credit_unit_id end\n--\n-- É deriva de de-para, que é exatamente o que \\`augment\\` existe para declarar —\n-- e some sozinha no dia em que a origem unificar as duas colunas. O writer\n-- apenas grava o que chega, resolvido pelo ledger de \\`companies\\`.\n--\n-- Idempotente.\n\n-- The row is already frozen, and trg_allowlist_frozen refuses a plain write.\n-- A migration file is the one writer the hatch exists for (same idiom as\n-- 033/035); SET LOCAL so the unlock dies with this transaction.\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'financial_movements', 'public.plg_financial_movements', 'financial',\n 'fin_movement', '{}'::jsonb, ARRAY['amount', 'paid_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'type', 'jsonb:v1.type',\n 'notes', 'notes',\n 'amount', 'amount',\n 'status', 'status',\n 'barcode', 'jsonb:barcode',\n 'due_date', 'due_date',\n 'direction', 'direction',\n 'card_brand', 'jsonb:card.brand',\n 'invoice_id', 'financial_invoice_id@invoices:public.plg_financial_invoices',\n 'external_id', 'external_id',\n 'paid_amount', 'paid_amount',\n 'confirmed_at', 'confirmed_at',\n 'is_confirmed', 'jsonb:confirmed',\n 'payment_date', 'paid_at',\n 'movement_type', 'movement_kind',\n 'order_guest_id', '-',\n 'bank_account_id', 'jsonb:v1_bank_account_ref',\n 'cash_session_id', 'jsonb:v1_cash_session_ref',\n -- A coluna desta migration. Ela não existe na origem: é derivada das duas\n -- pernas do V1 pelo \\`augment\\` do extrator, com a regra da tela dele.\n 'unit_id', 'unit_id@companies:app.units',\n 'debit_unit_id', 'jsonb:v1.debit_unit_ref',\n 'credit_unit_id', 'jsonb:v1.credit_unit_ref',\n 'external_source', 'external_source',\n 'debit_account_id', 'debit_account_id@account_central',\n 'card_installments', 'jsonb:card.installments',\n 'credit_account_id', 'credit_account_id@account_central',\n 'payment_method_id', 'payment_method_id@payment_methods',\n 'installment_number', 'installment_number',\n 'parent_movement_id', 'parent_movement_id@financial_movements',\n 'payment_method_type_id','payment_method_type_id@payment_method_types'\n ),\n 'PRD 05 R7/R13: append-only two-leg movements (debit + credit account). Never '\n 'overwritten. \\`cash_session_id\\` é REGISTRO (v1_cash_session_ref), não '\n 'referência. \\`unit_id\\` é DERIVADA das duas pernas do V1 pela regra da tela '\n 'dele (D → debit_unit_id, C → credit_unit_id): sem ela o total fecha e não se '\n 'deixa recortar por unidade.'\n);\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- O writer grava a unidade\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- Duas pernas mudam: a coluna entra no INSERT, e quando ela não vem resolvida a\n-- unidade DO TÍTULO responde. A segunda não é um palpite — um movimento quita um\n-- título, e o título já sabe de que unidade é (2.374 dos 2.375 no tenant). Ela é\n-- o que faz o movimento antigo, que atravessou antes desta migration, ganhar a\n-- unidade na próxima oferta sem precisar de um backfill separado.\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_movement(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'movement_kind', ''))), '');\n v_kind text := CASE v_kind_raw\n WHEN 'bill' THEN 'bill' WHEN 'payment' THEN 'payment'\n WHEN 'transfer' THEN 'transfer' WHEN 'commission' THEN 'commission'\n WHEN 'conta' THEN 'bill' WHEN 'titulo' THEN 'bill' WHEN 'título' THEN 'bill'\n WHEN 'pagamento' THEN 'payment' WHEN 'recebimento' THEN 'payment'\n WHEN 'transferencia' THEN 'transfer' WHEN 'transferência' THEN 'transfer'\n WHEN 'comissao' THEN 'commission' WHEN 'comissão' THEN 'commission'\n END;\n v_status_raw text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text := CASE v_status_raw\n WHEN 'canceled' THEN 'cancelled'\n WHEN 'cancelado' THEN 'cancelled'\n WHEN 'cancelada' THEN 'cancelled'\n WHEN 'pago' THEN 'paid'\n WHEN 'paga' THEN 'paid'\n WHEN 'pendente' THEN 'pending'\n WHEN 'vencido' THEN 'overdue'\n WHEN 'vencida' THEN 'overdue'\n WHEN 'atrasado' THEN 'overdue'\n WHEN 'parcial' THEN 'partial'\n ELSE coalesce(v_status_raw, 'pending')\n END;\n v_dir text := nullif(btrim(lower(coalesce(p_row ->> 'direction', ''))), '');\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_invoice uuid := (p_row ->> 'financial_invoice_id')::uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_legs record;\n v_debit uuid := (p_row ->> 'debit_account_id')::uuid;\n v_credit uuid := (p_row ->> 'credit_account_id')::uuid;\n v_exists boolean;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a movement needs an id');\n END IF;\n IF v_kind IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown movement kind %L (expected bill|payment|transfer|commission, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));\n END IF;\n IF v_dir IS NOT NULL AND v_dir NOT IN ('debit', 'credit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown direction %L (expected debit|credit)', v_dir));\n END IF;\n IF v_dir IS NULL AND v_invoice IS NOT NULL THEN\n SELECT CASE t.direction WHEN 'receivable' THEN 'credit' WHEN 'payable' THEN 'debit' END INTO v_dir\n FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n IF v_dir IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'the movement carries no direction and settles no invoice — which way the money went cannot be derived, and must not be defaulted');\n END IF;\n IF v_amount < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a money event cannot carry a negative amount — direction is the direction, not the sign');\n END IF;\n\n -- A unidade do título responde pelo movimento que não trouxe a sua. Não é\n -- palpite: o movimento quita AQUELE título, e o título é de uma unidade só.\n IF v_unit IS NULL AND v_invoice IS NOT NULL THEN\n SELECT t.unit_id INTO v_unit FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n\n SELECT EXISTS (SELECT 1 FROM public.plg_financial_movements WHERE id = p_id) INTO v_exists;\n IF v_exists THEN\n -- append-only: a segunda oferta do mesmo evento é replay. A unidade é a\n -- exceção, e cabe: o gatilho não a protege (não é valor, nem conta, nem\n -- vínculo), e recusá-la deixaria todo movimento já portado sem unidade para\n -- sempre — um backfill eterno em vez de uma segunda passada.\n IF v_unit IS NOT NULL THEN\n UPDATE public.plg_financial_movements\n SET unit_id = v_unit\n WHERE id = p_id AND unit_id IS DISTINCT FROM v_unit;\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'already in the ledger (append-only)');\n END IF;\n\n IF v_debit IS NULL OR v_credit IS NULL THEN\n SELECT * INTO v_legs FROM public.plg_financial_legacy_legs(\n p_tenant, v_kind, v_dir, (p_row ->> 'bank_account_id')::uuid, (p_row ->> 'cash_session_id')::uuid,\n v_invoice, v_meta);\n IF v_legs.o_mirror THEN\n v_meta := v_meta || jsonb_build_object('legacy_mirror', true);\n v_debit := NULL; v_credit := NULL;\n ELSE\n v_debit := coalesce(v_debit, v_legs.o_debit);\n v_credit := coalesce(v_credit, v_legs.o_credit);\n END IF;\n END IF;\n\n INSERT INTO public.plg_financial_movements\n (id, tenant_id, unit_id, financial_invoice_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, notes, metadata, debit_account_id, credit_account_id)\n VALUES (p_id, p_tenant, v_unit, v_invoice, v_dir, v_kind, v_amount,\n round(coalesce((p_row ->> 'paid_amount')::numeric, 0), 2),\n v_status,\n coalesce((p_row ->> 'due_date')::date, (p_row ->> 'paid_at')::date, current_date),\n (p_row ->> 'paid_at')::date,\n (p_row ->> 'installment_number')::integer, p_row ->> 'notes', v_meta, v_debit, v_credit);\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_movements', p_id, jsonb_build_object('reason', 'created')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');\nEND $function$;\n`\n\nexport const MIGRATION_044_A_DRE_NAO_PAGA_PEDAGIO_POR_ITEM = `-- A DRE não paga pedágio por item.\n--\n-- Conserto de um defeito que eu introduzi na 034: o relatório funcionava e\n-- derrubava a página inteira de \\`/analytics\\`.\n--\n-- ── O que estava acontecendo ─────────────────────────────────────────────────\n--\n-- \\`analytics_run_batch\\` roda os cards de uma página em SEQUÊNCIA, num único\n-- request. Um card lento não fica lento sozinho — ele estoura o tempo de todos.\n-- Medido em 19/09/2026 no tenant Pertinho do Céu, card a card:\n--\n-- 12 dos 14 read models < 0,20 s\n-- plg_financial_rep_receivables 4,94 s\n-- plg_financial_rep_dre 15,06 s ← este arquivo\n--\n-- ── Por que 15 segundos para somar 425 linhas ────────────────────────────────\n--\n-- A view tem \\`security_invoker = true\\`, então lê \\`items\\` com a RLS de \\`items\\`.\n-- E a política de \\`items\\` faz DUAS chamadas CORRELACIONADAS por linha:\n--\n-- (SELECT app.has_unit(items.unit_id))\n-- (SELECT app.has_permission('orders.read', items.unit_id))\n--\n-- Correlacionada quer dizer que o planejador não pode içá-la para fora do laço:\n-- ela roda uma vez POR LINHA. O \\`EXPLAIN (ANALYZE, BUFFERS)\\` do simples\n-- \\`count(*) from items\\` mostra o preço:\n--\n-- SubPlan 2 loops=5582 0,208 ms cada\n-- SubPlan 10 loops=5582 0,274 ms cada\n-- Buffers: shared hit=119364 (21 buffers por linha, só de RLS)\n--\n-- E as duas dependem apenas de \\`unit_id\\`, que neste tenant tem DOIS valores\n-- distintos. São 11.164 respostas para duas perguntas.\n--\n-- Não é um problema da DRE: é o preço de varrer uma tabela de fatos inteira sob\n-- essa política. A DRE é só o primeiro relatório do produto que precisa varrer\n-- uma.\n--\n-- ── O conserto, e o que ele preserva ─────────────────────────────────────────\n--\n-- A view deixa de ser \\`security_invoker\\` e passa a carregar a própria cerca —\n-- que é o padrão de view de relatório, e é MAIS explícito, não menos:\n--\n-- WHERE fi.tenant_id = app.current_tenant_id()\n-- AND (fi.unit_id IS NULL OR fi.unit_id = ANY (app.visible_units()))\n--\n-- As duas condições são avaliadas UMA vez cada (são não-correlacionadas: o\n-- planejador as transforma em InitPlan) e no nível do TÍTULO — 2.421 linhas —\n-- em vez do nível do item.\n--\n-- O que continua valendo:\n-- · a cerca de tenant, explícita e obrigatória — sem claim, \\`current_tenant_id()\\`\n-- devolve NULL e a view devolve zero linha;\n-- · o recorte por unidade, com a mesma regra de \\`app.has_unit\\` (escopo ligado\n-- → \\`user_unit_access\\`; desligado → as unidades do tenant);\n-- · a allowlist \\`analytics_read_models\\`, que é o que torna a view alcançável\n-- pelo motor — uma view fora dela é inalcançável por melhor que seja o card.\n--\n-- O que deixa de valer, de propósito: o escopo por DONO (\\`owner_id\\`) do item.\n-- Um demonstrativo de resultado não é um documento por dono — ele é da unidade.\n-- Quem pode ver a DRE da unidade vê a DRE inteira da unidade; quem não pode ver\n-- a unidade não vê linha nenhuma dela.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. O conjunto de unidades visíveis, calculado uma vez\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- Mesma regra de \\`app.has_unit\\`, com uma diferença que é o ponto: não recebe\n-- argumento. Função sem argumento é içada para InitPlan e roda UMA vez por\n-- statement; a per-linha vira um teste de pertinência num array de dois\n-- elementos.\n--\n-- \\`app.current_unit_id()\\` (a unidade fixada no token) entra aqui também, para\n-- que o conjunto já venha estreitado quando houver uma.\nCREATE OR REPLACE FUNCTION app._units_of_member() RETURNS uuid[]\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path TO ''\nAS $$\n SELECT CASE\n WHEN app.unit_scoping_on()\n THEN coalesce((SELECT array_agg(a.unit_id) FROM app.user_unit_access a\n WHERE a.tenant_id = app.current_tenant_id()\n AND a.user_id = auth.uid()), '{}'::uuid[])\n ELSE coalesce((SELECT array_agg(u.id) FROM app.units u\n WHERE u.tenant_id = app.current_tenant_id()), '{}'::uuid[])\n END;\n$$;\n\nCREATE OR REPLACE FUNCTION app.visible_units() RETURNS uuid[]\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path TO ''\nAS $$\n SELECT CASE\n WHEN app.current_unit_id() IS NOT NULL\n THEN CASE WHEN app.current_unit_id() = ANY (app._units_of_member())\n THEN ARRAY[app.current_unit_id()] ELSE '{}'::uuid[] END\n ELSE app._units_of_member()\n END;\n$$;\n\nCOMMENT ON FUNCTION app.visible_units() IS\n 'As unidades que o chamador enxerga, como array. Mesma regra de app.has_unit, '\n 'sem argumento — e é a ausência do argumento que importa: sem ela o predicado '\n 'é correlacionado e roda uma vez por linha.';\n\nGRANT EXECUTE ON FUNCTION app.visible_units() TO authenticated, service_role;\nGRANT EXECUTE ON FUNCTION app._units_of_member() TO authenticated, service_role;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. A view leva a própria cerca\n-- ─────────────────────────────────────────────────────────────────────────────\n\nDROP VIEW IF EXISTS public.plg_financial_rep_dre;\n\nCREATE VIEW public.plg_financial_rep_dre AS\n-- \\`MATERIALIZED\\` não é enfeite. \\`app.current_tenant_id()\\` é STABLE e SECURITY\n-- DEFINER; escrita solta num \\`WHERE\\`, ela cai no *Filter* do plano e o Postgres\n-- a chama UMA VEZ POR LINHA — foi o que sobrou depois de tirar a RLS: 40.928\n-- buffers para varrer 272 blocos. A CTE materializada obriga o planejador a\n-- avaliá-la uma vez e a carregar o resultado como constante, que é a mesma\n-- razão de as políticas de RLS deste schema escreverem\n-- \\`(SELECT app.current_tenant_id())\\` e não \\`app.current_tenant_id()\\`.\nWITH cerca AS MATERIALIZED (\n SELECT app.current_tenant_id() AS tenant_id,\n app.visible_units() AS units\n)\nSELECT\n fi.tenant_id,\n fi.unit_id,\n date_trunc('month', fi.competence_date)::date AS competence_month,\n fi.direction,\n coa.id AS account_id,\n coa.code AS account_code,\n coa.name AS account_name,\n coa.account_type,\n coa.parent_id AS account_parent_id,\n (coa.id IS NULL) AS unclassified,\n count(*) AS line_count,\n sum(it.total) AS amount,\n sum(it.discount) AS discount,\n sum(it.quantity) AS quantity\n FROM cerca\n JOIN public.plg_financial_invoices fi ON fi.tenant_id = cerca.tenant_id\n JOIN public.orders o ON o.id = fi.order_id\n JOIN public.items it ON it.order_id = o.id\n LEFT JOIN public.plg_financial_chart_of_accounts coa\n ON coa.id = it.chart_account_id\n AND coa.tenant_id = fi.tenant_id\n WHERE fi.cancelled_at IS NULL\n AND it.cancelled_at IS NULL\n -- A cerca, no nível do TÍTULO (2.421 linhas) e não do item (5.670), e com o\n -- valor já resolvido pela CTE acima.\n AND (fi.unit_id IS NULL OR fi.unit_id = ANY (cerca.units))\n GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10;\n\nCOMMENT ON VIEW public.plg_financial_rep_dre IS\n 'DRE: soma das linhas de venda por conta contábil × mês de competência × '\n 'direção. Liga o item ao título PELO PEDIDO (items_one_parent proíbe o item '\n 'de ter os dois pais). Confere com o V1 em 376 de 376 chaves. NÃO é '\n 'security_invoker: carrega a própria cerca de tenant e de unidade, porque '\n 'herdar a RLS de \\`items\\` custava uma chamada de função por linha — 15 s para '\n 'somar 425 linhas, e o batch de analytics inteiro junto.';\n\n-- \\`security_invoker\\` fica DESLIGADO de propósito. Explícito, para que um\n-- \\`ALTER ... SET (security_invoker = true)\\` bem-intencionado no futuro não\n-- reintroduza o problema sem que ninguém entenda o que mudou.\nALTER VIEW public.plg_financial_rep_dre SET (security_invoker = false);\nGRANT SELECT ON public.plg_financial_rep_dre TO authenticated, service_role;\n\nDO $$\nBEGIN\n IF to_regclass('public.analytics_read_models') IS NULL THEN\n RAISE NOTICE 'financial 038: motor de analytics não instalado — read model não registrado';\n RETURN;\n END IF;\n INSERT INTO public.analytics_read_models (name, date_column, tenant_column, description)\n VALUES ('plg_financial_rep_dre', 'competence_month', 'tenant_id',\n 'Receitas e despesas por plano de contas e mês de competência, da linha da venda.')\n ON CONFLICT (name) DO UPDATE\n SET date_column = EXCLUDED.date_column,\n tenant_column = EXCLUDED.tenant_column,\n description = EXCLUDED.description;\nEND $$;\n`\n\nexport const MIGRATION_045_A_CONTA_SO_CLASSIFICA_SE_A_NATUREZA_CASA = `-- A conta só classifica se a natureza casa com o sentido.\n--\n-- A tela de DRE do V1 não coloca o item na conta dele só porque ele tem conta.\n-- Está escrito no componente (\\`src/pages/reports/DRE.tsx\\`):\n--\n-- const isRev = it.invoice_type === 'C'\n-- const node = nodeIndex.get(\\`coa-\\${it.account_id}\\`)\n-- if (node && node.section === (isRev ? 'revenue' : 'expense')) { ...coloca }\n-- // senão: cai no balde sintético \"sem classificação\"\n--\n-- Uma despesa apontando para conta de RECEITA não vira receita e não vira\n-- despesa classificada: vira \"Despesas sem classificação\". E isso não é canto\n-- raro. Medido em 19/09/2026 no tenant Pertinho do Céu:\n--\n-- itens em nota D apontando para conta de receita 345\n-- valor R$ 125.990,38\n--\n-- A view devolvia \\`account_code\\` sempre que havia conta, então o demonstrativo\n-- do V2 classificava esses 345 como despesa de uma conta de receita — uma linha\n-- que a tela do V1 não tem, num ramo em que ela não existe.\n--\n-- ── O que muda ───────────────────────────────────────────────────────────────\n--\n-- Duas colunas novas, \\`statement_code\\` e \\`statement_name\\`: o código e o nome da\n-- conta QUANDO a natureza casa com o sentido, e NULL quando não casa. As\n-- colunas antigas continuam, cruas, para quem quiser a classificação sem a\n-- regra do demonstrativo.\n--\n-- A regra mora aqui, em SQL, e não no componente de tela, porque ela é da\n-- CONTABILIDADE e não da apresentação: qualquer outra tela que leia este read\n-- model precisa da mesma resposta.\n--\n-- ── A prova ──────────────────────────────────────────────────────────────────\n--\n-- Reproduzida na origem e no destino, unidade Matriz, exercício 2026:\n--\n-- A (V1) B (V2)\n-- receita sem classif. 5.847,86 5.847,86 ✓\n-- despesa classificada 1.384.146,65 1.360.407,63\n-- despesa sem classif. 231.822,74 231.822,73 ✓\n--\n-- A diferença de R$ 23.739,02 nas despesas classificadas são 7 notas com status\n-- \\`canceled\\` (1 L) que o filtro do V1 — \\`status <> 'cancelled'\\`, 2 L — deixa\n-- passar. Medido: 7 itens, R$ 23.739,00, todos em conta de despesa. O V2 as\n-- exclui por \\`cancelled_at\\`, de propósito, como a migration 034 já declara.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.plg_financial_rep_dre;\n\nCREATE VIEW public.plg_financial_rep_dre AS\nWITH cerca AS MATERIALIZED (\n SELECT app.current_tenant_id() AS tenant_id,\n app.visible_units() AS units\n)\nSELECT\n fi.tenant_id,\n fi.unit_id,\n date_trunc('month', fi.competence_date)::date AS competence_month,\n fi.direction,\n coa.id AS account_id,\n coa.code AS account_code,\n coa.name AS account_name,\n coa.account_type,\n coa.parent_id AS account_parent_id,\n -- A conta pela regra do demonstrativo: presente quando a natureza dela casa\n -- com o sentido do título, ausente quando não casa. O NULL aqui é a\n -- resposta certa — \"esta linha não pertence a nenhum ramo da árvore\" — e é\n -- o que manda o valor para a linha sem classificação, que é onde a tela do\n -- V1 também o coloca.\n CASE WHEN (fi.direction = 'receivable' AND coa.account_type = 'revenue')\n OR (fi.direction = 'payable' AND coa.account_type = 'expense')\n THEN coa.code END AS statement_code,\n CASE WHEN (fi.direction = 'receivable' AND coa.account_type = 'revenue')\n OR (fi.direction = 'payable' AND coa.account_type = 'expense')\n THEN coa.name END AS statement_name,\n (coa.id IS NULL) AS unclassified,\n count(*) AS line_count,\n sum(it.total) AS amount,\n sum(it.discount) AS discount,\n sum(it.quantity) AS quantity\n FROM cerca\n JOIN public.plg_financial_invoices fi ON fi.tenant_id = cerca.tenant_id\n JOIN public.orders o ON o.id = fi.order_id\n JOIN public.items it ON it.order_id = o.id\n LEFT JOIN public.plg_financial_chart_of_accounts coa\n ON coa.id = it.chart_account_id\n AND coa.tenant_id = fi.tenant_id\n WHERE fi.cancelled_at IS NULL\n AND it.cancelled_at IS NULL\n AND (fi.unit_id IS NULL OR fi.unit_id = ANY (cerca.units))\n GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12;\n\nCOMMENT ON VIEW public.plg_financial_rep_dre IS\n 'DRE: soma das linhas de venda por conta contábil × mês de competência × '\n 'direção. \\`statement_code\\` é a conta PELA REGRA DO DEMONSTRATIVO (a natureza '\n 'tem que casar com o sentido); \\`account_code\\` é a classificação crua. Confere '\n 'com o V1 em 376 de 376 chaves. NÃO é security_invoker: carrega a própria '\n 'cerca de tenant e de unidade — herdar a RLS de \\`items\\` custava uma chamada '\n 'de função por linha, 15 s para somar 425 linhas.';\n\nALTER VIEW public.plg_financial_rep_dre SET (security_invoker = false);\nGRANT SELECT ON public.plg_financial_rep_dre TO authenticated, service_role;\n\nDO $$\nBEGIN\n IF to_regclass('public.analytics_read_models') IS NULL THEN\n RAISE NOTICE 'financial 039: motor de analytics não instalado — read model não registrado';\n RETURN;\n END IF;\n INSERT INTO public.analytics_read_models (name, date_column, tenant_column, description)\n VALUES ('plg_financial_rep_dre', 'competence_month', 'tenant_id',\n 'Receitas e despesas por plano de contas e mês de competência, da linha da venda.')\n ON CONFLICT (name) DO UPDATE\n SET date_column = EXCLUDED.date_column,\n tenant_column = EXCLUDED.tenant_column,\n description = EXCLUDED.description;\nEND $$;\n`\n\nexport const MIGRATION_046_O_DEMONSTRATIVO_MOSTRA_O_PLANO_INTEIRO = `-- O demonstrativo mostra o plano inteiro, não só o que teve movimento.\n--\n-- A DRE do V1 desenha a ÁRVORE DO PLANO DE CONTAS e depois derrama os valores\n-- nela. Conta sem lançamento no exercício aparece assim mesmo, com travessão em\n-- todos os meses — está na tela dele, medida em 18/09/2026:\n--\n-- 1 RECEITAS — — — … —\n-- 1.1 Receita Bruta — — — … —\n-- 1.2 Deduções da Receita — — — … —\n-- 5.3 Hora Extra — — — … —\n-- 5.5 FGTS — — — … —\n-- 9.9.99 Quebra de Caixa — — — … —\n--\n-- Esta view fazia o contrário: partia dos LANÇAMENTOS e só conhecia a conta que\n-- algum item citou. O ramo 1 inteiro — as 45 contas de receita do tenant —\n-- simplesmente não existia na tela, e a árvore começava no 2.\n--\n-- A diferença não é cosmética. Um plano de contas é uma DECLARAÇÃO do que o\n-- negócio acompanha; a conta zerada é uma resposta (\"não houve\"), e a conta\n-- ausente é uma pergunta que ninguém fez. Quem lê um demonstrativo precisa\n-- distinguir as duas, e é por isso que o V1 as distingue.\n--\n-- ── Como ────────────────────────────────────────────────────────────────────\n--\n-- A view vira a união de dois ramos:\n--\n-- ESTRUTURA uma linha por conta ativa do plano, com \\`competence_month\\` NULO\n-- e valor zero. É o esqueleto: existe, não teve movimento.\n--\n-- MOVIMENTO o que a view já fazia — a soma dos itens por conta × mês.\n--\n-- \\`competence_month\\` NULO é o que separa os dois, e é deliberado: quem monta o\n-- demonstrativo cria o nó e NÃO cria coluna para uma linha sem mês. Um zero com\n-- mês seria outra coisa — seria afirmar que houve movimento de zero naquele mês.\n--\n-- O esqueleto também conserta um efeito colateral feio: as contas-mãe ('2',\n-- '5.4') quase nunca têm lançamento próprio, então a árvore as inventava só com\n-- o código e a tela mostrava \\`2 2\\`, \\`5.4 5.4\\`. Agora elas vêm do plano, com\n-- nome.\n--\n-- Só \\`revenue\\` e \\`expense\\`. É o mesmo recorte do V1 (\\`DRE.tsx:260\\`): uma conta\n-- de ativo ou passivo não tem linha num demonstrativo de RESULTADO.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.plg_financial_rep_dre;\n\nCREATE VIEW public.plg_financial_rep_dre AS\nWITH cerca AS MATERIALIZED (\n SELECT app.current_tenant_id() AS tenant_id,\n app.visible_units() AS units\n)\n-- ── 1. A estrutura: o plano de contas, com ou sem movimento ─────────────────\nSELECT\n coa.tenant_id,\n NULL::uuid AS unit_id,\n NULL::date AS competence_month,\n CASE coa.account_type WHEN 'revenue' THEN 'receivable' ELSE 'payable' END AS direction,\n coa.id AS account_id,\n coa.code AS account_code,\n coa.name AS account_name,\n coa.account_type,\n coa.parent_id AS account_parent_id,\n coa.code AS statement_code,\n coa.name AS statement_name,\n false AS unclassified,\n 0::bigint AS line_count,\n 0::numeric AS amount,\n 0::numeric AS discount,\n 0::numeric AS quantity\n FROM cerca\n JOIN public.plg_financial_chart_of_accounts coa ON coa.tenant_id = cerca.tenant_id\n WHERE coa.is_active\n AND coa.account_type IN ('revenue', 'expense')\n\nUNION ALL\n\n-- ── 2. O movimento ──────────────────────────────────────────────────────────\nSELECT\n fi.tenant_id,\n fi.unit_id,\n date_trunc('month', fi.competence_date)::date AS competence_month,\n fi.direction,\n coa.id AS account_id,\n coa.code AS account_code,\n coa.name AS account_name,\n coa.account_type,\n coa.parent_id AS account_parent_id,\n -- A conta pela regra do demonstrativo: presente quando a natureza dela casa\n -- com o sentido do título, ausente quando não casa (migration 039).\n CASE WHEN (fi.direction = 'receivable' AND coa.account_type = 'revenue')\n OR (fi.direction = 'payable' AND coa.account_type = 'expense')\n THEN coa.code END AS statement_code,\n CASE WHEN (fi.direction = 'receivable' AND coa.account_type = 'revenue')\n OR (fi.direction = 'payable' AND coa.account_type = 'expense')\n THEN coa.name END AS statement_name,\n (coa.id IS NULL) AS unclassified,\n count(*) AS line_count,\n sum(it.total) AS amount,\n sum(it.discount) AS discount,\n sum(it.quantity) AS quantity\n FROM cerca\n JOIN public.plg_financial_invoices fi ON fi.tenant_id = cerca.tenant_id\n JOIN public.orders o ON o.id = fi.order_id\n JOIN public.items it ON it.order_id = o.id\n LEFT JOIN public.plg_financial_chart_of_accounts coa\n ON coa.id = it.chart_account_id\n AND coa.tenant_id = fi.tenant_id\n WHERE fi.cancelled_at IS NULL\n AND it.cancelled_at IS NULL\n AND (fi.unit_id IS NULL OR fi.unit_id = ANY (cerca.units))\n GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12;\n\nCOMMENT ON VIEW public.plg_financial_rep_dre IS\n 'DRE: a ÁRVORE do plano de contas (linhas com competence_month nulo, valor '\n 'zero — a conta existe e não teve movimento) somada ao MOVIMENTO por conta × '\n 'mês. \\`statement_code\\` é a conta pela regra do demonstrativo (a natureza tem '\n 'que casar com o sentido); \\`account_code\\` é a classificação crua. Confere com '\n 'o V1 em 376 de 376 chaves. NÃO é security_invoker: carrega a própria cerca '\n 'de tenant e de unidade — herdar a RLS de \\`items\\` custava uma chamada de '\n 'função por linha, 15 s para somar 425 linhas.';\n\nALTER VIEW public.plg_financial_rep_dre SET (security_invoker = false);\nGRANT SELECT ON public.plg_financial_rep_dre TO authenticated, service_role;\n\nDO $$\nBEGIN\n IF to_regclass('public.analytics_read_models') IS NULL THEN\n RAISE NOTICE 'financial 040: motor de analytics não instalado — read model não registrado';\n RETURN;\n END IF;\n -- \\`include_undated\\`: as linhas de ESTRUTURA têm \\`competence_month\\` nulo e o\n -- recorte de período as descartaria — a árvore do plano sumiria assim que\n -- alguém escolhesse um mês. Ver a migration 052 do spine.\n INSERT INTO public.analytics_read_models (name, date_column, tenant_column, description, include_undated)\n VALUES ('plg_financial_rep_dre', 'competence_month', 'tenant_id',\n 'Receitas e despesas por plano de contas e mês de competência, com a árvore inteira do plano.',\n true)\n ON CONFLICT (name) DO UPDATE\n SET date_column = EXCLUDED.date_column,\n tenant_column = EXCLUDED.tenant_column,\n description = EXCLUDED.description,\n include_undated = EXCLUDED.include_undated;\nEND $$;\n`\n\nexport const MIGRATION_047_A_CONTA_RECONHECE_A_CONTRAPARTE_NA_SEGUNDA_OFERTA = `-- A conta reconhece a contraparte na segunda oferta.\n--\n-- O extrato de uma PESSOA — profissional, funcionário, fornecedor — é a lista\n-- dos movimentos que citam a conta dela. No V2 esse extrato vinha pela metade.\n-- Medido em 19/09/2026, profissional ALBANEZ EVANI SANTANA DA SILVA\n-- (\\`professionals #655\\`, \\`account_central #654140\\`):\n--\n-- V1 V2\n-- bill pago 22 R$ 11.061,44 0\n-- bill pendente 13 R$ 8.100,70 0\n-- pagamento 20 R$ 8.629,23 20 R$ 8.629,23 ✓\n-- ────────────────────────────────────────────────────────\n-- total 55 R$ 27.791,37 20 R$ 8.629,23\n--\n-- Os pagamentos atravessaram exatos. As CONTAS atravessaram — o valor está\n-- certo, \\`make parity\\` fecha — mas chegaram sem NENHUMA das duas pernas, então\n-- não citam ninguém e não aparecem no extrato de quem devem. No tenant inteiro:\n--\n-- bills sem perna alguma 2.574 R$ 2.224.751,46\n-- payments com as duas pernas 2.018 R$ 1.594.810,09\n--\n-- ── Por que só as contas ─────────────────────────────────────────────────────\n--\n-- Ordem. \\`financial_movements\\` é oferecido antes de \\`account_central\\` terminar\n-- de migrar; na primeira oferta as referências ainda não resolvem e a linha\n-- entra sem pernas. Na segunda oferta elas resolvem — e o writer devolve:\n--\n-- already in the ledger (append-only)\n--\n-- sem olhar para o que chegou. O \\`append-only\\` está certo quanto ao DINHEIRO:\n-- valor, espécie e sentido de um evento não se reescrevem. Ele não deveria\n-- valer para uma perna AUSENTE, e o gatilho da tabela concorda — ele admite\n-- preencher uma perna nula exatamente uma vez:\n--\n-- OR (OLD.debit_account_id IS NOT NULL AND NEW.debit_account_id IS DISTINCT FROM OLD.debit_account_id)\n--\n-- Quem recusava era o writer, não o modelo. Uma perna nula não é um fato: é uma\n-- resposta que ainda não chegou.\n--\n-- ── O que muda ───────────────────────────────────────────────────────────────\n--\n-- Na repetição, a linha que já existe recebe as pernas que vierem resolvidas —\n-- e SÓ as que estiverem vazias. Perna preenchida continua intocável, que é o\n-- que o append-only protege de verdade.\n--\n-- A segunda mudança é a que impede o mesmo buraco de voltar: ao montar as\n-- pernas de um \\`bill\\`, \\`plg_financial_legacy_legs\\` responde \"isto é espelho de\n-- obrigação\" e devolve as duas nulas. O writer aceitava essa resposta mesmo\n-- quando a origem TINHA dado a contraparte, e apagava o que já sabia. Agora ele\n-- só pergunta quando não sabe.\n--\n-- Idempotente. Depois de aplicar, uma passada de \\`financial_movements\\`\n-- preenche o que já está lá.\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_movement(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'movement_kind', ''))), '');\n v_kind text := CASE v_kind_raw\n WHEN 'bill' THEN 'bill' WHEN 'payment' THEN 'payment'\n WHEN 'transfer' THEN 'transfer' WHEN 'commission' THEN 'commission'\n WHEN 'conta' THEN 'bill' WHEN 'titulo' THEN 'bill' WHEN 'título' THEN 'bill'\n WHEN 'pagamento' THEN 'payment' WHEN 'recebimento' THEN 'payment'\n WHEN 'transferencia' THEN 'transfer' WHEN 'transferência' THEN 'transfer'\n WHEN 'comissao' THEN 'commission' WHEN 'comissão' THEN 'commission'\n END;\n v_status_raw text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text := CASE v_status_raw\n WHEN 'canceled' THEN 'cancelled'\n WHEN 'cancelado' THEN 'cancelled'\n WHEN 'cancelada' THEN 'cancelled'\n WHEN 'pago' THEN 'paid'\n WHEN 'paga' THEN 'paid'\n WHEN 'pendente' THEN 'pending'\n WHEN 'vencido' THEN 'overdue'\n WHEN 'vencida' THEN 'overdue'\n WHEN 'atrasado' THEN 'overdue'\n WHEN 'parcial' THEN 'partial'\n ELSE coalesce(v_status_raw, 'pending')\n END;\n v_dir text := nullif(btrim(lower(coalesce(p_row ->> 'direction', ''))), '');\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_invoice uuid := (p_row ->> 'financial_invoice_id')::uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_legs record;\n v_debit uuid := (p_row ->> 'debit_account_id')::uuid;\n v_credit uuid := (p_row ->> 'credit_account_id')::uuid;\n v_exists boolean;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a movement needs an id');\n END IF;\n IF v_kind IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown movement kind %L (expected bill|payment|transfer|commission, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));\n END IF;\n IF v_dir IS NOT NULL AND v_dir NOT IN ('debit', 'credit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown direction %L (expected debit|credit)', v_dir));\n END IF;\n IF v_dir IS NULL AND v_invoice IS NOT NULL THEN\n SELECT CASE t.direction WHEN 'receivable' THEN 'credit' WHEN 'payable' THEN 'debit' END INTO v_dir\n FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n IF v_dir IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'the movement carries no direction and settles no invoice — which way the money went cannot be derived, and must not be defaulted');\n END IF;\n IF v_amount < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a money event cannot carry a negative amount — direction is the direction, not the sign');\n END IF;\n\n IF v_unit IS NULL AND v_invoice IS NOT NULL THEN\n SELECT t.unit_id INTO v_unit FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n\n -- ── As pernas ──────────────────────────────────────────────────────────────\n --\n -- Só PERGUNTA quando não sabe. A origem que entregou a contraparte tem a\n -- palavra final: \\`plg_financial_legacy_legs\\` existe para DEDUZIR a postagem\n -- de um pool que não a tem, não para descartar a que tem.\n IF v_debit IS NULL OR v_credit IS NULL THEN\n SELECT * INTO v_legs FROM public.plg_financial_legacy_legs(\n p_tenant, v_kind, v_dir, (p_row ->> 'bank_account_id')::uuid, (p_row ->> 'cash_session_id')::uuid,\n v_invoice, v_meta);\n IF v_legs.o_mirror THEN\n -- Espelho de obrigação: \\`legs_check\\` o admite sem pernas. Mas o que a\n -- origem deu FICA — apagar a contraparte de uma conta a pagar é apagar a\n -- quem se deve, e é o extrato da pessoa que some junto.\n v_meta := v_meta || jsonb_build_object('legacy_mirror', true);\n ELSE\n v_debit := coalesce(v_debit, v_legs.o_debit);\n v_credit := coalesce(v_credit, v_legs.o_credit);\n END IF;\n END IF;\n\n SELECT EXISTS (SELECT 1 FROM public.plg_financial_movements WHERE id = p_id) INTO v_exists;\n IF v_exists THEN\n -- Repetição. O DINHEIRO não se reescreve — valor, espécie e sentido ficam\n -- como entraram. O que se completa é o que ainda estava vazio: a unidade e\n -- as pernas que a primeira oferta não conseguiu resolver porque\n -- \\`account_central\\` ainda não tinha migrado. O gatilho append-only admite\n -- exatamente isto — preencher uma perna nula uma vez — e recusa reescrever\n -- uma preenchida, que é a trava que importa.\n UPDATE public.plg_financial_movements SET\n unit_id = coalesce(unit_id, v_unit),\n debit_account_id = coalesce(debit_account_id, v_debit),\n credit_account_id = coalesce(credit_account_id, v_credit)\n WHERE id = p_id\n AND ( (unit_id IS NULL AND v_unit IS NOT NULL)\n OR (debit_account_id IS NULL AND v_debit IS NOT NULL)\n OR (credit_account_id IS NULL AND v_credit IS NOT NULL));\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', 'already in the ledger (append-only; empty legs and unit completed)');\n END IF;\n\n INSERT INTO public.plg_financial_movements\n (id, tenant_id, unit_id, financial_invoice_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, notes, metadata, debit_account_id, credit_account_id)\n VALUES (p_id, p_tenant, v_unit, v_invoice, v_dir, v_kind, v_amount,\n round(coalesce((p_row ->> 'paid_amount')::numeric, 0), 2),\n v_status,\n coalesce((p_row ->> 'due_date')::date, (p_row ->> 'paid_at')::date, current_date),\n (p_row ->> 'paid_at')::date,\n (p_row ->> 'installment_number')::integer, p_row ->> 'notes', v_meta, v_debit, v_credit);\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_movements', p_id, jsonb_build_object('reason', 'created')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');\nEND $function$;\n`\n\nexport const MIGRATION_048_A_PARCELA_E_A_CONTA_DO_V1_E_A_BAIXA_E_A_ALOCACAO = `-- A parcela é a conta do V1, e a baixa é a alocação.\n--\n-- Nenhuma conta a pagar aparecia como PAGA no V2. Todas, sem exceção, liam\n-- \"Aberto\" ou \"Vencido\" — inclusive as 22 contas de ALBANEZ que o V1 mostra\n-- quitadas, inclusive R$ 161.301,71 de pagamentos de setembro.\n--\n-- Não era o dado: \\`make parity\\` fecha, e o movimento de conta chega com\n-- \\`status = 'paid'\\` e \\`paid_amount\\` preenchido. Era a CADEIA que a tela lê:\n--\n-- v_financial_invoice_balances o que a lista mostra\n-- → v_financial_installment_balances\n-- → plg_financial_invoice_installments as parcelas\n-- → plg_financial_allocations o que liga pagamento à parcela\n--\n-- E \\`plg_financial_allocations\\` estava VAZIA. O V1 tem 2.021 linhas de\n-- \\`payment_allocations\\`, R$ 1.588.723,76, e a tabela não tinha linha nenhuma na\n-- \\`migration.allowlist\\`. Sem alocação, \\`paid\\` é zero para toda parcela, e o\n-- título inteiro lê como não pago — por mais que o movimento diga o contrário.\n--\n-- ── A ponte, e por que ela é o id ────────────────────────────────────────────\n--\n-- \\`payment_allocations\\` liga PAGAMENTO a CONTA (\\`bill_movement_id\\`); o V2 liga\n-- pagamento a PARCELA (\\`installment_id\\`). Para atravessar, a conta do V1 tem\n-- que virar uma parcela do V2 — e ela É uma: no V1 uma compra em 4× são quatro\n-- linhas de \\`financial_movements\\`, cada uma com o seu vencimento e o seu valor.\n-- É a definição de parcela.\n--\n-- Então a parcela nasce com o MESMO id do movimento de conta. Um para um, sem\n-- tabela de tradução: a alocação resolve \\`bill_movement_id\\` pelo ledger e o\n-- \\`target_id\\` que volta já é o \\`installment_id\\`. Nada a adivinhar e nada a\n-- guardar de lado.\n--\n-- ── A parcela única que já estava lá ─────────────────────────────────────────\n--\n-- O \\`extra\\` do writer de venda cria UMA parcela por título, com o valor cheio.\n-- Para 2.331 das 2.420 faturas do tenant isso está certo — elas têm uma parcela\n-- só. Para as 89 restantes está errado: a fatura #1869 tem 6 parcelas de\n-- R$ 966,67 no V1 e virava uma de R$ 5.800,00.\n--\n-- Quando a conta do V1 chega, ela manda: a parcela dela entra e a parcela\n-- genérica do título sai — reconhecível porque não tem \\`metadata.migration\\`.\n-- Sai só a que ninguém referencia; a que já tem baixa fica, e o problema\n-- aparece em vez de sumir.\n--\n-- Idempotente. Depois de aplicar, \\`mirror-remap financial_movements\\` e uma\n-- passada refazem as parcelas; \\`payment_allocations\\` entra na passada seguinte.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. A conta do V1 vira a parcela do V2\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration._fin_installment_from_bill(\n p_tenant uuid, p_id uuid, p_invoice uuid, p_unit uuid,\n p_number integer, p_due date, p_amount numeric\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE v_number integer := greatest(coalesce(p_number, 1), 1);\nBEGIN\n IF p_invoice IS NULL THEN RETURN; END IF;\n\n -- A parcela genérica do título sai para dar lugar à do V1 — mas só a que não\n -- veio da migração E que ninguém referencia. Uma parcela com baixa fica onde\n -- está: apagá-la levaria a baixa junto (ON DELETE CASCADE) e o dinheiro\n -- sumiria sem ninguém pedir.\n DELETE FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice\n AND i.id <> p_id\n AND NOT (i.metadata ? 'migration')\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations a WHERE a.installment_id = i.id)\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_movements m WHERE m.financial_invoice_installment_id = i.id);\n\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, metadata)\n VALUES (p_id, p_tenant, p_unit, p_invoice, v_number, p_due, greatest(coalesce(p_amount, 0), 0),\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements')))\n ON CONFLICT (id) DO UPDATE\n SET number = EXCLUDED.number,\n due_date = EXCLUDED.due_date,\n amount = EXCLUDED.amount,\n unit_id = coalesce(public.plg_financial_invoice_installments.unit_id, EXCLUDED.unit_id),\n metadata = public.plg_financial_invoice_installments.metadata || EXCLUDED.metadata;\nEXCEPTION\n -- \\`UNIQUE (invoice_id, number)\\`: duas contas do V1 com o mesmo número de\n -- parcela no mesmo título existem — a fatura #1869 tem duas com vencimento\n -- 19/08. Recusar a segunda perderia dinheiro; ela entra no próximo número\n -- livre, que é o que a tela precisa para somar certo.\n WHEN unique_violation THEN\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, metadata)\n SELECT p_id, p_tenant, p_unit, p_invoice,\n coalesce(max(i.number), 0) + 1, p_due, greatest(coalesce(p_amount, 0), 0),\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements', 'renumbered', true))\n FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice\n ON CONFLICT (id) DO NOTHING;\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O writer do movimento cria a parcela e se liga a ela\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_movement(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'movement_kind', ''))), '');\n v_kind text := CASE v_kind_raw\n WHEN 'bill' THEN 'bill' WHEN 'payment' THEN 'payment'\n WHEN 'transfer' THEN 'transfer' WHEN 'commission' THEN 'commission'\n WHEN 'conta' THEN 'bill' WHEN 'titulo' THEN 'bill' WHEN 'título' THEN 'bill'\n WHEN 'pagamento' THEN 'payment' WHEN 'recebimento' THEN 'payment'\n WHEN 'transferencia' THEN 'transfer' WHEN 'transferência' THEN 'transfer'\n WHEN 'comissao' THEN 'commission' WHEN 'comissão' THEN 'commission'\n END;\n v_status_raw text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text := CASE v_status_raw\n WHEN 'canceled' THEN 'cancelled'\n WHEN 'cancelado' THEN 'cancelled'\n WHEN 'cancelada' THEN 'cancelled'\n WHEN 'pago' THEN 'paid'\n WHEN 'paga' THEN 'paid'\n WHEN 'pendente' THEN 'pending'\n WHEN 'vencido' THEN 'overdue'\n WHEN 'vencida' THEN 'overdue'\n WHEN 'atrasado' THEN 'overdue'\n WHEN 'parcial' THEN 'partial'\n ELSE coalesce(v_status_raw, 'pending')\n END;\n v_dir text := nullif(btrim(lower(coalesce(p_row ->> 'direction', ''))), '');\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_invoice uuid := (p_row ->> 'financial_invoice_id')::uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_due date;\n v_legs record;\n v_debit uuid := (p_row ->> 'debit_account_id')::uuid;\n v_credit uuid := (p_row ->> 'credit_account_id')::uuid;\n v_exists boolean;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a movement needs an id');\n END IF;\n IF v_kind IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown movement kind %L (expected bill|payment|transfer|commission, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));\n END IF;\n IF v_dir IS NOT NULL AND v_dir NOT IN ('debit', 'credit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown direction %L (expected debit|credit)', v_dir));\n END IF;\n IF v_dir IS NULL AND v_invoice IS NOT NULL THEN\n SELECT CASE t.direction WHEN 'receivable' THEN 'credit' WHEN 'payable' THEN 'debit' END INTO v_dir\n FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n IF v_dir IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'the movement carries no direction and settles no invoice — which way the money went cannot be derived, and must not be defaulted');\n END IF;\n IF v_amount < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a money event cannot carry a negative amount — direction is the direction, not the sign');\n END IF;\n\n IF v_unit IS NULL AND v_invoice IS NOT NULL THEN\n SELECT t.unit_id INTO v_unit FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n\n v_due := coalesce((p_row ->> 'due_date')::date, (p_row ->> 'paid_at')::date, current_date);\n\n -- Só pergunta as pernas quando não sabe (migration 041).\n IF v_debit IS NULL OR v_credit IS NULL THEN\n SELECT * INTO v_legs FROM public.plg_financial_legacy_legs(\n p_tenant, v_kind, v_dir, (p_row ->> 'bank_account_id')::uuid, (p_row ->> 'cash_session_id')::uuid,\n v_invoice, v_meta);\n IF v_legs.o_mirror THEN\n v_meta := v_meta || jsonb_build_object('legacy_mirror', true);\n ELSE\n v_debit := coalesce(v_debit, v_legs.o_debit);\n v_credit := coalesce(v_credit, v_legs.o_credit);\n END IF;\n END IF;\n\n -- A conta do V1 É a parcela do V2, com o mesmo id.\n IF v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN\n PERFORM migration._fin_installment_from_bill(\n p_tenant, p_id, v_invoice, v_unit,\n (p_row ->> 'installment_number')::integer, v_due, v_amount);\n END IF;\n\n SELECT EXISTS (SELECT 1 FROM public.plg_financial_movements WHERE id = p_id) INTO v_exists;\n IF v_exists THEN\n UPDATE public.plg_financial_movements SET\n unit_id = coalesce(unit_id, v_unit),\n debit_account_id = coalesce(debit_account_id, v_debit),\n credit_account_id = coalesce(credit_account_id, v_credit),\n financial_invoice_installment_id = coalesce(\n financial_invoice_installment_id,\n CASE WHEN v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN p_id END)\n WHERE id = p_id;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', 'already in the ledger (append-only; empty legs, unit and installment completed)');\n END IF;\n\n INSERT INTO public.plg_financial_movements\n (id, tenant_id, unit_id, financial_invoice_id, financial_invoice_installment_id,\n direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, notes, metadata, debit_account_id, credit_account_id)\n VALUES (p_id, p_tenant, v_unit, v_invoice,\n CASE WHEN v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN p_id END,\n v_dir, v_kind, v_amount,\n round(coalesce((p_row ->> 'paid_amount')::numeric, 0), 2),\n v_status, v_due, (p_row ->> 'paid_at')::date,\n (p_row ->> 'installment_number')::integer, p_row ->> 'notes', v_meta, v_debit, v_credit);\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_movements', p_id, jsonb_build_object('reason', 'created')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 3. A baixa: quem pagou o quê\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_allocation(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_payment uuid := (p_row ->> 'payment_movement_id')::uuid;\n v_bill uuid := (p_row ->> 'installment_id')::uuid;\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_unit uuid;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_payment IS NULL OR v_bill IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma baixa precisa do pagamento e da conta que ela quita');\n END IF;\n -- Zero não é baixa. O CHECK da tabela exige \\`amount > 0\\` e recusar aqui, com a\n -- frase, é melhor que o erro do banco no meio do lote.\n IF v_amount <= 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', 'uma baixa de valor zero ou negativo não quita nada');\n END IF;\n\n -- A parcela tem o id do movimento de conta (parte 1 deste arquivo). Se ela\n -- não existe, a conta ainda não atravessou — quarentena de \\`fk\\`, e a segunda\n -- passada resolve. Inventar a parcela aqui criaria dívida que o V1 não tem.\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_invoice_installments i\n WHERE i.id = v_bill AND i.tenant_id = p_tenant) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a conta que esta baixa quita ainda não virou parcela (%s)', v_bill));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_movements m\n WHERE m.id = v_payment AND m.tenant_id = p_tenant) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('o pagamento desta baixa ainda não atravessou (%s)', v_payment));\n END IF;\n\n SELECT m.unit_id INTO v_unit FROM public.plg_financial_movements m WHERE m.id = v_payment;\n\n INSERT INTO public.plg_financial_allocations\n (id, tenant_id, unit_id, payment_movement_id, installment_id, amount, metadata)\n VALUES (p_id, p_tenant, v_unit, v_payment, v_bill, v_amount,\n coalesce(p_row -> 'metadata', '{}'::jsonb))\n ON CONFLICT (payment_movement_id, installment_id) DO UPDATE\n SET amount = EXCLUDED.amount;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_allocations', p_id, jsonb_build_object('reason', 'created')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');\nEND $function$;\n\n-- O vocabulário de writers é fechado por CHECK — de propósito: um writer\n-- escrito errado na allowlist seria uma tabela inteira caindo em silêncio. O\n-- nome novo entra na lista antes de ser usado.\nDO $$\nDECLARE v_def text;\nBEGIN\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n IF v_def IS NULL OR strpos(v_def, 'fin_allocation') > 0 THEN RETURN; END IF;\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''fin_movement''::text',\n '''fin_movement''::text, ''fin_allocation''::text');\nEND $$;\n\n-- A exclusão foi ADIAMENTO, e o motivo dela acabou.\n--\n-- Deferred: V1 links a payment movement to a BILL MOVEMENT; V2 links it to\n-- an INSTALLMENT\n--\n-- Era verdade e era o impedimento certo: sem a ponte, a baixa não tinha para\n-- onde apontar. A ponte existe agora — a conta do V1 é a parcela do V2, com o\n-- mesmo id — então a linha sai da lista de excluídas em vez de continuar lá\n-- como uma decisão que ninguém revisita.\nDELETE FROM migration.excluded_tables WHERE source_table = 'payment_allocations';\n\n-- The row is already frozen, and trg_allowlist_frozen refuses a plain write.\n-- A migration file is the one writer the hatch exists for (same idiom as\n-- 033/035); SET LOCAL so the unlock dies with this transaction.\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'payment_allocations', 'public.plg_financial_allocations', 'financial',\n 'fin_allocation', '{}'::jsonb, ARRAY['amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'payment_movement_id', 'payment_movement_id@financial_movements',\n -- A conta do V1 é a parcela do V2, e as duas têm o mesmo id: resolver pelo\n -- ledger de \\`financial_movements\\` devolve exatamente o \\`installment_id\\`.\n 'bill_movement_id', 'installment_id@financial_movements',\n 'allocated_amount', 'amount',\n 'origin', 'jsonb:v1.origin',\n 'origin_description', 'jsonb:v1.origin_description',\n 'origin_invoice_item_id', 'jsonb:v1.origin_invoice_item_ref',\n 'origin_service_id', 'jsonb:v1.origin_service_ref',\n 'origin_product_id', 'jsonb:v1.origin_product_ref',\n 'created_by', '-'\n ),\n 'A baixa: quanto de QUAL pagamento quitou QUAL parcela. Sem ela toda parcela '\n 'lê \\`paid = 0\\` e todo título aparece em aberto, por mais que o movimento diga '\n 'que foi pago — era R$ 161.301,71 de setembro invisíveis na tela.'\n);\n`\n\nexport const MIGRATION_049_A_PARCELA_CARREGA_O_CANCELAMENTO = `-- A parcela carrega o cancelamento.\n--\n-- Conserto de uma regressão que a migration 042 introduziu, achada pela\n-- asserção de paridade no mesmo dia: depois de portar as baixas, agosto passou\n-- a ter R$ 13.939,00 A MAIS no V2 que no V1.\n--\n-- A (V1) 662.018,65\n-- B (V2) 675.957,65 Δ +13.939,00 em 5 movimentos\n--\n-- As cinco contas estão \\`canceled\\` no V1 e apareceram \\`paid\\` no V2. Elas não\n-- foram inventadas — o valor estava certo e o cancelamento é que sumiu.\n--\n-- ── Quem apagou o cancelamento ───────────────────────────────────────────────\n--\n-- \\`plg_financial_sync_mirror\\` recalcula o estado da conta a partir da PARCELA,\n-- toda vez que uma baixa entra:\n--\n-- status = CASE WHEN v_cancelled THEN 'cancelled'\n-- WHEN v_paid >= v_amount THEN 'paid'\n-- WHEN v_paid > 0 THEN 'partial'\n-- ...\n--\n-- e \\`v_cancelled\\` vem de \\`installment.cancelled_at\\`. A 042 criou a parcela a\n-- partir da conta do V1 e não levou o cancelamento junto: parcela viva, baixa\n-- cheia, conta \"paga\". O gatilho fez exatamente o que devia com a informação\n-- que tinha.\n--\n-- O modelo está certo e o erro era meu. Num título parcelado quem se cancela é\n-- a PARCELA — dá para cancelar a terceira e manter as duas primeiras —, então é\n-- ela que carrega o carimbo, e o movimento é espelho dela. Carimbar o movimento\n-- e deixar a parcela viva é escrever a resposta em dois lugares e deixar um\n-- deles desatualizado.\n--\n-- ── O conserto ───────────────────────────────────────────────────────────────\n--\n-- A parcela nasce cancelada quando a conta do V1 está cancelada, e o writer\n-- chama o sincronizador logo em seguida, para que o espelho acompanhe sem\n-- esperar uma baixa que talvez nunca venha.\n--\n-- Idempotente. Depois de aplicar: \\`mirror-remap financial_movements\\` e uma\n-- passada carimbam as parcelas que já estão lá.\n\nCREATE OR REPLACE FUNCTION migration._fin_installment_from_bill(\n p_tenant uuid, p_id uuid, p_invoice uuid, p_unit uuid,\n p_number integer, p_due date, p_amount numeric, p_cancelled boolean DEFAULT false\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_number integer := greatest(coalesce(p_number, 1), 1);\n -- \\`now()\\` seria a hora desta migration, não a do cancelamento. O V1 não\n -- guarda quando a conta foi cancelada — guarda só que foi —, então o carimbo\n -- fica na data de VENCIMENTO: é a data que a linha já tem e a única que não\n -- inventa um fato novo. Quem precisar do instante exato vai ao V1.\n v_cancel timestamptz := CASE WHEN coalesce(p_cancelled, false)\n THEN coalesce(p_due, current_date)::timestamptz END;\nBEGIN\n IF p_invoice IS NULL THEN RETURN; END IF;\n\n DELETE FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice\n AND i.id <> p_id\n AND NOT (i.metadata ? 'migration')\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations a WHERE a.installment_id = i.id)\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_movements m WHERE m.financial_invoice_installment_id = i.id);\n\n BEGIN\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)\n VALUES (p_id, p_tenant, p_unit, p_invoice, v_number, p_due,\n greatest(coalesce(p_amount, 0), 0), v_cancel,\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements')))\n ON CONFLICT (id) DO UPDATE\n SET number = EXCLUDED.number,\n due_date = EXCLUDED.due_date,\n amount = EXCLUDED.amount,\n -- O cancelamento é da ORIGEM e ela manda nos dois sentidos: uma conta\n -- que voltou a valer no V1 volta a valer aqui.\n cancelled_at = EXCLUDED.cancelled_at,\n unit_id = coalesce(public.plg_financial_invoice_installments.unit_id, EXCLUDED.unit_id),\n metadata = public.plg_financial_invoice_installments.metadata || EXCLUDED.metadata;\n EXCEPTION WHEN unique_violation THEN\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)\n SELECT p_id, p_tenant, p_unit, p_invoice,\n coalesce(max(i.number), 0) + 1, p_due, greatest(coalesce(p_amount, 0), 0), v_cancel,\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements', 'renumbered', true))\n FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice\n ON CONFLICT (id) DO NOTHING;\n END;\n\n -- O espelho acompanha agora, e não na próxima baixa. Uma conta cancelada sem\n -- baixa nenhuma nunca receberia a visita do gatilho — e é justamente ela que\n -- não pode ficar contando como viva.\n PERFORM public.plg_financial_sync_mirror(p_id);\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- O writer passa o cancelamento adiante\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- Uma linha muda no corpo da 042: o oitavo argumento. O resto é igual.\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_movement(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'movement_kind', ''))), '');\n v_kind text := CASE v_kind_raw\n WHEN 'bill' THEN 'bill' WHEN 'payment' THEN 'payment'\n WHEN 'transfer' THEN 'transfer' WHEN 'commission' THEN 'commission'\n WHEN 'conta' THEN 'bill' WHEN 'titulo' THEN 'bill' WHEN 'título' THEN 'bill'\n WHEN 'pagamento' THEN 'payment' WHEN 'recebimento' THEN 'payment'\n WHEN 'transferencia' THEN 'transfer' WHEN 'transferência' THEN 'transfer'\n WHEN 'comissao' THEN 'commission' WHEN 'comissão' THEN 'commission'\n END;\n v_status_raw text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text := CASE v_status_raw\n WHEN 'canceled' THEN 'cancelled'\n WHEN 'cancelado' THEN 'cancelled'\n WHEN 'cancelada' THEN 'cancelled'\n WHEN 'pago' THEN 'paid'\n WHEN 'paga' THEN 'paid'\n WHEN 'pendente' THEN 'pending'\n WHEN 'vencido' THEN 'overdue'\n WHEN 'vencida' THEN 'overdue'\n WHEN 'atrasado' THEN 'overdue'\n WHEN 'parcial' THEN 'partial'\n ELSE coalesce(v_status_raw, 'pending')\n END;\n v_dir text := nullif(btrim(lower(coalesce(p_row ->> 'direction', ''))), '');\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_invoice uuid := (p_row ->> 'financial_invoice_id')::uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_due date;\n v_legs record;\n v_debit uuid := (p_row ->> 'debit_account_id')::uuid;\n v_credit uuid := (p_row ->> 'credit_account_id')::uuid;\n v_exists boolean;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a movement needs an id');\n END IF;\n IF v_kind IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown movement kind %L (expected bill|payment|transfer|commission, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));\n END IF;\n IF v_dir IS NOT NULL AND v_dir NOT IN ('debit', 'credit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown direction %L (expected debit|credit)', v_dir));\n END IF;\n IF v_dir IS NULL AND v_invoice IS NOT NULL THEN\n SELECT CASE t.direction WHEN 'receivable' THEN 'credit' WHEN 'payable' THEN 'debit' END INTO v_dir\n FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n IF v_dir IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'the movement carries no direction and settles no invoice — which way the money went cannot be derived, and must not be defaulted');\n END IF;\n IF v_amount < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a money event cannot carry a negative amount — direction is the direction, not the sign');\n END IF;\n\n IF v_unit IS NULL AND v_invoice IS NOT NULL THEN\n SELECT t.unit_id INTO v_unit FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n\n v_due := coalesce((p_row ->> 'due_date')::date, (p_row ->> 'paid_at')::date, current_date);\n\n IF v_debit IS NULL OR v_credit IS NULL THEN\n SELECT * INTO v_legs FROM public.plg_financial_legacy_legs(\n p_tenant, v_kind, v_dir, (p_row ->> 'bank_account_id')::uuid, (p_row ->> 'cash_session_id')::uuid,\n v_invoice, v_meta);\n IF v_legs.o_mirror THEN\n v_meta := v_meta || jsonb_build_object('legacy_mirror', true);\n ELSE\n v_debit := coalesce(v_debit, v_legs.o_debit);\n v_credit := coalesce(v_credit, v_legs.o_credit);\n END IF;\n END IF;\n\n SELECT EXISTS (SELECT 1 FROM public.plg_financial_movements WHERE id = p_id) INTO v_exists;\n IF NOT v_exists THEN\n INSERT INTO public.plg_financial_movements\n (id, tenant_id, unit_id, financial_invoice_id, financial_invoice_installment_id,\n direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, notes, metadata, debit_account_id, credit_account_id)\n VALUES (p_id, p_tenant, v_unit, v_invoice,\n CASE WHEN v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN p_id END,\n v_dir, v_kind, v_amount,\n round(coalesce((p_row ->> 'paid_amount')::numeric, 0), 2),\n v_status, v_due, (p_row ->> 'paid_at')::date,\n (p_row ->> 'installment_number')::integer, p_row ->> 'notes', v_meta, v_debit, v_credit);\n ELSE\n UPDATE public.plg_financial_movements SET\n unit_id = coalesce(unit_id, v_unit),\n debit_account_id = coalesce(debit_account_id, v_debit),\n credit_account_id = coalesce(credit_account_id, v_credit),\n financial_invoice_installment_id = coalesce(\n financial_invoice_installment_id,\n CASE WHEN v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN p_id END)\n WHERE id = p_id;\n END IF;\n\n -- DEPOIS do movimento existir: a parcela referencia o título e o\n -- sincronizador procura o movimento por \\`financial_invoice_installment_id\\`.\n -- Na 042 isto vinha antes, e no primeiro porte a sincronia não achava nada\n -- para espelhar.\n IF v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN\n PERFORM migration._fin_installment_from_bill(\n p_tenant, p_id, v_invoice, v_unit,\n (p_row ->> 'installment_number')::integer, v_due, v_amount,\n v_status = 'cancelled');\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_movements', p_id, jsonb_build_object('reason', CASE WHEN v_exists THEN 'updated' ELSE 'created' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_exists THEN 'already in the ledger (append-only; legs, unit, installment and cancellation completed)' ELSE 'created' END);\nEND $function$;\n`\n\nexport const MIGRATION_050_O_SALDO_DO_TITULO_SE_CALCULA_UMA_VEZ = `-- O saldo do título se calcula uma vez, não uma vez por título.\n--\n-- \\`v_financial_invoice_balances\\` é a view que a lista de Contas a Pagar lê. Ela\n-- pendurava um \\`LEFT JOIN LATERAL\\` por título sobre\n-- \\`v_financial_installment_balances\\` — e essa view começa com:\n--\n-- WITH paid AS (\n-- SELECT a.installment_id, sum(a.amount)\n-- FROM plg_financial_allocations a JOIN plg_financial_movements m ...\n-- GROUP BY a.installment_id) -- ← TODAS as baixas do pool\n--\n-- Dentro de um LATERAL, esse agregado é refeito para cada título. Enquanto a\n-- tabela de baixas estava vazia ninguém sentiu; no dia em que ela recebeu as\n-- 2.020 baixas do V1 (migration 042), a conta virou quadrática e a tela passou\n-- a responder 500:\n--\n-- canceling statement due to statement timeout\n--\n-- Medido em 19/09/2026, tenant Pertinho do Céu, 2.375 títulos:\n--\n-- página de 50 (o que a tela pede) 0,10 – 0,69 s\n-- contagem exata (o \\`count\\` do PostgREST) 1,55 – 2,76 s\n-- a view inteira estoura\n--\n-- A página passava porque o planejador só avalia o LATERAL das 50 linhas que\n-- sobrevivem ao \\`LIMIT\\`. A contagem não tem limite — e é ela que o PostgREST\n-- pede junto, no mesmo request.\n--\n-- ── O conserto ───────────────────────────────────────────────────────────────\n--\n-- Uma passada. As baixas viram um agregado por parcela; as parcelas viram um\n-- agregado por título; o título junta. Nada é recalculado por linha, e o\n-- resultado é o MESMO — as mesmas colunas, a mesma aritmética, as mesmas\n-- palavras de status.\n--\n-- \\`v_financial_installment_balances\\` fica como está: ela é a granularidade de\n-- parcela e outras telas a leem. O que muda é que o saldo do TÍTULO deixa de\n-- passar por dentro dela.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.v_financial_invoice_balances CASCADE;\n\nCREATE VIEW public.v_financial_invoice_balances AS\n-- O que cada parcela recebeu. Uma vez, para o pool inteiro.\nWITH paid AS (\n SELECT a.installment_id, sum(a.amount) AS paid\n FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE m.status <> 'cancelled' AND m.reversed_by_id IS NULL\n GROUP BY a.installment_id\n),\n-- O estado de cada parcela, com a mesma regra de\n-- \\`v_financial_installment_balances\\` — se as duas divergirem, a lista e o\n-- detalhe passam a contar histórias diferentes sobre a mesma conta.\nparcela AS (\n SELECT i.invoice_id,\n i.amount,\n i.due_date,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'\n WHEN coalesce(p.paid, 0) >= i.amount THEN 'paid'\n WHEN coalesce(p.paid, 0) > 0 THEN 'partial'\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'\n ELSE 'pending'\n END AS status,\n coalesce(p.paid, 0)::numeric AS paid\n FROM public.plg_financial_invoice_installments i\n JOIN public.plg_financial_invoices t ON t.id = i.invoice_id\n LEFT JOIN paid p ON p.installment_id = i.id\n),\n-- E o que o título tem, somando as parcelas dele.\ntitulo AS (\n SELECT invoice_id,\n count(*) FILTER (WHERE status <> 'cancelled') AS installment_count,\n sum(amount) FILTER (WHERE status <> 'cancelled') AS billed,\n sum(paid) AS paid,\n bool_or(status = 'overdue') AS has_overdue,\n min(due_date) FILTER (WHERE status IN ('pending', 'partial', 'overdue')) AS next_due_date,\n max(due_date) FILTER (WHERE status <> 'cancelled') AS last_due_date\n FROM parcela\n GROUP BY invoice_id\n)\nSELECT t.id,\n t.tenant_id,\n t.unit_id,\n t.direction,\n t.number,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n t.order_id,\n t.competence_date,\n t.issue_date,\n t.total,\n COALESCE(ib.billed, 0::numeric)::numeric(14,2) AS billed,\n COALESCE(ib.paid, 0::numeric)::numeric(14,2) AS paid,\n CASE\n WHEN t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(COALESCE(ib.billed, t.total, 0::numeric) - COALESCE(ib.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n CASE\n WHEN t.cancelled_at IS NOT NULL THEN 'cancelled'::text\n WHEN COALESCE(ib.billed, t.total, 0::numeric) = 0::numeric THEN 'paid'::text\n WHEN COALESCE(ib.paid, 0::numeric) >= COALESCE(ib.billed, t.total, 0::numeric) THEN 'paid'::text\n WHEN COALESCE(ib.paid, 0::numeric) > 0::numeric THEN 'partial'::text\n WHEN COALESCE(ib.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n COALESCE(ib.installment_count, 0::bigint)::integer AS installment_count,\n ib.next_due_date,\n ib.last_due_date,\n t.description,\n t.notes,\n t.metadata,\n t.idempotency_key,\n t.cancelled_at,\n t.cancel_reason,\n t.created_by,\n t.created_at,\n t.updated_at\n FROM public.plg_financial_invoices t\n LEFT JOIN titulo ib ON ib.invoice_id = t.id\n LEFT JOIN public.people p ON p.id = t.counterparty_person_id;\n\nCOMMENT ON VIEW public.v_financial_invoice_balances IS\n 'Saldo e estado de cada título, das parcelas dele. UMA passada: as baixas '\n 'agregam por parcela, as parcelas por título. A forma anterior pendurava um '\n 'LATERAL por título sobre uma view que somava TODAS as baixas do pool — '\n 'quadrático, e a lista de contas a pagar passou a dar timeout no dia em que a '\n 'tabela de baixas deixou de estar vazia.';\n\nALTER VIEW public.v_financial_invoice_balances SET (security_invoker = true);\nGRANT SELECT ON public.v_financial_invoice_balances TO authenticated, service_role;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- A view que dependia dela volta igual\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- \\`v_invoice_balances\\` (o saldo por PEDIDO, que \\`plg_financial_rep_receivables\\`\n-- lê) é derrubada pelo CASCADE acima. Corpo extraído do próprio cluster com\n-- \\`pg_get_viewdef\\` e recriado sem uma vírgula de diferença — o que muda debaixo\n-- dela é só a velocidade.\n\nCREATE VIEW public.v_invoice_balances AS\nWITH tb AS (\n SELECT t.order_id,\n min(t.number) AS reference_number,\n min(t.counterparty_person_id::text)::uuid AS party_id,\n min(t.direction) AS flow,\n sum(t.billed) AS amount,\n sum(t.paid) AS paid,\n sum(t.balance) AS balance,\n bool_and(t.status = 'cancelled'::text) AS all_cancelled,\n bool_or(t.status = 'overdue'::text) AS has_overdue,\n min(t.created_at) AS created_at,\n max(t.updated_at) AS updated_at\n FROM v_financial_invoice_balances t\n WHERE t.order_id IS NOT NULL\n GROUP BY t.order_id\n )\n SELECT o.id AS invoice_id,\n o.tenant_id,\n o.kind,\n tb.reference_number,\n tb.party_id,\n CASE\n WHEN tb.flow = 'payable'::text THEN 'debit'::text\n ELSE 'credit'::text\n END AS direction,\n tb.flow,\n COALESCE(tb.amount, 0::numeric) AS amount,\n COALESCE(tb.paid, 0::numeric) AS paid,\n CASE\n WHEN o.status = 'cancelled'::text OR o.stage = 'cancelled'::text OR tb.all_cancelled THEN 0::numeric\n ELSE COALESCE(tb.balance, 0::numeric)\n END AS balance,\n CASE\n WHEN o.status = 'cancelled'::text OR o.stage = 'cancelled'::text OR tb.all_cancelled THEN 'cancelled'::text\n WHEN COALESCE(tb.amount, 0::numeric) = 0::numeric THEN 'open'::text\n WHEN COALESCE(tb.paid, 0::numeric) >= COALESCE(tb.amount, 0::numeric) THEN 'paid'::text\n WHEN COALESCE(tb.paid, 0::numeric) > 0::numeric THEN 'partial'::text\n WHEN COALESCE(tb.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n tb.created_at,\n tb.updated_at\n FROM tb\n JOIN orders o ON o.id = tb.order_id;\n\nALTER VIEW public.v_invoice_balances SET (security_invoker = true);\nGRANT SELECT ON public.v_invoice_balances TO authenticated, service_role;\n`\n\nexport const MIGRATION_051_O_SALDO_SE_CALCULA_SO_NA_LINHA_QUE_A_TELA_MOSTRA = `-- O saldo se calcula só na linha que a tela mostra.\n--\n-- Segunda passada no mesmo problema. A migration 044 achatou\n-- \\`v_financial_invoice_balances\\` em CTEs para matar o custo quadrático — e\n-- matou, para a CONTAGEM. Mas trocou o problema de lugar: uma CTE é\n-- materializada inteira antes de o \\`LIMIT\\` existir, então a consulta que a tela\n-- realmente faz voltou a custar caro.\n--\n-- Medido em 19/09/2026, tenant Pertinho do Céu, 2.375 títulos, com estatísticas\n-- frescas:\n--\n-- antes (044)\n-- contagem exata, sem colunas 1,0 s\n-- select=* order by competence_date limit 50 7,1 s ← a tela\n--\n-- Sete segundos contra um \\`statement_timeout\\` de 8 s no papel \\`authenticated\\`:\n-- a lista abria vazia de vez em quando, sem dizer por quê. A forma achatada\n-- calcula o saldo dos 2.375 títulos para mostrar 50.\n--\n-- ── A terceira forma ────────────────────────────────────────────────────────\n--\n-- Nem o LATERAL original nem as CTEs: LATERAL **correlacionado até o fim**.\n--\n-- O LATERAL de antes era caro porque o que morava dentro dele não era\n-- correlacionado — \\`v_financial_installment_balances\\` soma TODAS as baixas do\n-- pool num \\`WITH\\`, e um agregado sem correlação dentro de um laço é refeito a\n-- cada volta. Aqui cada nível filtra pela linha de fora:\n--\n-- parcelas WHERE i.invoice_id = t.id → índice (invoice_id, number)\n-- baixas WHERE a.installment_id = i.id → índice (installment_id)\n--\n-- Assim o planejador só visita as parcelas do título que sobreviveu ao \\`LIMIT\\`,\n-- e só as baixas daquelas parcelas. Cinquenta títulos × poucas parcelas ×\n-- poucas baixas.\n--\n-- A aritmética e as palavras de status seguem idênticas às da 044 e às de\n-- \\`v_financial_installment_balances\\` — se as três divergirem, a lista, o\n-- detalhe e o relatório passam a contar histórias diferentes sobre a mesma\n-- conta.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.v_financial_invoice_balances CASCADE;\n\nCREATE VIEW public.v_financial_invoice_balances AS\nSELECT t.id,\n t.tenant_id,\n t.unit_id,\n t.direction,\n t.number,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n t.order_id,\n t.competence_date,\n t.issue_date,\n t.total,\n COALESCE(ib.billed, 0::numeric)::numeric(14,2) AS billed,\n COALESCE(ib.paid, 0::numeric)::numeric(14,2) AS paid,\n CASE\n WHEN t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(COALESCE(ib.billed, t.total, 0::numeric) - COALESCE(ib.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n CASE\n WHEN t.cancelled_at IS NOT NULL THEN 'cancelled'::text\n WHEN COALESCE(ib.billed, t.total, 0::numeric) = 0::numeric THEN 'paid'::text\n WHEN COALESCE(ib.paid, 0::numeric) >= COALESCE(ib.billed, t.total, 0::numeric) THEN 'paid'::text\n WHEN COALESCE(ib.paid, 0::numeric) > 0::numeric THEN 'partial'::text\n WHEN COALESCE(ib.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n COALESCE(ib.installment_count, 0::bigint)::integer AS installment_count,\n ib.next_due_date,\n ib.last_due_date,\n t.description,\n t.notes,\n t.metadata,\n t.idempotency_key,\n t.cancelled_at,\n t.cancel_reason,\n t.created_by,\n t.created_at,\n t.updated_at\n FROM public.plg_financial_invoices t\n LEFT JOIN LATERAL (\n SELECT count(*) FILTER (WHERE s.st <> 'cancelled') AS installment_count,\n sum(i.amount) FILTER (WHERE s.st <> 'cancelled') AS billed,\n sum(s.paid) AS paid,\n bool_or(s.st = 'overdue') AS has_overdue,\n min(i.due_date) FILTER (WHERE s.st = ANY (ARRAY['pending','partial','overdue'])) AS next_due_date,\n max(i.due_date) FILTER (WHERE s.st <> 'cancelled') AS last_due_date\n FROM public.plg_financial_invoice_installments i\n -- Correlacionado em \\`i.id\\`: só as baixas DESTA parcela, pelo índice.\n LEFT JOIN LATERAL (\n SELECT sum(a.amount) AS paid\n FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = i.id\n AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL\n ) pa ON true\n CROSS JOIN LATERAL (\n SELECT COALESCE(pa.paid, 0::numeric) AS paid,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'\n WHEN COALESCE(pa.paid, 0::numeric) >= i.amount THEN 'paid'\n WHEN COALESCE(pa.paid, 0::numeric) > 0::numeric THEN 'partial'\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'\n ELSE 'pending'\n END AS st\n ) s\n WHERE i.invoice_id = t.id\n ) ib ON true\n LEFT JOIN public.people p ON p.id = t.counterparty_person_id;\n\nCOMMENT ON VIEW public.v_financial_invoice_balances IS\n 'Saldo e estado de cada título, das parcelas dele. LATERAL correlacionado em '\n 'todos os níveis (parcela por invoice_id, baixa por installment_id), para que '\n 'o cálculo aconteça só nas linhas que a consulta pediu — a forma anterior '\n 'somava os 2.375 títulos para mostrar 50, e a tela levava 7 s contra um '\n 'statement_timeout de 8 s.';\n\nALTER VIEW public.v_financial_invoice_balances SET (security_invoker = true);\nGRANT SELECT ON public.v_financial_invoice_balances TO authenticated, service_role;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- A view que dependia dela volta igual\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- \\`v_invoice_balances\\` (o saldo por PEDIDO, que \\`plg_financial_rep_receivables\\`\n-- lê) é derrubada pelo CASCADE acima. Corpo extraído do próprio cluster com\n-- \\`pg_get_viewdef\\` e recriado sem uma vírgula de diferença — o que muda debaixo\n-- dela é só a velocidade.\n\nCREATE VIEW public.v_invoice_balances AS\nWITH tb AS (\n SELECT t.order_id,\n min(t.number) AS reference_number,\n min(t.counterparty_person_id::text)::uuid AS party_id,\n min(t.direction) AS flow,\n sum(t.billed) AS amount,\n sum(t.paid) AS paid,\n sum(t.balance) AS balance,\n bool_and(t.status = 'cancelled'::text) AS all_cancelled,\n bool_or(t.status = 'overdue'::text) AS has_overdue,\n min(t.created_at) AS created_at,\n max(t.updated_at) AS updated_at\n FROM v_financial_invoice_balances t\n WHERE t.order_id IS NOT NULL\n GROUP BY t.order_id\n )\n SELECT o.id AS invoice_id,\n o.tenant_id,\n o.kind,\n tb.reference_number,\n tb.party_id,\n CASE\n WHEN tb.flow = 'payable'::text THEN 'debit'::text\n ELSE 'credit'::text\n END AS direction,\n tb.flow,\n COALESCE(tb.amount, 0::numeric) AS amount,\n COALESCE(tb.paid, 0::numeric) AS paid,\n CASE\n WHEN o.status = 'cancelled'::text OR o.stage = 'cancelled'::text OR tb.all_cancelled THEN 0::numeric\n ELSE COALESCE(tb.balance, 0::numeric)\n END AS balance,\n CASE\n WHEN o.status = 'cancelled'::text OR o.stage = 'cancelled'::text OR tb.all_cancelled THEN 'cancelled'::text\n WHEN COALESCE(tb.amount, 0::numeric) = 0::numeric THEN 'open'::text\n WHEN COALESCE(tb.paid, 0::numeric) >= COALESCE(tb.amount, 0::numeric) THEN 'paid'::text\n WHEN COALESCE(tb.paid, 0::numeric) > 0::numeric THEN 'partial'::text\n WHEN COALESCE(tb.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n tb.created_at,\n tb.updated_at\n FROM tb\n JOIN orders o ON o.id = tb.order_id;\n\nALTER VIEW public.v_invoice_balances SET (security_invoker = true);\nGRANT SELECT ON public.v_invoice_balances TO authenticated, service_role;\n`\n\nexport const MIGRATION_052_A_LISTA_DE_CONTAS_FALA_VENCIMENTO = `-- A lista de contas fala VENCIMENTO.\n--\n-- A tela de Contas a Pagar do V1 é construída em torno do vencimento: é a\n-- coluna que ela mostra, é por ele que ordena e é ele que o filtro de período\n-- recorta (\\`PayablesList.tsx\\`: \\`.gte('due_date', …)\\`).\n--\n-- A do V2 mostrava a COMPETÊNCIA. As duas datas existem e são diferentes, e a\n-- confusão que isso cria é concreta: sete títulos aparecem no V2 sob a data\n-- 11/09 — a competência deles — enquanto o V1, na mesma data, mostra outros\n-- seis, que são os que VENCEM em 11/09. Comparar as duas telas pelo \"dia 11/09\"\n-- compara conjuntos diferentes, e a conclusão natural é que o porte falhou.\n-- Conferido um a um: os sete estão \\`pending\\` no V1 também.\n--\n-- A competência é do RAZÃO — é o eixo do DRE, e lá ela continua mandando. O\n-- vencimento é da OPERAÇÃO: quem abre esta tela está perguntando o que paga\n-- esta semana.\n--\n-- Esta migration só acrescenta a coluna \\`due_date\\` à view; quem passa a usá-la\n-- é a tela.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.v_financial_invoice_balances CASCADE;\n\nCREATE VIEW public.v_financial_invoice_balances AS\nSELECT t.id,\n t.tenant_id,\n t.unit_id,\n t.direction,\n t.number,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n t.order_id,\n t.competence_date,\n t.issue_date,\n t.total,\n COALESCE(ib.billed, 0::numeric)::numeric(14,2) AS billed,\n COALESCE(ib.paid, 0::numeric)::numeric(14,2) AS paid,\n CASE\n WHEN t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(COALESCE(ib.billed, t.total, 0::numeric) - COALESCE(ib.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n CASE\n WHEN t.cancelled_at IS NOT NULL THEN 'cancelled'::text\n WHEN COALESCE(ib.billed, t.total, 0::numeric) = 0::numeric THEN 'paid'::text\n WHEN COALESCE(ib.paid, 0::numeric) >= COALESCE(ib.billed, t.total, 0::numeric) THEN 'paid'::text\n WHEN COALESCE(ib.paid, 0::numeric) > 0::numeric THEN 'partial'::text\n WHEN COALESCE(ib.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n COALESCE(ib.installment_count, 0::bigint)::integer AS installment_count,\n ib.next_due_date,\n ib.last_due_date,\n -- O VENCIMENTO do título, numa coluna só.\n --\n -- \\`next_due_date\\` é o próximo em aberto e fica NULO quando tudo já foi\n -- pago; \\`last_due_date\\` é o último de todos. Ordenar ou filtrar por\n -- \\`next_due_date\\` sozinho esconderia justamente os títulos quitados — que\n -- é a metade da lista que a pessoa mais confere.\n COALESCE(ib.next_due_date, ib.last_due_date) AS due_date,\n t.description,\n t.notes,\n t.metadata,\n t.idempotency_key,\n t.cancelled_at,\n t.cancel_reason,\n t.created_by,\n t.created_at,\n t.updated_at\n FROM public.plg_financial_invoices t\n LEFT JOIN LATERAL (\n SELECT count(*) FILTER (WHERE s.st <> 'cancelled') AS installment_count,\n sum(i.amount) FILTER (WHERE s.st <> 'cancelled') AS billed,\n sum(s.paid) AS paid,\n bool_or(s.st = 'overdue') AS has_overdue,\n min(i.due_date) FILTER (WHERE s.st = ANY (ARRAY['pending','partial','overdue'])) AS next_due_date,\n max(i.due_date) FILTER (WHERE s.st <> 'cancelled') AS last_due_date\n FROM public.plg_financial_invoice_installments i\n -- Correlacionado em \\`i.id\\`: só as baixas DESTA parcela, pelo índice.\n LEFT JOIN LATERAL (\n SELECT sum(a.amount) AS paid\n FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = i.id\n AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL\n ) pa ON true\n CROSS JOIN LATERAL (\n SELECT COALESCE(pa.paid, 0::numeric) AS paid,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'\n WHEN COALESCE(pa.paid, 0::numeric) >= i.amount THEN 'paid'\n WHEN COALESCE(pa.paid, 0::numeric) > 0::numeric THEN 'partial'\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'\n ELSE 'pending'\n END AS st\n ) s\n WHERE i.invoice_id = t.id\n ) ib ON true\n LEFT JOIN public.people p ON p.id = t.counterparty_person_id;\n\nCOMMENT ON VIEW public.v_financial_invoice_balances IS\n 'Saldo e estado de cada título, das parcelas dele. LATERAL correlacionado em '\n 'todos os níveis (parcela por invoice_id, baixa por installment_id), para que '\n 'o cálculo aconteça só nas linhas que a consulta pediu — a forma anterior '\n 'somava os 2.375 títulos para mostrar 50, e a tela levava 7 s contra um '\n 'statement_timeout de 8 s.';\n\nALTER VIEW public.v_financial_invoice_balances SET (security_invoker = true);\nGRANT SELECT ON public.v_financial_invoice_balances TO authenticated, service_role;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- A view que dependia dela volta igual\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- \\`v_invoice_balances\\` (o saldo por PEDIDO, que \\`plg_financial_rep_receivables\\`\n-- lê) é derrubada pelo CASCADE acima. Corpo extraído do próprio cluster com\n-- \\`pg_get_viewdef\\` e recriado sem uma vírgula de diferença — o que muda debaixo\n-- dela é só a velocidade.\n\nCREATE VIEW public.v_invoice_balances AS\nWITH tb AS (\n SELECT t.order_id,\n min(t.number) AS reference_number,\n min(t.counterparty_person_id::text)::uuid AS party_id,\n min(t.direction) AS flow,\n sum(t.billed) AS amount,\n sum(t.paid) AS paid,\n sum(t.balance) AS balance,\n bool_and(t.status = 'cancelled'::text) AS all_cancelled,\n bool_or(t.status = 'overdue'::text) AS has_overdue,\n min(t.created_at) AS created_at,\n max(t.updated_at) AS updated_at\n FROM v_financial_invoice_balances t\n WHERE t.order_id IS NOT NULL\n GROUP BY t.order_id\n )\n SELECT o.id AS invoice_id,\n o.tenant_id,\n o.kind,\n tb.reference_number,\n tb.party_id,\n CASE\n WHEN tb.flow = 'payable'::text THEN 'debit'::text\n ELSE 'credit'::text\n END AS direction,\n tb.flow,\n COALESCE(tb.amount, 0::numeric) AS amount,\n COALESCE(tb.paid, 0::numeric) AS paid,\n CASE\n WHEN o.status = 'cancelled'::text OR o.stage = 'cancelled'::text OR tb.all_cancelled THEN 0::numeric\n ELSE COALESCE(tb.balance, 0::numeric)\n END AS balance,\n CASE\n WHEN o.status = 'cancelled'::text OR o.stage = 'cancelled'::text OR tb.all_cancelled THEN 'cancelled'::text\n WHEN COALESCE(tb.amount, 0::numeric) = 0::numeric THEN 'open'::text\n WHEN COALESCE(tb.paid, 0::numeric) >= COALESCE(tb.amount, 0::numeric) THEN 'paid'::text\n WHEN COALESCE(tb.paid, 0::numeric) > 0::numeric THEN 'partial'::text\n WHEN COALESCE(tb.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n tb.created_at,\n tb.updated_at\n FROM tb\n JOIN orders o ON o.id = tb.order_id;\n\nALTER VIEW public.v_invoice_balances SET (security_invoker = true);\nGRANT SELECT ON public.v_invoice_balances TO authenticated, service_role;\n`\n\nexport const MIGRATION_053_UM_TITULO_PARCELADO_VENCE_VARIAS_VEZES = `-- Um título com quatro parcelas vence quatro vezes.\n--\n-- Filtrar \"vencimento de 11/09 a 11/09\" no V2 não achava nada, e no V1 achava\n-- seis contas. Não é dado faltando — é a unidade da lista.\n--\n-- O V1 lista PARCELAS: uma compra em 4× são quatro linhas, cada uma com o seu\n-- vencimento, e o filtro casa com a linha. O V2 lista TÍTULOS: a compra é uma\n-- linha só, e uma linha só tem uma data. Das seis contas que vencem em 11/09\n-- no V1, três pertencem a títulos parcelados cujo \\`due_date\\` representativo cai\n-- em outro dia — então o recorte de um dia perdia metade delas.\n--\n-- A pergunta certa para um título não é \"qual é a sua data\" e sim \"alguma\n-- parcela sua cai neste período\". Isso é um teste de SOBREPOSIÇÃO, e ele\n-- precisa das duas pontas:\n--\n-- first_due_date <= fim AND last_due_date >= início\n--\n-- \\`first_due_date\\` é o que faltava. Com as duas, o filtro do dia 11/09 devolve\n-- as seis — e continua sendo uma consulta que o PostgREST expressa sem RPC.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.v_financial_invoice_balances CASCADE;\n\nCREATE VIEW public.v_financial_invoice_balances AS\nSELECT t.id,\n t.tenant_id,\n t.unit_id,\n t.direction,\n t.number,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n t.order_id,\n t.competence_date,\n t.issue_date,\n t.total,\n COALESCE(ib.billed, 0::numeric)::numeric(14,2) AS billed,\n COALESCE(ib.paid, 0::numeric)::numeric(14,2) AS paid,\n CASE\n WHEN t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(COALESCE(ib.billed, t.total, 0::numeric) - COALESCE(ib.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n CASE\n WHEN t.cancelled_at IS NOT NULL THEN 'cancelled'::text\n WHEN COALESCE(ib.billed, t.total, 0::numeric) = 0::numeric THEN 'paid'::text\n WHEN COALESCE(ib.paid, 0::numeric) >= COALESCE(ib.billed, t.total, 0::numeric) THEN 'paid'::text\n WHEN COALESCE(ib.paid, 0::numeric) > 0::numeric THEN 'partial'::text\n WHEN COALESCE(ib.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n COALESCE(ib.installment_count, 0::bigint)::integer AS installment_count,\n ib.next_due_date,\n ib.first_due_date,\n ib.last_due_date,\n -- O VENCIMENTO do título, numa coluna só.\n --\n -- \\`next_due_date\\` é o próximo em aberto e fica NULO quando tudo já foi\n -- pago; \\`last_due_date\\` é o último de todos. Ordenar ou filtrar por\n -- \\`next_due_date\\` sozinho esconderia justamente os títulos quitados — que\n -- é a metade da lista que a pessoa mais confere.\n COALESCE(ib.next_due_date, ib.last_due_date) AS due_date,\n t.description,\n t.notes,\n t.metadata,\n t.idempotency_key,\n t.cancelled_at,\n t.cancel_reason,\n t.created_by,\n t.created_at,\n t.updated_at\n FROM public.plg_financial_invoices t\n LEFT JOIN LATERAL (\n SELECT count(*) FILTER (WHERE s.st <> 'cancelled') AS installment_count,\n sum(i.amount) FILTER (WHERE s.st <> 'cancelled') AS billed,\n sum(s.paid) AS paid,\n bool_or(s.st = 'overdue') AS has_overdue,\n min(i.due_date) FILTER (WHERE s.st = ANY (ARRAY['pending','partial','overdue'])) AS next_due_date,\n min(i.due_date) FILTER (WHERE s.st <> 'cancelled') AS first_due_date,\n max(i.due_date) FILTER (WHERE s.st <> 'cancelled') AS last_due_date\n FROM public.plg_financial_invoice_installments i\n -- Correlacionado em \\`i.id\\`: só as baixas DESTA parcela, pelo índice.\n LEFT JOIN LATERAL (\n SELECT sum(a.amount) AS paid\n FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = i.id\n AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL\n ) pa ON true\n CROSS JOIN LATERAL (\n SELECT COALESCE(pa.paid, 0::numeric) AS paid,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'\n WHEN COALESCE(pa.paid, 0::numeric) >= i.amount THEN 'paid'\n WHEN COALESCE(pa.paid, 0::numeric) > 0::numeric THEN 'partial'\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'\n ELSE 'pending'\n END AS st\n ) s\n WHERE i.invoice_id = t.id\n ) ib ON true\n LEFT JOIN public.people p ON p.id = t.counterparty_person_id;\n\nCOMMENT ON VIEW public.v_financial_invoice_balances IS\n 'Saldo e estado de cada título, das parcelas dele. LATERAL correlacionado em '\n 'todos os níveis (parcela por invoice_id, baixa por installment_id), para que '\n 'o cálculo aconteça só nas linhas que a consulta pediu — a forma anterior '\n 'somava os 2.375 títulos para mostrar 50, e a tela levava 7 s contra um '\n 'statement_timeout de 8 s.';\n\nALTER VIEW public.v_financial_invoice_balances SET (security_invoker = true);\nGRANT SELECT ON public.v_financial_invoice_balances TO authenticated, service_role;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- A view que dependia dela volta igual\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- \\`v_invoice_balances\\` (o saldo por PEDIDO, que \\`plg_financial_rep_receivables\\`\n-- lê) é derrubada pelo CASCADE acima. Corpo extraído do próprio cluster com\n-- \\`pg_get_viewdef\\` e recriado sem uma vírgula de diferença — o que muda debaixo\n-- dela é só a velocidade.\n\nCREATE VIEW public.v_invoice_balances AS\nWITH tb AS (\n SELECT t.order_id,\n min(t.number) AS reference_number,\n min(t.counterparty_person_id::text)::uuid AS party_id,\n min(t.direction) AS flow,\n sum(t.billed) AS amount,\n sum(t.paid) AS paid,\n sum(t.balance) AS balance,\n bool_and(t.status = 'cancelled'::text) AS all_cancelled,\n bool_or(t.status = 'overdue'::text) AS has_overdue,\n min(t.created_at) AS created_at,\n max(t.updated_at) AS updated_at\n FROM v_financial_invoice_balances t\n WHERE t.order_id IS NOT NULL\n GROUP BY t.order_id\n )\n SELECT o.id AS invoice_id,\n o.tenant_id,\n o.kind,\n tb.reference_number,\n tb.party_id,\n CASE\n WHEN tb.flow = 'payable'::text THEN 'debit'::text\n ELSE 'credit'::text\n END AS direction,\n tb.flow,\n COALESCE(tb.amount, 0::numeric) AS amount,\n COALESCE(tb.paid, 0::numeric) AS paid,\n CASE\n WHEN o.status = 'cancelled'::text OR o.stage = 'cancelled'::text OR tb.all_cancelled THEN 0::numeric\n ELSE COALESCE(tb.balance, 0::numeric)\n END AS balance,\n CASE\n WHEN o.status = 'cancelled'::text OR o.stage = 'cancelled'::text OR tb.all_cancelled THEN 'cancelled'::text\n WHEN COALESCE(tb.amount, 0::numeric) = 0::numeric THEN 'open'::text\n WHEN COALESCE(tb.paid, 0::numeric) >= COALESCE(tb.amount, 0::numeric) THEN 'paid'::text\n WHEN COALESCE(tb.paid, 0::numeric) > 0::numeric THEN 'partial'::text\n WHEN COALESCE(tb.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n tb.created_at,\n tb.updated_at\n FROM tb\n JOIN orders o ON o.id = tb.order_id;\n\nALTER VIEW public.v_invoice_balances SET (security_invoker = true);\nGRANT SELECT ON public.v_invoice_balances TO authenticated, service_role;\n`\n\nexport const MIGRATION_054_AS_DATAS_EM_QUE_O_TITULO_VENCE = `-- As datas em que o título vence, todas elas.\n--\n-- A 047 tentou o recorte por SOBREPOSIÇÃO de janela — \\`first_due_date <= fim\\`\n-- e \\`last_due_date >= início\\`. Funciona para um mês; erra para um dia.\n--\n-- Medido: o filtro \"11/09 a 11/09\" trazia PAG-001842, cujas duas parcelas\n-- vencem em 12/08 e 12/09. Nenhuma das duas é 11/09 — mas a janela 12/08→12/09\n-- contém o dia, e a sobreposição de janelas não sabe que entre as pontas não há\n-- nada. Catorze títulos vinham assim.\n--\n-- Uma janela só sabe onde começa e onde termina. A pergunta é outra: \"alguma\n-- parcela vence NESTE dia\", e ela precisa das datas, não dos extremos.\n--\n-- \\`due_dates\\` é o vetor dos vencimentos não cancelados do título. O recorte\n-- vira um teste de interseção de conjuntos (\\`&&\\`), que o PostgREST expressa\n-- direto (\\`due_dates=ov.{…}\\`) e o Postgres resolve sem RPC.\n--\n-- Para um período longo a lista de dias fica grande, e aí a janela volta a\n-- servir: ela erra para mais, nunca para menos, e num recorte de um ano a\n-- diferença some. O corte está no provider.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.v_financial_invoice_balances CASCADE;\n\nCREATE VIEW public.v_financial_invoice_balances AS\nSELECT t.id,\n t.tenant_id,\n t.unit_id,\n t.direction,\n t.number,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n t.order_id,\n t.competence_date,\n t.issue_date,\n t.total,\n COALESCE(ib.billed, 0::numeric)::numeric(14,2) AS billed,\n COALESCE(ib.paid, 0::numeric)::numeric(14,2) AS paid,\n CASE\n WHEN t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(COALESCE(ib.billed, t.total, 0::numeric) - COALESCE(ib.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n CASE\n WHEN t.cancelled_at IS NOT NULL THEN 'cancelled'::text\n WHEN COALESCE(ib.billed, t.total, 0::numeric) = 0::numeric THEN 'paid'::text\n WHEN COALESCE(ib.paid, 0::numeric) >= COALESCE(ib.billed, t.total, 0::numeric) THEN 'paid'::text\n WHEN COALESCE(ib.paid, 0::numeric) > 0::numeric THEN 'partial'::text\n WHEN COALESCE(ib.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n COALESCE(ib.installment_count, 0::bigint)::integer AS installment_count,\n ib.next_due_date,\n ib.first_due_date,\n ib.last_due_date,\n COALESCE(ib.due_dates, ARRAY[]::date[]) AS due_dates,\n -- O VENCIMENTO do título, numa coluna só.\n --\n -- \\`next_due_date\\` é o próximo em aberto e fica NULO quando tudo já foi\n -- pago; \\`last_due_date\\` é o último de todos. Ordenar ou filtrar por\n -- \\`next_due_date\\` sozinho esconderia justamente os títulos quitados — que\n -- é a metade da lista que a pessoa mais confere.\n COALESCE(ib.next_due_date, ib.last_due_date) AS due_date,\n t.description,\n t.notes,\n t.metadata,\n t.idempotency_key,\n t.cancelled_at,\n t.cancel_reason,\n t.created_by,\n t.created_at,\n t.updated_at\n FROM public.plg_financial_invoices t\n LEFT JOIN LATERAL (\n SELECT count(*) FILTER (WHERE s.st <> 'cancelled') AS installment_count,\n sum(i.amount) FILTER (WHERE s.st <> 'cancelled') AS billed,\n sum(s.paid) AS paid,\n bool_or(s.st = 'overdue') AS has_overdue,\n min(i.due_date) FILTER (WHERE s.st = ANY (ARRAY['pending','partial','overdue'])) AS next_due_date,\n min(i.due_date) FILTER (WHERE s.st <> 'cancelled') AS first_due_date,\n array_agg(i.due_date ORDER BY i.due_date)\n FILTER (WHERE s.st <> 'cancelled') AS due_dates,\n max(i.due_date) FILTER (WHERE s.st <> 'cancelled') AS last_due_date\n FROM public.plg_financial_invoice_installments i\n -- Correlacionado em \\`i.id\\`: só as baixas DESTA parcela, pelo índice.\n LEFT JOIN LATERAL (\n SELECT sum(a.amount) AS paid\n FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = i.id\n AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL\n ) pa ON true\n CROSS JOIN LATERAL (\n SELECT COALESCE(pa.paid, 0::numeric) AS paid,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'\n WHEN COALESCE(pa.paid, 0::numeric) >= i.amount THEN 'paid'\n WHEN COALESCE(pa.paid, 0::numeric) > 0::numeric THEN 'partial'\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'\n ELSE 'pending'\n END AS st\n ) s\n WHERE i.invoice_id = t.id\n ) ib ON true\n LEFT JOIN public.people p ON p.id = t.counterparty_person_id;\n\nCOMMENT ON VIEW public.v_financial_invoice_balances IS\n 'Saldo e estado de cada título, das parcelas dele. LATERAL correlacionado em '\n 'todos os níveis (parcela por invoice_id, baixa por installment_id), para que '\n 'o cálculo aconteça só nas linhas que a consulta pediu — a forma anterior '\n 'somava os 2.375 títulos para mostrar 50, e a tela levava 7 s contra um '\n 'statement_timeout de 8 s.';\n\nALTER VIEW public.v_financial_invoice_balances SET (security_invoker = true);\nGRANT SELECT ON public.v_financial_invoice_balances TO authenticated, service_role;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- A view que dependia dela volta igual\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- \\`v_invoice_balances\\` (o saldo por PEDIDO, que \\`plg_financial_rep_receivables\\`\n-- lê) é derrubada pelo CASCADE acima. Corpo extraído do próprio cluster com\n-- \\`pg_get_viewdef\\` e recriado sem uma vírgula de diferença — o que muda debaixo\n-- dela é só a velocidade.\n\nCREATE VIEW public.v_invoice_balances AS\nWITH tb AS (\n SELECT t.order_id,\n min(t.number) AS reference_number,\n min(t.counterparty_person_id::text)::uuid AS party_id,\n min(t.direction) AS flow,\n sum(t.billed) AS amount,\n sum(t.paid) AS paid,\n sum(t.balance) AS balance,\n bool_and(t.status = 'cancelled'::text) AS all_cancelled,\n bool_or(t.status = 'overdue'::text) AS has_overdue,\n min(t.created_at) AS created_at,\n max(t.updated_at) AS updated_at\n FROM v_financial_invoice_balances t\n WHERE t.order_id IS NOT NULL\n GROUP BY t.order_id\n )\n SELECT o.id AS invoice_id,\n o.tenant_id,\n o.kind,\n tb.reference_number,\n tb.party_id,\n CASE\n WHEN tb.flow = 'payable'::text THEN 'debit'::text\n ELSE 'credit'::text\n END AS direction,\n tb.flow,\n COALESCE(tb.amount, 0::numeric) AS amount,\n COALESCE(tb.paid, 0::numeric) AS paid,\n CASE\n WHEN o.status = 'cancelled'::text OR o.stage = 'cancelled'::text OR tb.all_cancelled THEN 0::numeric\n ELSE COALESCE(tb.balance, 0::numeric)\n END AS balance,\n CASE\n WHEN o.status = 'cancelled'::text OR o.stage = 'cancelled'::text OR tb.all_cancelled THEN 'cancelled'::text\n WHEN COALESCE(tb.amount, 0::numeric) = 0::numeric THEN 'open'::text\n WHEN COALESCE(tb.paid, 0::numeric) >= COALESCE(tb.amount, 0::numeric) THEN 'paid'::text\n WHEN COALESCE(tb.paid, 0::numeric) > 0::numeric THEN 'partial'::text\n WHEN COALESCE(tb.has_overdue, false) THEN 'overdue'::text\n ELSE 'open'::text\n END AS status,\n tb.created_at,\n tb.updated_at\n FROM tb\n JOIN orders o ON o.id = tb.order_id;\n\nALTER VIEW public.v_invoice_balances SET (security_invoker = true);\nGRANT SELECT ON public.v_invoice_balances TO authenticated, service_role;\n`\n\nexport const MIGRATION_055_A_LISTA_DE_CONTAS_LISTA_PARCELAS = `-- A lista de contas lista parcelas.\n--\n-- Três queixas diferentes na mesma tela, e uma causa só.\n--\n-- · \"dia 11/09 está tudo pago e aqui aparece em aberto\"\n-- · \"filtro 11/09 não acha nada\"\n-- · \"contas a pagar está muito lento\"\n--\n-- A tela do V2 listava TÍTULOS; a do V1 lista PARCELAS. Uma compra em 4× é uma\n-- linha lá e quatro linhas no V1 — e quase tudo que incomoda vem daí:\n--\n-- O ESTADO. A parcela de 11/09 está paga; as de 18, 25 e 02/10 não. O título\n-- inteiro, então, é \"parcial\" — e a pessoa que pagou a de 11/09 vê\n-- \"parcial\" onde o V1 escreve \"Pago\".\n--\n-- O RECORTE. Um título tem uma data; quatro parcelas têm quatro. Filtrar um\n-- DIA num título obriga a escolher qual das quatro datas o\n-- representa, e a escolha erra três vezes em quatro.\n--\n-- O PREÇO. Não dá para indexar \"alguma das minhas parcelas cai neste dia\"\n-- quando as parcelas são um agregado calculado por título. Medido,\n-- tenant Pertinho do Céu:\n--\n-- por título por parcela\n-- dia 11/09 7,48 s 0,15 s\n-- mês corrente 4,27 s 0,61 s\n--\n-- Cinquenta vezes, e não por esperteza: a parcela filtra por\n-- \\`plg_financial_invoice_installments (tenant_id, due_date)\\`, que\n-- é um índice que já existia.\n--\n-- E o número certo aparece junto: no dia 11/09 esta view devolve 6 parcelas,\n-- R$ 9.157,82, todas pagas — exatamente o que a tela do V1 mostra. A forma por\n-- título devolvia 6 títulos e R$ 16.890,07, porque somava parcelas de outros\n-- dias.\n--\n-- ── O que a view é ──────────────────────────────────────────────────────────\n--\n-- Uma linha por parcela, com o que a linha da lista precisa: a contraparte, a\n-- descrição, o valor, o pago, o estado e \"3 de 4\". O \\`invoice_id\\` vai junto\n-- para que clicar na linha abra o título — a lista é de parcelas, o documento\n-- continua sendo o título.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.v_financial_invoice_lines_due;\n\nCREATE VIEW public.v_financial_invoice_lines_due AS\nSELECT\n i.id AS installment_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n t.direction,\n t.number,\n t.competence_date,\n t.issue_date,\n t.description,\n t.notes,\n t.metadata,\n t.order_id,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n i.number AS installment_number,\n it.total AS installment_count,\n i.due_date,\n i.amount,\n COALESCE(pa.paid, 0::numeric)::numeric(14,2) AS paid,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(i.amount - COALESCE(pa.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n -- Mesmas palavras de \\`v_financial_installment_balances\\`. Se as duas\n -- divergirem, a lista e o detalhe passam a dizer coisas diferentes sobre a\n -- mesma parcela.\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'\n WHEN COALESCE(pa.paid, 0::numeric) >= i.amount THEN 'paid'\n WHEN COALESCE(pa.paid, 0::numeric) > 0::numeric THEN 'partial'\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'\n ELSE 'pending'\n END AS status,\n i.cancelled_at,\n t.cancel_reason,\n t.created_by,\n i.created_at,\n i.updated_at\n FROM public.plg_financial_invoice_installments i\n JOIN public.plg_financial_invoices t ON t.id = i.invoice_id\n -- Correlacionado: só as baixas DESTA parcela, pelo índice (installment_id).\n LEFT JOIN LATERAL (\n SELECT sum(a.amount) AS paid\n FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = i.id\n AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL\n ) pa ON true\n -- \"3 de 4\": o denominador é do título, e sem ele a coluna de parcela mente.\n LEFT JOIN LATERAL (\n SELECT count(*) AS total\n FROM public.plg_financial_invoice_installments x\n WHERE x.invoice_id = i.invoice_id AND x.cancelled_at IS NULL\n ) it ON true\n LEFT JOIN public.people p ON p.id = t.counterparty_person_id;\n\nCOMMENT ON VIEW public.v_financial_invoice_lines_due IS\n 'Uma linha por PARCELA — a granularidade da lista de contas a pagar do V1, e '\n 'a única em que \"o que vence no dia 11\" tem resposta. Filtra pelo índice '\n '(tenant_id, due_date) das parcelas: 0,15 s contra 7,5 s da forma por título.';\n\nALTER VIEW public.v_financial_invoice_lines_due SET (security_invoker = true);\nGRANT SELECT ON public.v_financial_invoice_lines_due TO authenticated, service_role;\n`\n\nexport const MIGRATION_056_WHAT_THE_PORT_LEFT_OPEN = `-- ---------------------------------------------------------------------------\n-- 056_what_the_port_left_open.sql — the posture the V1 financial wave skipped.\n--\n-- Three findings of the chain's own suites (900_verification/001_posture and\n-- 090_migration_ledger/001_schema_contract), each live on the cluster since\n-- the wave was applied:\n--\n-- 1. \\`v_financial_payables\\` read every tenant's payables. 033 created it with\n-- CREATE OR REPLACE and no options, so it ran with its owner's rights —\n-- RLS off — and carries no tenant filter in its body; it is also granted\n-- to \\`authenticated\\`. Any signed-in user of any tenant could list every\n-- other tenant's bills and the names of who they pay. It becomes\n-- security_invoker, so the base tables' RLS decides again.\n--\n-- 2. \\`plg_financial_recurring_bill_occurrences.amount\\` and \\`.paid_amount\\` were\n-- bare \\`numeric\\` (N25, #182: a money column declares its scale). The view\n-- above reads both, and PostgreSQL will not retype a column a view\n-- depends on — so the view is dropped only when the retype is actually\n-- needed, and recreated below either way.\n--\n-- 3. Functions this plugin added to schemas \\`migration\\` and \\`app\\` kept the\n-- default EXECUTE TO PUBLIC, i.e. \\`anon\\`: the V1 port's writers\n-- (\\`upsert_fin_*\\`, \\`_fin_installment_from_bill\\`) and the DRE's two fence\n-- helpers (\\`app.visible_units\\`, \\`app._units_of_member\\`). Spine 066 sweeps\n-- what the spine created; this sweeps what this plugin created after it.\n--\n-- 4. \\`migration.upsert_fin_recurring_bill\\` (032) writes \\`items\\` and \\`metadata\\`\n-- on \\`plg_financial_recurring_bills\\`, and no file ever added either column:\n-- 014 created the table without them. PL/pgSQL resolves columns when the\n-- function runs, not when it is created, so 032 applied cleanly and every\n-- V1 recurring bill then landed as a \\`42703\\` quarantine. They are added\n-- here, with the shapes the two writers use (an array of items, an object\n-- of provenance).\n--\n-- The files that introduced these are applied on the cluster (their bytes\n-- are in the ledger), so the fix is this file, not an edit to them.\n-- ---------------------------------------------------------------------------\n\n-- ── 4: the columns the port writes ─────────────────────────────────────────\nALTER TABLE public.plg_financial_recurring_bills\n ADD COLUMN IF NOT EXISTS items jsonb NOT NULL DEFAULT '[]'::jsonb,\n ADD COLUMN IF NOT EXISTS metadata jsonb NOT NULL DEFAULT '{}'::jsonb;\n\n-- ── 2 first: the retype needs the view out of the way ──────────────────────\nDO $$\nBEGIN\n IF (SELECT format_type(a.atttypid, a.atttypmod)\n FROM pg_attribute a\n WHERE a.attrelid = 'public.plg_financial_recurring_bill_occurrences'::regclass\n AND a.attname = 'amount') <> 'numeric(14,2)'\n OR (SELECT format_type(a.atttypid, a.atttypmod)\n FROM pg_attribute a\n WHERE a.attrelid = 'public.plg_financial_recurring_bill_occurrences'::regclass\n AND a.attname = 'paid_amount') <> 'numeric(14,2)' THEN\n DROP VIEW IF EXISTS public.v_financial_payables;\n ALTER TABLE public.plg_financial_recurring_bill_occurrences\n ALTER COLUMN amount TYPE numeric(14,2),\n ALTER COLUMN paid_amount TYPE numeric(14,2);\n END IF;\nEND $$;\n\n-- ── 1: the same read, under the caller's rights ────────────────────────────\n-- Body unchanged from 033; only the WITH clause is new.\nCREATE OR REPLACE VIEW public.v_financial_payables WITH (security_invoker = true) AS\nSELECT\n o.tenant_id,\n o.unit_id,\n o.id AS occurrence_id,\n b.id AS recurring_bill_id,\n b.description,\n b.counterparty_person_id,\n p.name AS counterparty_name,\n b.frequency,\n o.seq,\n o.competence_date,\n o.due_date,\n o.amount,\n -- Payment comes from the invoice's movements; \\`o.paid_amount\\` is 0 on\n -- everything that came from V1. Summing the two will count a payment twice\n -- the day V2 starts writing the occurrence as well — a limit 033 named and\n -- this file keeps as it was.\n coalesce(o.paid_amount, 0) + coalesce(m.pago, 0) AS paid_amount,\n o.amount - (coalesce(o.paid_amount, 0) + coalesce(m.pago, 0)) AS open_amount,\n o.status,\n o.overridden,\n o.invoice_id,\n CASE\n WHEN o.status = 'cancelled' THEN 'cancelado'\n WHEN o.amount <= coalesce(o.paid_amount, 0) + coalesce(m.pago, 0) THEN 'pago'\n WHEN coalesce(m.pago, 0) > 0 THEN 'parcial'\n WHEN o.due_date < current_date THEN 'vencido'\n ELSE 'em aberto'\n END AS situacao\nFROM public.plg_financial_recurring_bill_occurrences o\nJOIN public.plg_financial_recurring_bills b ON b.id = o.recurring_bill_id\nLEFT JOIN public.people p ON p.id = b.counterparty_person_id\nLEFT JOIN LATERAL (\n SELECT sum(mv.paid_amount) AS pago\n FROM public.plg_financial_movements mv\n WHERE mv.financial_invoice_id = o.invoice_id\n) m ON o.invoice_id IS NOT NULL;\n\nCOMMENT ON VIEW public.v_financial_payables IS\n 'Payables by due date, with what was paid summed from the invoice movements. '\n 'security_invoker: the base tables'' RLS scopes it to the caller''s tenant (056).';\n\nGRANT SELECT ON public.v_financial_payables TO authenticated, service_role;\n\n-- ── 3: nobody without a session calls these ────────────────────────────────\nDO $$\nDECLARE f regprocedure;\nBEGIN\n FOR f IN\n SELECT p.oid::regprocedure\n FROM pg_proc p\n JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'migration'\n AND (has_function_privilege('anon', p.oid, 'EXECUTE')\n OR has_function_privilege('authenticated', p.oid, 'EXECUTE'))\n LOOP\n EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC, anon, authenticated', f);\n EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', f);\n END LOOP;\nEND $$;\n\n-- The DRE's fence helpers stay callable by a session (the view evaluates them\n-- as its owner, and RLS elsewhere may too) — just not by \\`anon\\`.\nDO $$\nBEGIN\n IF to_regprocedure('app.visible_units()') IS NOT NULL THEN\n REVOKE ALL ON FUNCTION app.visible_units() FROM PUBLIC, anon;\n GRANT EXECUTE ON FUNCTION app.visible_units() TO authenticated, service_role;\n END IF;\n IF to_regprocedure('app._units_of_member()') IS NOT NULL THEN\n REVOKE ALL ON FUNCTION app._units_of_member() FROM PUBLIC, anon;\n GRANT EXECUTE ON FUNCTION app._units_of_member() TO authenticated, service_role;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_057_A_LINHA_DIZ_QUE_TEM_RECIBO = `-- A linha diz que tem recibo.\n--\n-- Os 1.192 anexos das contas a pagar atravessaram (migration 053 do spine) e\n-- ficaram invisíveis: a lista não tinha por onde saber que existem. No V1 o\n-- clipe com o número está na linha, e é por ele que se sabe que a conta tem\n-- comprovante sem abrir a conta.\n--\n-- O anexo pertence ao TÍTULO, não à parcela — um recibo comprova a conta, não a\n-- terceira parcela dela. Então as quatro parcelas de uma conta em 4× mostram o\n-- mesmo número, que é o que a tela do V1 faz.\n--\n-- Medido em 19/09/2026 no tenant Pertinho do Céu: 854 contas com anexo, sendo\n-- 627 com um, 165 com dois, e até seis numa só.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.v_financial_invoice_lines_due;\n\nCREATE VIEW public.v_financial_invoice_lines_due AS\nSELECT\n i.id AS installment_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n t.direction,\n t.number,\n t.competence_date,\n t.issue_date,\n t.description,\n t.notes,\n t.metadata,\n t.order_id,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n i.number AS installment_number,\n it.total AS installment_count,\n i.due_date,\n i.amount,\n COALESCE(pa.paid, 0::numeric)::numeric(14,2) AS paid,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(i.amount - COALESCE(pa.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n -- Mesmas palavras de \\`v_financial_installment_balances\\`. Se as duas\n -- divergirem, a lista e o detalhe passam a dizer coisas diferentes sobre a\n -- mesma parcela.\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'\n WHEN COALESCE(pa.paid, 0::numeric) >= i.amount THEN 'paid'\n WHEN COALESCE(pa.paid, 0::numeric) > 0::numeric THEN 'partial'\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'\n ELSE 'pending'\n END AS status,\n -- O clipe da linha. O anexo pertence ao TÍTULO — um recibo comprova a\n -- conta, não a terceira parcela dela — então todas as parcelas de uma conta\n -- mostram o mesmo número, que é o que a tela do V1 faz.\n COALESCE(ax.n, 0)::integer AS attachment_count,\n i.cancelled_at,\n t.cancel_reason,\n t.created_by,\n i.created_at,\n i.updated_at\n FROM public.plg_financial_invoice_installments i\n JOIN public.plg_financial_invoices t ON t.id = i.invoice_id\n -- Correlacionado: só as baixas DESTA parcela, pelo índice (installment_id).\n LEFT JOIN LATERAL (\n SELECT sum(a.amount) AS paid\n FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = i.id\n AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL\n ) pa ON true\n -- \"3 de 4\": o denominador é do título, e sem ele a coluna de parcela mente.\n LEFT JOIN LATERAL (\n SELECT count(*) AS total\n FROM public.plg_financial_invoice_installments x\n WHERE x.invoice_id = i.invoice_id AND x.cancelled_at IS NULL\n ) it ON true\n -- Correlacionado, como o resto: só os documentos DESTE título.\n LEFT JOIN LATERAL (\n SELECT count(*) AS n\n FROM public.documents d\n WHERE d.subject_type = 'financial_invoice'\n AND d.subject_id = t.id\n AND d.is_active\n ) ax ON true\n LEFT JOIN public.people p ON p.id = t.counterparty_person_id;\n\nCOMMENT ON VIEW public.v_financial_invoice_lines_due IS\n 'Uma linha por PARCELA — a granularidade da lista de contas a pagar do V1, e '\n 'a única em que \"o que vence no dia 11\" tem resposta. Filtra pelo índice '\n '(tenant_id, due_date) das parcelas: 0,15 s contra 7,5 s da forma por título.';\n\nALTER VIEW public.v_financial_invoice_lines_due SET (security_invoker = true);\nGRANT SELECT ON public.v_financial_invoice_lines_due TO authenticated, service_role;\n`\n\nexport const MIGRATION_058_O_CLIPE_USA_O_INDICE = `-- O clipe passa a usar o índice que já existia.\n--\n-- A migration 050 acrescentou a contagem de anexos à linha e a consulta da tela\n-- foi de 0,2 s para 2,5 s. O \\`LATERAL\\` filtrava por \\`subject_type\\` e\n-- \\`subject_id\\` e não por \\`tenant_id\\` — e o índice é\n-- \\`documents_subject (tenant_id, subject_type, subject_id)\\`.\n--\n-- Um índice composto só serve a partir da coluna da frente. A RLS até impõe o\n-- tenant, mas como filtro por linha, depois de o índice já não ter sido usado.\n-- Escrever a condição explicitamente é o que a torna aproveitável.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.v_financial_invoice_lines_due;\n\nCREATE VIEW public.v_financial_invoice_lines_due AS\nSELECT\n i.id AS installment_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n t.direction,\n t.number,\n t.competence_date,\n t.issue_date,\n t.description,\n t.notes,\n t.metadata,\n t.order_id,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n i.number AS installment_number,\n it.total AS installment_count,\n i.due_date,\n i.amount,\n COALESCE(pa.paid, 0::numeric)::numeric(14,2) AS paid,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(i.amount - COALESCE(pa.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n -- Mesmas palavras de \\`v_financial_installment_balances\\`. Se as duas\n -- divergirem, a lista e o detalhe passam a dizer coisas diferentes sobre a\n -- mesma parcela.\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'\n WHEN COALESCE(pa.paid, 0::numeric) >= i.amount THEN 'paid'\n WHEN COALESCE(pa.paid, 0::numeric) > 0::numeric THEN 'partial'\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'\n ELSE 'pending'\n END AS status,\n -- O clipe da linha. O anexo pertence ao TÍTULO — um recibo comprova a\n -- conta, não a terceira parcela dela — então todas as parcelas de uma conta\n -- mostram o mesmo número, que é o que a tela do V1 faz.\n COALESCE(ax.n, 0)::integer AS attachment_count,\n i.cancelled_at,\n t.cancel_reason,\n t.created_by,\n i.created_at,\n i.updated_at\n FROM public.plg_financial_invoice_installments i\n JOIN public.plg_financial_invoices t ON t.id = i.invoice_id\n -- Correlacionado: só as baixas DESTA parcela, pelo índice (installment_id).\n LEFT JOIN LATERAL (\n SELECT sum(a.amount) AS paid\n FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = i.id\n AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL\n ) pa ON true\n -- \"3 de 4\": o denominador é do título, e sem ele a coluna de parcela mente.\n LEFT JOIN LATERAL (\n SELECT count(*) AS total\n FROM public.plg_financial_invoice_installments x\n WHERE x.invoice_id = i.invoice_id AND x.cancelled_at IS NULL\n ) it ON true\n -- Correlacionado, como o resto: só os documentos DESTE título.\n LEFT JOIN LATERAL (\n SELECT count(*) AS n\n FROM public.documents d\n -- \\`tenant_id\\` PRIMEIRO, e não por zelo: o índice é\n -- \\`documents_subject (tenant_id, subject_type, subject_id)\\`, e sem a\n -- coluna da frente ele não serve. Sem ela a consulta da tela ia de 0,2 s\n -- para 2,5 s — o clipe custando dez vezes o resto da linha.\n WHERE d.tenant_id = t.tenant_id\n AND d.subject_type = 'financial_invoice'\n AND d.subject_id = t.id\n AND d.is_active\n ) ax ON true\n LEFT JOIN public.people p ON p.id = t.counterparty_person_id;\n\nCOMMENT ON VIEW public.v_financial_invoice_lines_due IS\n 'Uma linha por PARCELA — a granularidade da lista de contas a pagar do V1, e '\n 'a única em que \"o que vence no dia 11\" tem resposta. Filtra pelo índice '\n '(tenant_id, due_date) das parcelas: 0,15 s contra 7,5 s da forma por título.';\n\nALTER VIEW public.v_financial_invoice_lines_due SET (security_invoker = true);\nGRANT SELECT ON public.v_financial_invoice_lines_due TO authenticated, service_role;\n`\n\nexport const MIGRATION_059_O_CLIPE_SAI_DA_VIEW = `-- O clipe sai da view e vira uma segunda pergunta.\n--\n-- A 050 pendurou a contagem de anexos num LATERAL sobre \\`documents\\`. Correto no\n-- resultado e caro no preço: \\`documents\\` é \\`security_invoker\\`, e a política\n-- dela faz as mesmas chamadas correlacionadas por linha que já custaram caro no\n-- DRE. Cinquenta avaliações do LATERAL, cinquenta vezes esse preço.\n--\n-- Medido em 19/09/2026, a consulta que a tela faz:\n--\n-- sem o clipe na view 0,20 s\n-- com o clipe na view 3,84 s (pico de 6,61 s)\n--\n-- Acrescentar o índice \\`tenant_id\\` na condição ajudou pouco (2,5 → 2,0 s no\n-- caso de varredura): o custo não é achar o documento, é a RLS que roda para\n-- cada um.\n--\n-- A lista já tem as cinquenta linhas na mão. Perguntar \"quais destes cinquenta\n-- títulos têm anexo\" é UMA consulta por página, pelo índice\n-- \\`documents_subject\\`, e o cliente junta. Uma pergunta a mais na página é mais\n-- barato que cinquenta perguntas dentro do plano.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.v_financial_invoice_lines_due;\n\nCREATE VIEW public.v_financial_invoice_lines_due AS\nSELECT\n i.id AS installment_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n t.direction,\n t.number,\n t.competence_date,\n t.issue_date,\n t.description,\n t.notes,\n t.metadata,\n t.order_id,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n i.number AS installment_number,\n it.total AS installment_count,\n i.due_date,\n i.amount,\n COALESCE(pa.paid, 0::numeric)::numeric(14,2) AS paid,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(i.amount - COALESCE(pa.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n -- Mesmas palavras de \\`v_financial_installment_balances\\`. Se as duas\n -- divergirem, a lista e o detalhe passam a dizer coisas diferentes sobre a\n -- mesma parcela.\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'\n WHEN COALESCE(pa.paid, 0::numeric) >= i.amount THEN 'paid'\n WHEN COALESCE(pa.paid, 0::numeric) > 0::numeric THEN 'partial'\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'\n ELSE 'pending'\n END AS status,\n i.cancelled_at,\n t.cancel_reason,\n t.created_by,\n i.created_at,\n i.updated_at\n FROM public.plg_financial_invoice_installments i\n JOIN public.plg_financial_invoices t ON t.id = i.invoice_id\n -- Correlacionado: só as baixas DESTA parcela, pelo índice (installment_id).\n LEFT JOIN LATERAL (\n SELECT sum(a.amount) AS paid\n FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = i.id\n AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL\n ) pa ON true\n -- \"3 de 4\": o denominador é do título, e sem ele a coluna de parcela mente.\n LEFT JOIN LATERAL (\n SELECT count(*) AS total\n FROM public.plg_financial_invoice_installments x\n WHERE x.invoice_id = i.invoice_id AND x.cancelled_at IS NULL\n ) it ON true\n LEFT JOIN public.people p ON p.id = t.counterparty_person_id;\n\nCOMMENT ON VIEW public.v_financial_invoice_lines_due IS\n 'Uma linha por PARCELA — a granularidade da lista de contas a pagar do V1, e '\n 'a única em que \"o que vence no dia 11\" tem resposta. Filtra pelo índice '\n '(tenant_id, due_date) das parcelas: 0,15 s contra 7,5 s da forma por título.';\n\nALTER VIEW public.v_financial_invoice_lines_due SET (security_invoker = true);\nGRANT SELECT ON public.v_financial_invoice_lines_due TO authenticated, service_role;\n`\n\nexport const MIGRATION_060_O_RECEBIVEL_VOLTA_DA_ARVORE_DERRUBADA = `-- O recebível volta da árvore derrubada.\n--\n-- Mesma causa da 007 da agenda: o \\`DROP VIEW … CASCADE\\` das migrations 044 e\n-- 045 derrubou tudo o que pendurava em \\`v_invoice_balances\\`, e elas só\n-- recriaram as duas views que o autor tinha em mente.\n--\n-- \\`plg_financial_rep_receivables\\` é a que o plugin-reports lê para o relatório\n-- de contas a receber — o alias \\`plg_*\\` é o nome pelo qual o catálogo de\n-- relatórios a encontra. Ela sumiu junto e ninguém percebeu, porque quem abre\n-- contas a receber no dia a dia abre a LISTA, não o relatório.\n--\n-- Corpo idêntico ao do baseline: nenhuma coluna muda, nenhuma regra muda.\n-- \\`security_invoker\\` e GRANT explícitos, porque uma view recriada não herda\n-- nem as opções nem as permissões da que morreu.\n--\n-- Idempotente.\n\nCREATE OR REPLACE VIEW public.plg_financial_rep_receivables WITH (security_invoker = true) AS\n SELECT b.invoice_id,\n b.tenant_id,\n b.created_at,\n b.reference_number,\n b.kind,\n b.status,\n b.party_id,\n p.name AS party_name,\n b.amount,\n b.paid,\n b.balance,\n CASE\n WHEN (b.status = ANY (ARRAY['open'::text, 'overdue'::text])) THEN b.balance\n ELSE (0)::numeric\n END AS open_balance,\n CASE\n WHEN (b.status = 'overdue'::text) THEN b.balance\n ELSE (0)::numeric\n END AS overdue_balance,\n CASE\n WHEN (b.status = 'paid'::text) THEN b.amount\n ELSE (0)::numeric\n END AS settled_amount,\n CASE\n WHEN (b.status = 'overdue'::text) THEN 1\n ELSE 0\n END AS is_overdue,\n CASE\n WHEN (b.status = 'paid'::text) THEN 1\n ELSE 0\n END AS is_settled\n FROM (public.v_invoice_balances b\n LEFT JOIN public.people p ON ((p.id = b.party_id)))\n WHERE (b.flow = 'receivable'::text);\n\nGRANT SELECT ON TABLE public.plg_financial_rep_receivables TO authenticated, service_role;\n`\n\nexport const MIGRATION_061_CONTAS_A_PAGAR_GANHAM_ONDE_MORAR = `-- Contas a pagar ganham onde morar.\n--\n-- \\`recurring_bills\\`, \\`recurring_bill_items\\` e \\`recurring_bill_occurrences\\` do V1\n-- estavam em \\`migration.excluded_tables\\` com este motivo: *\"no destination\n-- contract … plg_financial_recurring_bills nunca foi construída (#188)\"*.\n--\n-- **A nota está vencida.** \\`public.plg_financial_recurring_bills\\` existe, com\n-- forma própria e diferente da do V1 — e é essa forma que manda:\n--\n-- V1 V2\n-- recurring_bills plg_financial_recurring_bills\n-- recurring_bill_items (tabela) .items (jsonb na própria conta)\n-- recurring_bill_occurrences (tabela) — não existe\n-- interval_unit + interval_count frequency (weekly|monthly|quarterly|yearly)\n-- first_due_date starts_on + due_day (1..31)\n-- account_central_id → contacts counterparty_person_id → public.people\n-- (soma dos itens) amount NOT NULL, CHECK (amount > 0)\n-- (primeira descrição de item) description NOT NULL\n--\n-- Este arquivo faz três coisas, e só a terceira cria tabela:\n--\n-- 1. escreve os writers que levam a conta e os itens para a forma do V2\n-- 2. escreve o de-para e tira as três tabelas da lista de adiadas\n-- 3. cria \\`plg_financial_recurring_bill_occurrences\\`, que é a única lacuna\n-- DE VERDADE\n--\n-- ── Por que a ocorrência precisa existir ─────────────────────────────────────\n--\n-- O V2 deriva os vencimentos de \\`frequency\\` + \\`due_day\\`: a conta é a regra, e o\n-- calendário sai dela. Isso funciona enquanto ninguém mexe num vencimento — e o\n-- V1 deixa mexer. \\`recurring_bill_occurrences.overridden\\` marca o vencimento que\n-- alguém editou à mão (mudou o valor de março sem mexer na regra), e\n-- \\`paid_value\\` guarda o que de fato foi pago. Nenhum dos dois é derivável: a\n-- fórmula devolve o valor da regra, não o que aconteceu.\n--\n-- Medido na origem em 18/09/2026 (licença 8, Pertinho do Céu): 35 contas, 35\n-- itens, 833 ocorrências, R$ 1.937.827,67 entre 25/05/2026 e 02/09/2028, 111 já\n-- consolidadas. Descartar as ocorrências apagaria o histórico de pagamento e\n-- toda edição manual junto.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. A única tabela que falta\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_recurring_bill_occurrences (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid REFERENCES app.units(id),\n recurring_bill_id uuid NOT NULL\n REFERENCES public.plg_financial_recurring_bills(id) ON DELETE CASCADE,\n seq integer NOT NULL,\n competence_date date NOT NULL,\n due_date date NOT NULL,\n amount numeric NOT NULL,\n paid_amount numeric NOT NULL DEFAULT 0,\n status text NOT NULL DEFAULT 'provisioned'\n CHECK (status IN ('provisioned', 'consolidated', 'cancelled')),\n -- Alguém editou ESTE vencimento, fora da regra. É a razão de a ocorrência ser\n -- uma linha e não um cálculo.\n overridden boolean NOT NULL DEFAULT false,\n -- O título que o vencimento virou, quando virou. Fica nulo até consolidar, e\n -- não vem do V1: lá o campo aponta para \\`invoices\\`, cuja contraparte no V2 é\n -- uma VENDA, e ligar uma conta a pagar a uma venda inverteria o sentido do\n -- dinheiro para ganhar um link. A referência crua do V1 fica em metadata.\n invoice_id uuid REFERENCES public.plg_financial_invoices(id) ON DELETE SET NULL,\n notes text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (recurring_bill_id, seq)\n);\n\n-- A pergunta que a tela faz: \"o que vence neste período?\". Sem este índice ela\n-- varre as ocorrências de todos os tenants.\nCREATE INDEX IF NOT EXISTS plg_financial_recurring_bill_occurrences_due_idx\n ON public.plg_financial_recurring_bill_occurrences (tenant_id, due_date, status);\n\nDROP TRIGGER IF EXISTS plg_financial_recurring_bill_occurrences_updated_at\n ON public.plg_financial_recurring_bill_occurrences;\nCREATE TRIGGER plg_financial_recurring_bill_occurrences_updated_at\n BEFORE UPDATE ON public.plg_financial_recurring_bill_occurrences\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nGRANT SELECT, INSERT, UPDATE, DELETE\n ON public.plg_financial_recurring_bill_occurrences TO authenticated, service_role;\n\n-- O scaffold é quem sabe desenhar a política deste cluster (tenant, unidade,\n-- permissão do plugin, escotilhas de compartilhamento). Escrever a policy à mão\n-- criaria a segunda regra de autorização do financeiro, que diverge da primeira\n-- na próxima mudança do scaffold.\n--\n-- \\`unit_optional\\`, e não \\`unit\\`: \\`unit_id\\` aceita NULL aqui porque a conta pode\n-- não ter unidade. Declarar \\`unit\\` numa coluna anulável é o erro que já escondeu\n-- todo movimento financeiro sem unidade e zerou um relatório inteiro.\nSELECT app.scaffold_table('public.plg_financial_recurring_bill_occurrences'::regclass,\n 'financial.recurring_bill_occurrence', 'financial', false, 'enforce');\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. Os writers\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- O vocabulário do de-para é lista fechada de propósito: uma linha que nomeia um\n-- writer inexistente falha na hora, não na terceira hora da onda.\n--\n-- A LISTA É A UNIÃO, E NÃO UMA LISTA LITERAL. Este CHECK é de um recurso\n-- COMPARTILHADO: cada onda de porte acrescenta os seus writers a ele. Escrito\n-- como literal, o último arquivo a rodar apaga os writers dos outros — e foi\n-- exatamente o que aconteceu aqui: este arquivo nasceu antes de o workforce\n-- declarar os \\`wf_*\\`, e ao ser replayed derrubava todos eles, violando o\n-- próprio CHECK com as linhas que já existiam (\"is violated by some row\").\n--\n-- Somar o que ESTÁ EM USO não afrouxa nada: um writer já gravado na allowlist\n-- já passou por este mesmo portão alguma vez. O que o CHECK continua pegando é\n-- o writer NOVO com nome errado, que é para o que ele existe.\nDO $$\nDECLARE v_list text;\nBEGIN\n SELECT string_agg(DISTINCT quote_literal(w), ', ') INTO v_list FROM (\n SELECT unnest(ARRAY[\n 'person', 'unit', 'category', 'product', 'order', 'invoice', 'price_table',\n 'price_table_item', 'package', 'package_item', 'field_rule', 'holiday', 'facet',\n 'fin_account', 'fin_payment_method_type', 'fin_payment_method', 'fin_card_brand',\n 'fin_chart_account', 'fin_cost_center', 'inv_measurement_unit', 'inv_unit_conversion',\n 'inv_location', 'inv_movement', 'order_item', 'fin_movement', 'authz_role',\n 'authz_role_permission', 'person_registration',\n -- Os três que esta migration traz.\n 'fin_recurring_bill', 'fin_recurring_bill_item', 'fin_recurring_bill_occurrence']) AS w\n UNION\n SELECT a.writer FROM migration.allowlist a WHERE a.writer IS NOT NULL\n ) s;\n\n ALTER TABLE migration.allowlist DROP CONSTRAINT IF EXISTS allowlist_writer_check;\n EXECUTE format(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check CHECK (writer IS NULL OR writer = ANY (ARRAY[%s]))',\n v_list);\nEND $$;\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_dir_v1 text := nullif(btrim(upper(coalesce(p_row ->> 'direction', ''))), '');\n v_dir text;\n v_unit_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'interval_unit', ''))), '');\n v_count integer := coalesce(nullif(btrim(coalesce(p_row ->> 'interval_count', '')), '')::integer, 1);\n v_freq text;\n v_amount numeric := (p_row ->> 'amount')::numeric;\n v_desc text := nullif(btrim(coalesce(p_row ->> 'description', '')), '');\n v_due_day integer;\n v_starts date := (p_row ->> 'starts_on')::date;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_starts IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma conta recorrente precisa de id e de primeiro vencimento');\n END IF;\n\n -- Sem correção silenciosa: o V1 grava 'D' (despesa) e 'R' (receita) em \\`type\\`.\n -- Um vencimento com o sentido do dinheiro trocado some do contas a pagar e\n -- aparece no contas a receber, e ninguém confere um número que parece plausível.\n v_dir := CASE v_dir_v1 WHEN 'D' THEN 'payable' WHEN 'R' THEN 'receivable' ELSE NULL END;\n IF v_dir IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('sentido do dinheiro desconhecido: type=%L (o V1 usa D=despesa, R=receita)', v_dir_v1));\n END IF;\n\n -- Duas colunas do V1 (unidade + quantidade) viram uma palavra no V2. A tradução\n -- é exaustiva de propósito: \"a cada 2 meses\" não tem nome no vocabulário do V2,\n -- e arredondar para 'monthly' dobraria a previsão de caixa daquela conta.\n v_freq := CASE\n WHEN v_unit_v1 = 'week' AND v_count = 1 THEN 'weekly'\n WHEN v_unit_v1 = 'month' AND v_count = 1 THEN 'monthly'\n WHEN v_unit_v1 = 'month' AND v_count = 3 THEN 'quarterly'\n WHEN v_unit_v1 = 'month' AND v_count = 12 THEN 'yearly'\n WHEN v_unit_v1 = 'year' AND v_count = 1 THEN 'yearly'\n ELSE NULL END;\n IF v_freq IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('periodicidade sem equivalente no V2: a cada %s %s (o V2 conhece '\n 'weekly, monthly, quarterly e yearly)', v_count, v_unit_v1));\n END IF;\n\n -- \\`description\\` e \\`amount\\` são NOT NULL no V2 e não existem na conta do V1:\n -- lá eles moram nos itens. O plano de extração os reconstrói a partir da\n -- própria origem (Step.augment) — se vierem vazios, a conta não tem item, e\n -- isso é uma linha quebrada na origem, não um default a inventar.\n IF v_desc IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'conta recorrente sem descrição: no V1 ela vem do item, e esta conta não tem item');\n END IF;\n IF v_amount IS NULL OR v_amount <= 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('valor da conta recorrente é %s: o V2 exige amount > 0, e no V1 ele é a '\n 'soma dos itens', coalesce(v_amount::text, 'nulo')));\n END IF;\n\n v_due_day := extract(day from v_starts)::integer;\n\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bills WHERE id = p_id) THEN\n INSERT INTO public.plg_financial_recurring_bills\n (id, tenant_id, unit_id, description, direction, counterparty_person_id, amount,\n chart_account_id, cost_center_id, frequency, due_day, starts_on, ends_on,\n is_active, notes, items, metadata)\n VALUES (p_id, p_tenant, (p_row ->> 'unit_id')::uuid, v_desc, v_dir,\n (p_row ->> 'counterparty_person_id')::uuid, v_amount,\n (p_row ->> 'chart_account_id')::uuid, (p_row ->> 'cost_center_id')::uuid,\n v_freq, v_due_day, v_starts, (p_row ->> 'ends_on')::date,\n coalesce((p_row ->> 'is_active')::boolean, true), p_row ->> 'notes',\n '[]'::jsonb, v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_recurring_bills SET\n unit_id = CASE WHEN p_row ? 'unit_id' THEN (p_row ->> 'unit_id')::uuid ELSE unit_id END,\n description = v_desc,\n direction = v_dir,\n counterparty_person_id = CASE WHEN p_row ? 'counterparty_person_id' THEN (p_row ->> 'counterparty_person_id')::uuid ELSE counterparty_person_id END,\n amount = v_amount,\n chart_account_id = CASE WHEN p_row ? 'chart_account_id' THEN (p_row ->> 'chart_account_id')::uuid ELSE chart_account_id END,\n cost_center_id = CASE WHEN p_row ? 'cost_center_id' THEN (p_row ->> 'cost_center_id')::uuid ELSE cost_center_id END,\n frequency = v_freq,\n due_day = v_due_day,\n starts_on = v_starts,\n ends_on = CASE WHEN p_row ? 'ends_on' THEN (p_row ->> 'ends_on')::date ELSE ends_on END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bills', p_id, '{}'::jsonb); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- O item do V1 é uma LINHA; no V2 ele é um elemento do jsonb \\`items\\` da conta.\n-- Por isso este writer não tem tabela própria e devolve o id da CONTA: o ledger\n-- passa a apontar a linha de item do V1 para a conta que a contém, que é onde o\n-- dado foi parar de verdade.\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill_item(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_bill uuid := (p_row ->> 'recurring_bill_id')::uuid;\n v_item jsonb;\n v_items jsonb;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_bill IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'um item precisa de id e da conta recorrente a que pertence');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bills\n WHERE id = v_bill AND tenant_id = p_tenant) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a conta recorrente %s não existe neste tenant', v_bill));\n END IF;\n\n v_item := jsonb_strip_nulls(jsonb_build_object(\n 'id', p_id,\n 'description', p_row ->> 'description',\n 'quantity', (p_row ->> 'quantity')::numeric,\n 'unit_price', (p_row ->> 'unit_price')::numeric,\n 'discount', (p_row ->> 'discount')::numeric,\n 'addition', (p_row ->> 'addition')::numeric,\n 'total_amount', (p_row ->> 'total_amount')::numeric,\n 'chart_account_id', p_row ->> 'chart_account_id',\n 'cost_center_id', p_row ->> 'cost_center_id',\n -- No V2 um serviço É um produto: as duas colunas do V1 desaguam aqui.\n 'product_id', coalesce(p_row ->> 'product_id', p_row ->> 'service_product_id')\n ));\n\n -- Substitui o item de mesmo id em vez de acrescentar: reoferecer a mesma linha\n -- numa onda seguinte tem que atualizar, não duplicar o valor da conta.\n SELECT coalesce(jsonb_agg(x), '[]'::jsonb) INTO v_items\n FROM (SELECT value AS x FROM jsonb_array_elements(\n (SELECT items FROM public.plg_financial_recurring_bills WHERE id = v_bill))\n WHERE value ->> 'id' IS DISTINCT FROM p_id::text) s;\n\n UPDATE public.plg_financial_recurring_bills\n SET items = v_items || jsonb_build_array(v_item)\n WHERE id = v_bill;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bills', v_bill, '{}'::jsonb); END IF;\n -- target_id é a CONTA: é lá que o item mora no V2.\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_bill,\n 'reason', 'item gravado em plg_financial_recurring_bills.items');\nEND $function$;\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill_occurrence(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_bill uuid := (p_row ->> 'recurring_bill_id')::uuid;\n v_status_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text;\n v_unit uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_bill IS NULL OR p_row ->> 'due_date' IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma ocorrência precisa de id, da conta recorrente e de vencimento');\n END IF;\n SELECT unit_id INTO v_unit FROM public.plg_financial_recurring_bills\n WHERE id = v_bill AND tenant_id = p_tenant;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a conta recorrente %s não existe neste tenant', v_bill));\n END IF;\n\n -- O V1 usa provisioned | consolidated; 'cancelled' existe do lado do V2 para o\n -- vencimento cancelado DEPOIS da virada. Estado desconhecido vira quarentena\n -- com a palavra à vista, não um estado aproximado.\n v_status := CASE v_status_v1\n WHEN 'provisioned' THEN 'provisioned'\n WHEN 'consolidated' THEN 'consolidated'\n WHEN 'cancelled' THEN 'cancelled'\n WHEN 'canceled' THEN 'cancelled'\n ELSE NULL END;\n IF v_status IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('estado de vencimento desconhecido: %L', v_status_v1));\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bill_occurrences WHERE id = p_id) THEN\n INSERT INTO public.plg_financial_recurring_bill_occurrences\n (id, tenant_id, unit_id, recurring_bill_id, seq, competence_date, due_date,\n amount, paid_amount, status, overridden, notes, metadata)\n VALUES (p_id, p_tenant, v_unit, v_bill,\n coalesce((p_row ->> 'seq')::integer, 1),\n coalesce((p_row ->> 'competence_date')::date, (p_row ->> 'due_date')::date),\n (p_row ->> 'due_date')::date,\n coalesce((p_row ->> 'amount')::numeric, 0),\n coalesce((p_row ->> 'paid_amount')::numeric, 0),\n v_status, coalesce((p_row ->> 'overridden')::boolean, false),\n p_row ->> 'notes', v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_recurring_bill_occurrences SET\n unit_id = v_unit,\n seq = CASE WHEN p_row ? 'seq' THEN coalesce((p_row ->> 'seq')::integer, seq) ELSE seq END,\n competence_date = CASE WHEN p_row ? 'competence_date' THEN coalesce((p_row ->> 'competence_date')::date, competence_date) ELSE competence_date END,\n due_date = (p_row ->> 'due_date')::date,\n amount = CASE WHEN p_row ? 'amount' THEN coalesce((p_row ->> 'amount')::numeric, amount) ELSE amount END,\n paid_amount = CASE WHEN p_row ? 'paid_amount' THEN coalesce((p_row ->> 'paid_amount')::numeric, paid_amount) ELSE paid_amount END,\n status = v_status,\n overridden = CASE WHEN p_row ? 'overridden' THEN coalesce((p_row ->> 'overridden')::boolean, overridden) ELSE overridden END,\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bill_occurrences', p_id, '{}'::jsonb); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 3. O de-para\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- Saem da lista de adiadas: a nota delas dizia que o destino não existia, e ele\n-- existe (duas das três) ou passou a existir (a terceira, acima).\nDELETE FROM migration.excluded_tables\n WHERE source_table IN ('recurring_bills', 'recurring_bill_items', 'recurring_bill_occurrences');\n\n-- \\`_allow\\` é o construtor oficial: a allowlist tem gatilho de congelamento, e um\n-- INSERT direto por cima de uma linha \\`frozen\\` seria recusado.\n--\n-- As quatro colunas que o V2 exige e o V1 não tem na conta — description, amount,\n-- starts_on e o comprador — são reconstruídas a partir da PRÓPRIA origem pelo\n-- plano de extração (\\`Step.augment\\` em fayz_etl/sources/beautysoft_v1.py), que é\n-- a ponte declarada para isso. Nada aqui inventa valor.\n-- ── a allowlist está CONGELADA, e destravar é o caminho previsto ───────────\n--\n-- \\`trg_allowlist_frozen\\` recusa UPDATE numa linha \\`frozen\\`: mudança de core-A\n-- é arquivo de migration, não escrita solta — e esta É um arquivo de migration.\n-- \\`migration.allowlist_unlock\\` é a escotilha que o próprio gatilho lê, e ela é\n-- \\`true\\` no terceiro argumento: vale só até o fim desta transação, então não\n-- fica uma porta aberta atrás deste arquivo.\n--\n-- Sem isto, \\`_allow\\` falha ao reescrever \\`recurring_bills\\`, que uma onda\n-- anterior já havia declarado.\nSELECT set_config('migration.allowlist_unlock', 'on', true);\n\nSELECT migration._allow(\n 'recurring_bills', 'public.plg_financial_recurring_bills', 'financial',\n 'fin_recurring_bill', '{}'::jsonb, ARRAY['amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'unit_id', 'unit_id@companies:app.units',\n 'type', 'direction',\n 'first_due_date', 'starts_on',\n 'end_date', 'ends_on',\n 'interval_count', 'interval_count',\n 'interval_unit', 'interval_unit',\n 'active', 'is_active',\n 'observations', 'notes',\n -- Derivadas pelo plano, a partir dos itens e do account_central da conta.\n 'description', 'description',\n 'amount', 'amount',\n 'counterparty_contact_id','counterparty_person_id@contacts:public.people',\n 'chart_account_id', 'chart_account_id@chart_of_accounts',\n 'cost_center_id', 'cost_center_id@cost_centers',\n -- A conta financeira da contraparte continua registrada, ainda que o V2\n -- prefira a PESSOA: é por ela que o V1 alcança quem recebe.\n 'account_central_id', 'jsonb:v1.account_central_id',\n -- O autor é um login do V1, e nada liga login do V1 a usuário do V2 (#115).\n -- A referência crua vale mais que perdê-la e mais que fingir que resolve.\n 'created_by', 'jsonb:v1.created_by',\n 'payment_method_id', 'jsonb:v1.payment_method_id'\n ),\n 'Regra de conta recorrente. description/amount/contraparte são derivados dos itens e do account_central pelo plano de extração.'\n);\n\nSELECT migration._allow(\n 'recurring_bill_items', 'public.plg_financial_recurring_bills', 'financial',\n 'fin_recurring_bill_item', '{}'::jsonb, ARRAY[]::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'recurring_bill_id', 'recurring_bill_id@recurring_bills',\n 'description', 'description',\n 'quantity', 'quantity',\n 'unit_price', 'unit_price',\n 'discount', 'discount',\n 'additional', 'addition',\n 'total_amount', 'total_amount',\n 'account_id', 'chart_account_id@chart_of_accounts',\n 'cost_center_id', 'cost_center_id@cost_centers',\n 'product_id', 'product_id@products',\n 'service_id', 'service_product_id@services:public.products',\n -- Único valor medido: 'other'. Traduzi-lo exigiria um vocabulário de destino\n -- que não existe; no metadata ele fica recuperável sem inventar nada.\n 'item_type', 'jsonb:v1.item_type'\n ),\n 'Item da conta. No V2 ele é elemento do jsonb \\`items\\` da própria conta — não tem tabela.'\n);\n\nSELECT migration._allow(\n 'recurring_bill_occurrences', 'public.plg_financial_recurring_bill_occurrences',\n 'financial',\n 'fin_recurring_bill_occurrence', '{}'::jsonb, ARRAY['amount', 'paid_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'recurring_bill_id', 'recurring_bill_id@recurring_bills',\n 'seq', 'seq',\n 'competence_date', 'competence_date',\n 'due_date', 'due_date',\n 'value', 'amount',\n 'paid_value', 'paid_amount',\n 'status', 'status',\n 'overridden', 'overridden',\n 'observations', 'notes',\n -- NÃO vira FK: no V1 aponta para \\`invoices\\`, cuja contraparte no V2 é uma\n -- VENDA. Ligar uma conta a pagar a uma venda inverteria o sentido do\n -- dinheiro para ganhar um link. O estado \\`consolidated\\` já diz o que importa.\n 'invoice_id', 'jsonb:v1.invoice_id'\n ),\n 'O vencimento. \\`overridden\\` marca o que alguém editou fora da regra — não é derivável.'\n);\n\n-- Fecha a escotilha antes de qualquer outra coisa neste arquivo tocar a\n-- allowlist sem querer.\nSELECT set_config('migration.allowlist_unlock', 'off', true);\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\n--\n-- Função nova nasce executável por PUBLIC, e \\`anon\\`/\\`authenticated\\` herdam\n-- dali. Estas são de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcançável pela chave anônima é a base inteira\n-- alcançável pela chave anônima. Só o service_role, que é quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_recurring_bill(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_recurring_bill(uuid, uuid, jsonb, jsonb) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_fin_recurring_bill_item(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_recurring_bill_item(uuid, uuid, jsonb, jsonb) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_fin_recurring_bill_occurrence(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_recurring_bill_occurrence(uuid, uuid, jsonb, jsonb) TO service_role;\n`\n\nexport const MIGRATION_062_O_VENCIMENTO_APONTA_PARA_O_TITULO = `-- O vencimento aponta para o título.\n--\n-- Correção de um mapeamento da 046. Lá, \\`recurring_bill_occurrences.invoice_id\\`\n-- foi jogado em \\`metadata\\` com esta justificativa:\n--\n-- \"NÃO vira FK: no V1 aponta para \\`invoices\\`, cuja contraparte no V2 é uma\n-- VENDA. Ligar uma conta a pagar a uma venda inverteria o sentido do\n-- dinheiro para ganhar um link.\"\n--\n-- A primeira metade está certa e a segunda é falsa. \\`invoices\\` do V1 tem DOIS\n-- destinos no V2, não um: o writer \\`order\\` grava a Venda em \\`public.orders\\` e\n-- devolve, em \\`extra\\`, o TÍTULO em \\`public.plg_financial_invoices\\`. O ledger\n-- guarda as duas linhas. Medido no tenant 8: 2.365 para \\`public.orders\\` e 2.365\n-- para \\`public.plg_financial_invoices\\`.\n--\n-- E o título é exatamente o objeto certo: das 111 faturas que as ocorrências\n-- consolidadas referenciam, as 111 têm título no V2, e **todas com\n-- \\`direction = 'payable'\\`**. Não há inversão nenhuma a evitar.\n--\n-- ── O que o erro custava ─────────────────────────────────────────────────────\n--\n-- \\`recurring_bill_occurrences.paid_value\\` é 0 nas 833 linhas da origem — no V1 o\n-- pagamento não mora no vencimento, mora no movimento financeiro ligado ao\n-- título. Com \\`invoice_id\\` em metadata, a ocorrência consolidada chegava ao V2\n-- sem nenhum caminho até o dinheiro: 123 vencimentos marcados \\`consolidated\\` e\n-- indistinguíveis, na tela, de 710 em aberto. A tela dizia a verdade sobre o\n-- campo e mentia sobre o fato.\n--\n-- Com a FK, o caminho existe:\n--\n-- ocorrência → plg_financial_invoices → plg_financial_movements (paid_amount)\n--\n-- São R$ 311.006,00 pagos em 181 movimentos, que passam a ser alcançáveis a\n-- partir do vencimento.\n--\n-- Idempotente.\n\n-- A linha já está \\`frozen\\`, e o gatilho \\`trg_allowlist_frozen\\` recusa a escrita\n-- com a frase certa: \"core-A allowlist changes are a migration file, not a\n-- write\". Este É o arquivo de migration, e a escotilha existe exatamente para\n-- ele — \\`SET LOCAL\\`, para a destravada morrer com a transação e não vazar para a\n-- sessão seguinte.\nSET LOCAL migration.allowlist_unlock = 'on';\n\n-- \\`_allow\\` reescreve a linha inteira (ON CONFLICT DO UPDATE), então o mapa vai\n-- completo — inclusive as colunas que não mudaram.\nSELECT migration._allow(\n 'recurring_bill_occurrences', 'public.plg_financial_recurring_bill_occurrences',\n 'financial',\n 'fin_recurring_bill_occurrence', '{}'::jsonb, ARRAY['amount', 'paid_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'recurring_bill_id', 'recurring_bill_id@recurring_bills',\n 'seq', 'seq',\n 'competence_date', 'competence_date',\n 'due_date', 'due_date',\n 'value', 'amount',\n 'paid_value', 'paid_amount',\n 'status', 'status',\n 'overridden', 'overridden',\n 'observations', 'notes',\n -- O TÍTULO, não a venda. \\`invoices\\` tem dois destinos no V2 e este é o\n -- financeiro; o \\`:public.plg_financial_invoices\\` escolhe qual dos dois o\n -- ledger deve resolver.\n 'invoice_id', 'invoice_id@invoices:public.plg_financial_invoices'\n ),\n 'O vencimento. \\`overridden\\` marca o que alguém editou fora da regra — não é '\n 'derivável. \\`invoice_id\\` liga o vencimento consolidado ao título, que é onde o '\n 'pagamento mora: no V1 paid_value é sempre 0.'\n);\n\n-- O writer precisa gravar a coluna que o de-para agora entrega. Sem isto o mapa\n-- resolveria a referência e o writer a jogaria fora em silêncio — que é a forma\n-- mais cara de errar aqui, porque tudo parece ter funcionado.\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill_occurrence(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_bill uuid := (p_row ->> 'recurring_bill_id')::uuid;\n v_invoice uuid := (p_row ->> 'invoice_id')::uuid;\n v_status_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text;\n v_unit uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_bill IS NULL OR p_row ->> 'due_date' IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma ocorrência precisa de id, da conta recorrente e de vencimento');\n END IF;\n SELECT unit_id INTO v_unit FROM public.plg_financial_recurring_bills\n WHERE id = v_bill AND tenant_id = p_tenant;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a conta recorrente %s não existe neste tenant', v_bill));\n END IF;\n\n -- O V1 usa provisioned | consolidated; 'cancelled' existe do lado do V2 para o\n -- vencimento cancelado DEPOIS da virada. Estado desconhecido vira quarentena\n -- com a palavra à vista, não um estado aproximado.\n v_status := CASE v_status_v1\n WHEN 'provisioned' THEN 'provisioned'\n WHEN 'consolidated' THEN 'consolidated'\n WHEN 'cancelled' THEN 'cancelled'\n WHEN 'canceled' THEN 'cancelled'\n ELSE NULL END;\n IF v_status IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('estado de vencimento desconhecido: %L', v_status_v1));\n END IF;\n\n -- Um título que não existe (ou é de outro tenant) não derruba o vencimento: o\n -- vencimento é o dado, o vínculo é o enriquecimento. Perder o vínculo é ruim;\n -- perder o vencimento por causa dele seria pior.\n IF v_invoice IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.plg_financial_invoices\n WHERE id = v_invoice AND tenant_id = p_tenant) THEN\n v_meta := migration._deep_set(v_meta, ARRAY['v1', 'invoice_unresolved'], to_jsonb(v_invoice));\n v_invoice := NULL;\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bill_occurrences WHERE id = p_id) THEN\n INSERT INTO public.plg_financial_recurring_bill_occurrences\n (id, tenant_id, unit_id, recurring_bill_id, seq, competence_date, due_date,\n amount, paid_amount, status, overridden, invoice_id, notes, metadata)\n VALUES (p_id, p_tenant, v_unit, v_bill,\n coalesce((p_row ->> 'seq')::integer, 1),\n coalesce((p_row ->> 'competence_date')::date, (p_row ->> 'due_date')::date),\n (p_row ->> 'due_date')::date,\n coalesce((p_row ->> 'amount')::numeric, 0),\n coalesce((p_row ->> 'paid_amount')::numeric, 0),\n v_status, coalesce((p_row ->> 'overridden')::boolean, false),\n v_invoice, p_row ->> 'notes', v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_recurring_bill_occurrences SET\n unit_id = v_unit,\n seq = CASE WHEN p_row ? 'seq' THEN coalesce((p_row ->> 'seq')::integer, seq) ELSE seq END,\n competence_date = CASE WHEN p_row ? 'competence_date' THEN coalesce((p_row ->> 'competence_date')::date, competence_date) ELSE competence_date END,\n due_date = (p_row ->> 'due_date')::date,\n amount = CASE WHEN p_row ? 'amount' THEN coalesce((p_row ->> 'amount')::numeric, amount) ELSE amount END,\n paid_amount = CASE WHEN p_row ? 'paid_amount' THEN coalesce((p_row ->> 'paid_amount')::numeric, paid_amount) ELSE paid_amount END,\n status = v_status,\n overridden = CASE WHEN p_row ? 'overridden' THEN coalesce((p_row ->> 'overridden')::boolean, overridden) ELSE overridden END,\n invoice_id = coalesce(v_invoice, invoice_id),\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bill_occurrences', p_id, '{}'::jsonb); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- A leitura de contas a pagar, num lugar só\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- A pergunta que a tela faz é \"o que vence, e o que já foi pago?\", e respondê-la\n-- exige três junções e o conhecimento de que o pagamento mora no movimento, não\n-- no vencimento. Uma view põe esse conhecimento no banco em vez de em cada tela\n-- que vier a perguntar.\n-- \\`security_invoker\\` porque uma view sem ele roda com os direitos do DONO e\n-- atravessa a RLS das tabelas por baixo: quem abrisse esta leitura pelo\n-- PostgREST leria as contas a pagar de TODOS os inquilinos do cluster. É a\n-- postura N16 da bancada, e ela existe porque essa falha já aconteceu aqui.\nCREATE OR REPLACE VIEW public.v_financial_payables WITH (security_invoker = true) AS\nSELECT\n o.tenant_id,\n o.unit_id,\n o.id AS occurrence_id,\n b.id AS recurring_bill_id,\n b.description,\n b.counterparty_person_id,\n p.name AS counterparty_name,\n b.frequency,\n o.seq,\n o.competence_date,\n o.due_date,\n o.amount,\n -- O pagamento vem do movimento do título; \\`o.paid_amount\\` é 0 em tudo que veio\n -- do V1, e somar os dois contaria duas vezes o dia em que o V2 começar a\n -- escrever no vencimento.\n coalesce(o.paid_amount, 0) + coalesce(m.pago, 0) AS paid_amount,\n o.amount - (coalesce(o.paid_amount, 0) + coalesce(m.pago, 0)) AS open_amount,\n o.status,\n o.overridden,\n o.invoice_id,\n CASE\n WHEN o.status = 'cancelled' THEN 'cancelado'\n WHEN o.amount <= coalesce(o.paid_amount, 0) + coalesce(m.pago, 0) THEN 'pago'\n WHEN coalesce(m.pago, 0) > 0 THEN 'parcial'\n WHEN o.due_date < current_date THEN 'vencido'\n ELSE 'em aberto'\n END AS situacao\nFROM public.plg_financial_recurring_bill_occurrences o\nJOIN public.plg_financial_recurring_bills b ON b.id = o.recurring_bill_id\nLEFT JOIN public.people p ON p.id = b.counterparty_person_id\nLEFT JOIN LATERAL (\n SELECT sum(mv.paid_amount) AS pago\n FROM public.plg_financial_movements mv\n WHERE mv.financial_invoice_id = o.invoice_id\n) m ON o.invoice_id IS NOT NULL;\n\nCOMMENT ON VIEW public.v_financial_payables IS\n 'Contas a pagar por vencimento, com o pago somado do movimento do título. '\n 'No V1 recurring_bill_occurrences.paid_value é sempre 0: o pagamento mora no '\n 'movimento, e sem esta junção todo vencimento consolidado parece em aberto.';\n\nGRANT SELECT ON public.v_financial_payables TO authenticated, service_role;\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\n--\n-- Função nova nasce executável por PUBLIC, e \\`anon\\`/\\`authenticated\\` herdam\n-- dali. Estas são de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcançável pela chave anônima é a base inteira\n-- alcançável pela chave anônima. Só o service_role, que é quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_recurring_bill_occurrence(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_recurring_bill_occurrence(uuid, uuid, jsonb, jsonb) TO service_role;\n`\n\nexport const MIGRATION_063_A_UNIDADE_TEM_CONTA_E_A_PESSOA_TEM_UMA_SO = `-- A unidade tem conta, e a pessoa tem uma só.\n--\n-- Duas decisões sobre o razão financeiro, tomadas depois de medir o que cada uma\n-- custava. As duas estavam registradas como \"adiar\" com uma estimativa de custo,\n-- e a estimativa veio do cliente errado.\n--\n-- ── 1. \\`account_type = 'unit'\\` ───────────────────────────────────────────────\n--\n-- O plano de extração do V1 traduz o vocabulário de conta do V1 para as oito\n-- espécies do V2, e deixava \\`unit\\` passar sem tradução de propósito, com este\n-- comentário:\n--\n-- \"conta da unidade não é pessoa, nem banco, nem caixa. Encaixar em 'system'\n-- seria escolher a semântica no lugar de quem é dono do contrato\n-- financeiro. Custa 6 movimentos na onda 1.\"\n--\n-- A recusa em escolher estava certa. A estimativa não: os 6 movimentos são do\n-- Espaço Facial, uma rede de estética onde o dinheiro entra pela venda. Num\n-- restaurante o caixa passa pela conta da própria unidade, e no Pertinho do Céu\n-- (licença 8) DUAS contas travam **1.359 movimentos** — 30% do razão:\n--\n-- account_central #195 \"Pertinho do Céu\" → 777 movimentos\n-- account_central #654100 \"PC Serviços\" → 582 movimentos\n--\n-- E não é só a contagem: sem elas, \\`v_financial_payables\\` mostra 833 vencimentos\n-- em aberto num cliente que pagou R$ 311.006. O número certo some junto com a\n-- conta.\n--\n-- A escolha foi **criar a espécie**, não encaixar em \\`system\\`. \\`system\\` já tem\n-- dono semântico (\\`CHECK (kind <> 'system' OR code IS NOT NULL)\\` — conta de\n-- plano de contas, identificada por código) e jogar a conta da casa lá dentro\n-- faria os dois conceitos compartilharem um balde que nenhum relatório consegue\n-- separar depois.\n--\n-- ── 2. Uma conta por pessoa ──────────────────────────────────────────────────\n--\n-- \\`plg_financial_accounts_person_uq\\` diz que uma pessoa tem UMA conta no tenant.\n-- O V1 permite várias (48 casos no Pertinho do Céu), e cada colisão derrubava a\n-- conta E todos os movimentos que a referenciavam.\n--\n-- A constraint FICA. A decisão foi **fundir**: da segunda conta em diante, o\n-- writer devolve o id da conta que já existe, o ledger amarra a linha do V1 a\n-- ela, e todo movimento daquela conta passa a apontar para a conta única da\n-- pessoa. O saldo da pessoa fica certo; o que se perde é a separação que o V1\n-- fazia entre as contas dela, e isso fica escrito em\n-- \\`metadata.migration.merged_from\\`.\n--\n-- Fundir é o mesmo mecanismo que \\`upsert_person\\` já usa para pessoa duplicada, e\n-- é reversível pela leitura do ledger: \\`source_numeric_id\\` distintos apontando\n-- para o mesmo \\`target_id\\` é a marca da fusão.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. A espécie nova\n-- ─────────────────────────────────────────────────────────────────────────────\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_kind_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_kind_check\n -- \\`unidentified\\` JUNTO: a 037 já o cria (\"Cliente não identificado\" e\n -- \"Múltiplos participantes\", a venda de balcão sem quem a levou), e a 066\n -- o repõe três arquivos adiante. Sem ele aqui, esta lista REGRIDE o\n -- vocabulário por três arquivos — e num banco vazio isso não aparece,\n -- porque não há linha para violar a restrição. Num cluster vivo aparece\n -- na hora: \\`23514 ... is violated by some row\\`, medido com 2 contas.\n CHECK (kind = ANY (ARRAY['bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit',\n 'card_payable', 'person', 'system', 'unit',\n 'unidentified']));\n\n -- Uma conta de unidade sem unidade seria a conta de ninguém. O par espécie ↔\n -- coluna obrigatória é a mesma disciplina que \\`person\\`/\\`person_id\\` já tem.\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_unit_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_unit_check\n CHECK (kind <> 'unit' OR unit_id IS NOT NULL);\nEND $$;\n\nCOMMENT ON CONSTRAINT plg_financial_accounts_unit_check ON public.plg_financial_accounts IS\n 'A conta da própria unidade (a conta da casa) nomeia a unidade a que pertence. '\n 'Sem isso ela seria indistinguível de uma conta de sistema sem dono.';\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O writer\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L — V1 account_type does not map onto the ledger''s nine', v_kind));\n END IF;\n\n -- A pessoa da conta polimórfica. O ledger é a única fonte que sabe o de-para,\n -- e ele guarda a tabela de origem — então \\`clients\\`, \\`contacts\\` e\n -- \\`professionals\\` resolvem pelo mesmo caminho, sem o writer conhecer nenhuma.\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = (v_meta #>> '{v1,reference_table}')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n -- Recusar é o certo, e é temporário: a pessoa entra na mesma onda, e a\n -- segunda passada oferece a conta de novo. Inventar uma pessoa aqui seria\n -- pendurar dinheiro em quem não existe.\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda não migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', '<sem tabela>'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n\n -- FUSÃO. O V2 admite uma conta por pessoa e o V1 admite várias; da segunda\n -- em diante a conta do V1 passa a APONTAR para a que já existe, em vez de\n -- derrubar a si mesma e a todo movimento que a referencia.\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n -- A unidade da conta da casa, pelo mesmo caminho da pessoa: o ledger sabe qual\n -- \\`app.units\\` a \\`companies\\` do V1 virou.\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_meta #>> '{v1,reference_table}', 'companies')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda não migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', 'companies'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n -- Enriquece a conta que fica e devolve O ID DELA. O motor grava esse\n -- target_id no ledger, e a partir daí todo movimento que referenciava a\n -- conta do V1 resolve para a conta única da pessoa no V2.\n UPDATE public.plg_financial_accounts SET\n is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true),\n metadata = migration._merge_meta(\n metadata,\n migration._deep_set(v_meta, ARRAY['migration', 'merged_from'],\n coalesce(metadata #> '{migration,merged_from}', '[]'::jsonb)\n || jsonb_build_array(jsonb_build_object(\n 'v1_account', v_meta #> '{v1,reference_id}',\n 'name', p_row ->> 'name', 'account_id', p_id))))\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta única da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code',\n p_row ->> 'name',\n p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true),\n v_person,\n v_meta);\n v_created := true;\n ELSE\n -- kind e person_id andam JUNTOS: a constraint amarra os dois, então mudar\n -- um sem o outro derruba a linha que já estava lá.\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\n--\n-- Função nova nasce executável por PUBLIC, e \\`anon\\`/\\`authenticated\\` herdam\n-- dali. Estas são de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcançável pela chave anônima é a base inteira\n-- alcançável pela chave anônima. Só o service_role, que é quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) TO service_role;\n`\n\nexport const MIGRATION_064_A_CLASSIFICACAO_CONTABIL_CHEGA_INTEIRA = `-- A classificação contábil chega inteira.\n--\n-- Sem esta migration não existe DRE no V2 — e o motivo não é a tela. O DRE do V1\n-- é montado sobre DOIS eixos, e os dois se perdiam no caminho:\n--\n-- 1. a NATUREZA da conta (receita × despesa)\n-- 2. a CONTA CONTÁBIL DO ITEM (qual linha do DRE aquele lançamento é)\n--\n-- ── Eixo 1: a natureza estava na coluna errada ───────────────────────────────\n--\n-- O de-para mapeava \\`chart_of_accounts.account_type → node_type\\`. São coisas\n-- diferentes: no V1 \\`account_type\\` é a NATUREZA (asset|liability|equity|revenue|\n-- expense); no V2 \\`node_type\\` é a FORMA DO NÓ (group|leaf) e \\`account_type\\` é a\n-- natureza. O resultado, medido no cluster em 18/09/2026:\n--\n-- node_type = 'expense' → 302 linhas account_type = NULL\n-- node_type = 'revenue' → 38 linhas account_type = NULL\n-- node_type = 'leaf' → 244 linhas account_type preenchido\n--\n-- As 244 corretas são as que alguém criou pelo app; as 340 erradas são as que o\n-- porte escreveu. E \\`upsert_fin_chart_account\\` **nunca gravou \\`account_type\\`** —\n-- a coluna não aparece no INSERT nem no UPDATE dele.\n--\n-- Um DRE sem natureza não separa receita de despesa. Não erra por pouco: não\n-- monta.\n--\n-- ── Eixo 2: a conta do item virava texto opaco ───────────────────────────────\n--\n-- \\`invoice_items.account_id → jsonb:financial.v1_account_ref\\`. A classificação\n-- contábil, que no V1 é do ITEM e não do movimento, chegava ao V2 como o id\n-- numérico cru do V1 dentro de \\`metadata\\` — sem FK, sem join possível. E não\n-- havia para onde resolver: \\`public.items\\` não tinha coluna de conta contábil.\n--\n-- (\\`plg_financial_invoice_items.chart_account_id\\` existe e tem ZERO linhas: o\n-- item do V1 cai só em \\`public.items\\`; não há split financeiro para ele.)\n--\n-- ── O terceiro eixo NÃO precisou de nada ─────────────────────────────────────\n--\n-- A direção (receita × despesa do documento) parecia perdida também —\n-- \\`invoices.type → jsonb:financial.v1_type\\`. Não está: o writer \\`order\\` devolve\n-- em \\`extra\\` o TÍTULO em \\`plg_financial_invoices\\`, e lá \\`direction\\` já chega\n-- certa. Medido: 8.175 \\`receivable\\` + 2.503 \\`payable\\`. O DRE lê o título, não o\n-- pedido, e o eixo já existe.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. O item ganha onde guardar a classificação\n-- ─────────────────────────────────────────────────────────────────────────────\n\nALTER TABLE public.items\n ADD COLUMN IF NOT EXISTS chart_account_id uuid REFERENCES public.plg_financial_chart_of_accounts(id),\n ADD COLUMN IF NOT EXISTS cost_center_id uuid REFERENCES public.plg_financial_cost_centers(id);\n\nCOMMENT ON COLUMN public.items.chart_account_id IS\n 'Conta contábil DESTE item. A classificação é da linha, não do documento: no '\n 'V1 dois itens da mesma venda podem cair em ramos diferentes do DRE, e é essa '\n 'granularidade que o relatório precisa.';\nCOMMENT ON COLUMN public.items.cost_center_id IS\n 'Centro de custo do item. O DRE não usa — é a segunda dimensão, para análise '\n 'de custo — mas perder o dado na entrada seria irreversível.';\n\n-- A pergunta do DRE é \"some os itens por conta, no período\". Sem índice ela\n-- varre \\`items\\` inteira por linha do relatório.\nCREATE INDEX IF NOT EXISTS items_chart_account_idx\n ON public.items (tenant_id, chart_account_id) WHERE chart_account_id IS NOT NULL;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. Conserto do dado já migrado\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- Devolve a natureza para a coluna certa nas linhas que o porte escreveu. O\n-- teste é o próprio vocabulário: \\`node_type\\` só pode ser group|leaf|account, e\n-- qualquer outra coisa ali é natureza que foi parar no lugar errado.\nUPDATE public.plg_financial_chart_of_accounts\n SET account_type = node_type,\n node_type = 'leaf'\n WHERE node_type IN ('asset', 'liability', 'equity', 'revenue', 'expense')\n AND account_type IS NULL;\n\n-- \\`node_type\\` de verdade: é grupo quem tem filho. O V1 não tem essa coluna, e\n-- derivá-la da própria árvore é mais honesto que herdar um default.\nUPDATE public.plg_financial_chart_of_accounts p\n SET node_type = 'group'\n WHERE node_type <> 'group'\n AND EXISTS (SELECT 1 FROM public.plg_financial_chart_of_accounts c WHERE c.parent_id = p.id);\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 3. Os writers\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_chart_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_type text := nullif(btrim(lower(coalesce(p_row ->> 'account_type', ''))), '');\n v_parent uuid := (p_row ->> 'parent_id')::uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n\n -- Sem correção silenciosa: uma natureza fora do vocabulário do razão faria a\n -- conta sumir do DRE sem aparecer em lugar nenhum — nem na receita, nem na\n -- despesa, nem num aviso.\n IF v_type IS NOT NULL AND v_type NOT IN ('asset', 'liability', 'equity', 'revenue', 'expense') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('natureza de conta desconhecida: %L (o razão conhece asset, liability, equity, revenue e expense)', v_type));\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_chart_of_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_chart_of_accounts\n (id, tenant_id, code, name, node_type, account_type, parent_id, is_active)\n VALUES (p_id, p_tenant, p_row ->> 'code', p_row ->> 'name',\n -- Nasce folha; vira grupo abaixo, quando o primeiro filho chegar.\n coalesce(p_row ->> 'node_type', 'leaf'),\n v_type, v_parent,\n coalesce((p_row ->> 'is_active')::boolean, true));\n v_created := true;\n ELSE\n UPDATE public.plg_financial_chart_of_accounts SET\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n node_type = CASE WHEN p_row ? 'node_type' THEN coalesce(p_row ->> 'node_type', node_type) ELSE node_type END,\n account_type = coalesce(v_type, account_type),\n parent_id = CASE WHEN p_row ? 'parent_id' THEN v_parent ELSE parent_id END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END\n WHERE id = p_id;\n END IF;\n\n -- Quem tem filho é grupo. Marcar aqui, e não num passe no fim, faz a árvore\n -- ficar correta mesmo se a onda parar no meio.\n IF v_parent IS NOT NULL THEN\n UPDATE public.plg_financial_chart_of_accounts\n SET node_type = 'group'\n WHERE id = v_parent AND tenant_id = p_tenant AND node_type <> 'group';\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_chart_of_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- O writer do item passa a gravar as duas colunas novas. O resto do corpo é o\n-- que já estava lá — está reemitido inteiro porque é assim que se troca uma\n-- função no Postgres, não porque mudou.\nCREATE OR REPLACE FUNCTION migration.upsert_order_item(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_order uuid := (p_row ->> 'order_id')::uuid;\n v_chart uuid := (p_row ->> 'chart_account_id')::uuid;\n v_center uuid := (p_row ->> 'cost_center_id')::uuid;\n v_order_tenant uuid;\n v_existing uuid;\n v_cancelled timestamptz;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_order IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an order line needs an id and an order');\n END IF;\n SELECT tenant_id INTO v_order_tenant FROM public.orders WHERE id = v_order;\n IF v_order_tenant IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk', 'reason', format('order %s is not in the destination yet', v_order));\n END IF;\n IF v_order_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order %s belongs to another tenant', v_order));\n END IF;\n\n v_cancelled := nullif(v_meta #>> '{cancellation,at}', '')::timestamptz;\n\n SELECT tenant_id INTO v_existing FROM public.items WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order line %s already exists in another tenant', p_id));\n END IF;\n IF v_existing IS NULL THEN\n -- \\`order_id\\` e SÓ ele: a fatura alcança a linha pelo pedido (items_one_parent).\n INSERT INTO public.items (id, tenant_id, order_id, product_id, name, description,\n quantity, unit_price, discount, total, sort_order,\n cancelled_at, cancel_reason, chart_account_id, cost_center_id, metadata)\n VALUES (p_id, p_tenant, v_order, (p_row ->> 'product_id')::uuid,\n coalesce(nullif(btrim(coalesce(p_row ->> 'name', '')), ''), 'Item'),\n coalesce(p_row ->> 'description', nullif(btrim(coalesce(p_row ->> 'name', '')), '')),\n greatest(coalesce((p_row ->> 'quantity')::numeric, 1), 0.0001),\n round(coalesce((p_row ->> 'unit_price')::numeric, 0), 2),\n round(coalesce((p_row ->> 'discount')::numeric, 0), 2),\n round(coalesce((p_row ->> 'total')::numeric, 0), 2),\n coalesce((p_row ->> 'sort_order')::integer, 0),\n v_cancelled, v_meta #>> '{cancellation,reason}',\n v_chart, v_center, v_meta);\n v_created := true;\n ELSE\n UPDATE public.items SET\n order_id = v_order,\n product_id = CASE WHEN p_row ? 'product_id' THEN (p_row ->> 'product_id')::uuid ELSE product_id END,\n name = CASE WHEN p_row ? 'name' THEN coalesce(nullif(btrim(p_row ->> 'name'), ''), name) ELSE name END,\n quantity = CASE WHEN p_row ? 'quantity' THEN greatest(coalesce((p_row ->> 'quantity')::numeric, 1), 0.0001) ELSE quantity END,\n unit_price = CASE WHEN p_row ? 'unit_price' THEN round(coalesce((p_row ->> 'unit_price')::numeric, 0), 2) ELSE unit_price END,\n discount = CASE WHEN p_row ? 'discount' THEN round(coalesce((p_row ->> 'discount')::numeric, 0), 2) ELSE discount END,\n total = CASE WHEN p_row ? 'total' THEN round(coalesce((p_row ->> 'total')::numeric, 0), 2) ELSE total END,\n cancelled_at = coalesce(v_cancelled, cancelled_at),\n cancel_reason = coalesce(v_meta #>> '{cancellation,reason}', cancel_reason),\n -- \\`coalesce\\` e não CASE: a classificação é enriquecimento. Uma reoferta\n -- sem a coluna não deve apagar a conta que já está lá.\n chart_account_id = coalesce(v_chart, chart_account_id),\n cost_center_id = coalesce(v_center, cost_center_id),\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.items', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 4. O de-para\n-- ─────────────────────────────────────────────────────────────────────────────\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\n-- \\`account_type\\` para \\`account_type\\`. O mapa anterior mandava a natureza para\n-- \\`node_type\\`, e é a correção que faz o DRE existir.\nSELECT migration._allow(\n 'chart_of_accounts', 'public.plg_financial_chart_of_accounts', 'financial',\n 'fin_chart_account', '{}'::jsonb, ARRAY[]::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'code', 'code',\n 'name', 'name',\n 'account_type', 'account_type',\n 'parent_id', 'parent_id@chart_of_accounts',\n 'active', 'is_active'\n ),\n 'Plano de contas. \\`account_type\\` é a NATUREZA (o eixo do DRE); \\`node_type\\` é '\n 'derivado da árvore pelo writer — quem tem filho é grupo.'\n);\n\n-- O item passa a resolver a conta contábil e o centro de custo em vez de\n-- carregá-los como id cru do V1 em metadata.\nSELECT migration._allow(\n 'invoice_items', 'public.items', 'financial',\n 'order_item', '{}'::jsonb, ARRAY['unit_price', 'total_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'invoice_id', 'order_id@invoices:public.orders',\n 'description', 'name',\n 'quantity', 'quantity',\n 'unit_price', 'unit_price',\n 'discount', 'discount',\n 'total_amount', 'total',\n 'additional', 'jsonb:financial.additional',\n 'product_id', 'product_id@products',\n 'service_id', 'product_id@services',\n 'account_id', 'chart_account_id@chart_of_accounts',\n 'cost_center_id', 'cost_center_id@cost_centers',\n 'item_type', 'jsonb:item_type',\n 'canceled_at', 'jsonb:cancellation.at',\n 'cancel_reason', 'jsonb:cancellation.reason',\n 'appointment_id', 'jsonb:agenda.v1_appointment_ref',\n 'execution_date', 'jsonb:execution.date',\n 'execution_start_time', 'jsonb:execution.start_time',\n 'execution_end_time', 'jsonb:execution.end_time',\n 'execution_photos', '-',\n 'executed_by_professional_id', 'jsonb:execution.v1_professional_ref',\n 'is_executed', 'jsonb:execution.is_executed',\n 'gift_card_id', 'jsonb:v1_gift_card_ref',\n 'service_package_id', 'jsonb:v1_package_ref'\n ),\n 'A linha da venda. \\`account_id\\` é a classificação contábil DO ITEM — o eixo '\n 'que o DRE agrupa; antes ela vinha como id cru do V1 em metadata.'\n);\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\n--\n-- Função nova nasce executável por PUBLIC, e \\`anon\\`/\\`authenticated\\` herdam\n-- dali. Estas são de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcançável pela chave anônima é a base inteira\n-- alcançável pela chave anônima. Só o service_role, que é quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_chart_account(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_chart_account(uuid, uuid, jsonb, jsonb) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_order_item(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_order_item(uuid, uuid, jsonb, jsonb) TO service_role;\n`\n\nexport const MIGRATION_065_A_FUSAO_PARA_DE_DOBRAR_A_PROPRIA_MEMORIA = `-- A fusão para de dobrar a própria memória.\n--\n-- Correção de um defeito que eu introduzi na 048 e que derrubou o cluster quatro\n-- vezes em 18/09/2026.\n--\n-- ── O que eu escrevi ─────────────────────────────────────────────────────────\n--\n-- Na fusão de conta de pessoa, para registrar de onde a conta veio:\n--\n-- metadata = migration._merge_meta(\n-- metadata,\n-- migration._deep_set(v_meta, ARRAY['migration','merged_from'],\n-- coalesce(metadata #> '{migration,merged_from}', '[]'::jsonb)\n-- || jsonb_build_array(...)))\n--\n-- Leio o array que já está na linha, acrescento um elemento — e entrego o\n-- resultado a \\`_merge_meta\\`, que **concatena com o array que já está na linha de\n-- novo**. Cada oferta não acrescenta um: ela DOBRA.\n--\n-- oferta 1 → 1 elemento\n-- oferta 5 → 16\n-- oferta 10 → 512\n-- oferta 18 → 262.143 ← medido\n--\n-- ── O que isso custou ────────────────────────────────────────────────────────\n--\n-- Medido antes deste conserto, no tenant Pertinho do Céu:\n--\n-- maior merged_from 262.143 elementos (2¹⁸ − 1)\n-- maior metadata 1.378 kB numa única linha\n--\n-- Montar e mesclar um jsonb desse tamanho, uma vez por linha, dentro de um\n-- statement, num cluster com \\`shared_buffers\\` de 224 MB, mata o backend. E o\n-- sintoma não apontava para cá: a conexão caía no meio de \\`account_central\\`, o\n-- transporte reportava \"server closed the connection unexpectedly\", e eu passei\n-- duas rodadas reduzindo o tamanho do lote — de 2.000 para 250, depois para 100.\n-- Nenhuma ajudou, porque 417 linhas nunca foram o problema.\n--\n-- ── O conserto ───────────────────────────────────────────────────────────────\n--\n-- \\`merged_from\\` sai. Não vira \"append idempotente\", não vira \"array com limite\":\n-- sai inteiro, porque ele nunca deveria ter existido — **o ledger já registra a\n-- fusão**, e melhor: dois \\`source_numeric_id\\` distintos apontando para o mesmo\n-- \\`target_id\\` É a definição de fusão, é consultável, e é o lugar onde o resto\n-- deste schema já procura essa resposta.\n--\n-- select source_numeric_id, target_id\n-- from migration.ledger\n-- where source_table = 'account_central' and target_table = 'public.plg_financial_accounts'\n-- order by target_id;\n--\n-- Guardar a mesma informação num jsonb ao lado era a segunda cópia da regra —\n-- exatamente o que o cabeçalho do \\`sdkmig.py\\` diz para não fazer. A segunda\n-- cópia divergiu no primeiro dia, e de forma exponencial.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. Desarmar o que já explodiu\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- Primeiro o dado, depois a função: enquanto as linhas gordas estiverem lá,\n-- qualquer rodada que as toque derruba o banco de novo — inclusive a que\n-- aplicaria esta migration mais adiante.\nUPDATE public.plg_financial_accounts\n SET metadata = (coalesce(metadata, '{}'::jsonb) #- '{migration,merged_from}')\n WHERE metadata #> '{migration,merged_from}' IS NOT NULL;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O writer para de acumular\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L — V1 account_type does not map onto the ledger''s nine', v_kind));\n END IF;\n\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = (v_meta #>> '{v1,reference_table}')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda não migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', '<sem tabela>'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_meta #>> '{v1,reference_table}', 'companies')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda não migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', 'companies'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n -- A fusão NÃO escreve histórico aqui. Quem registra que duas contas do V1\n -- viraram uma no V2 é o ledger: dois \\`source_numeric_id\\` apontando para o\n -- mesmo \\`target_id\\`. Acumular a mesma informação num array de metadata foi o\n -- defeito que dobrava de tamanho a cada oferta e derrubou o cluster.\n UPDATE public.plg_financial_accounts\n SET is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true)\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta única da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code', p_row ->> 'name', p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true), v_person, v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\n--\n-- Função nova nasce executável por PUBLIC, e \\`anon\\`/\\`authenticated\\` herdam\n-- dali. Estas são de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcançável pela chave anônima é a base inteira\n-- alcançável pela chave anônima. Só o service_role, que é quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) TO service_role;\n`\n\nexport const MIGRATION_066_A_VENDA_DE_BALCAO_TEM_CONTA = `-- A venda de balcão tem conta.\n--\n-- Terceira espécie de conta que o V1 tem e o razão do V2 não tinha — depois de\n-- \\`unit\\` (a conta da casa, migration 048) vem \\`unidentified\\`: a conta da venda\n-- para quem não deixou cadastro.\n--\n-- No V1 ela é uma linha de \\`account_central\\` com \\`account_type = 'client'\\` e\n-- \\`reference_id = 0\\`. O tipo diz \"pessoa\"; a referência é o sentinela de \"não\n-- há pessoa\". O V2 não aceita a combinação, e com razão:\n--\n-- CHECK ((kind = 'person') = (person_id IS NOT NULL))\n--\n-- Uma conta de pessoa sem pessoa é dinheiro pendurado em ninguém. O CHECK está\n-- certo; o que faltava era a palavra para o caso em que **não há contraparte e\n-- isso é o fato**, não uma falha de cadastro.\n--\n-- ── Por que espécie própria, e não \\`system\\` ──────────────────────────────────\n--\n-- \\`system\\` já tem dono: é a conta do plano de contas, identificada por código\n-- (\\`CHECK (kind <> 'system' OR code IS NOT NULL)\\`). Jogar a venda de balcão lá\n-- dentro faria dois conceitos dividirem um balde que nenhum relatório separa\n-- depois — e o de balcão não é detalhe: num restaurante ele é a maior parte do\n-- movimento.\n--\n-- \\`unidentified\\` diz a verdade: existe contraparte, ela comprou, e ela não se\n-- identificou. É diferente de \\`system\\` (não há contraparte) e de \\`person\\` (há, e\n-- sabemos quem).\n--\n-- ── O que estava preso ───────────────────────────────────────────────────────\n--\n-- Medido em 18/09/2026 no tenant Pertinho do Céu, antes deste arquivo:\n--\n-- account_central em quarentena por \\`clients #0\\` 5\n-- financial_movements travados atrás delas 110\n-- valor R$ 15.102,78\n--\n-- A asserção de paridade (\\`make parity\\`) apontava exatamente isso: contas a\n-- pagar com Δ −10.218,30 contra a origem, e a cadeia inteira levava a uma conta.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. A espécie\n-- ─────────────────────────────────────────────────────────────────────────────\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_kind_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_kind_check\n CHECK (kind = ANY (ARRAY['bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit',\n 'card_payable', 'person', 'system', 'unit',\n 'unidentified']));\nEND $$;\n\nCOMMENT ON COLUMN public.plg_financial_accounts.kind IS\n 'bank | cash_physical | cash_session | card_receivable_credit | '\n 'card_receivable_debit | card_payable | person | system | unit | unidentified. '\n '\\`unidentified\\` é a contraparte que comprou e não se identificou (venda de '\n 'balcão): diferente de \\`system\\`, onde não há contraparte, e de \\`person\\`, onde '\n 'há e sabemos quem.';\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O writer reconhece o sentinela\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- Só a perna que muda: quando a conta é de pessoa e a referência da origem é o\n-- sentinela (\\`reference_id = 0\\`) ou aponta para linha que não existe mais, ela\n-- deixa de ser \\`person\\` e passa a ser \\`unidentified\\` — em vez de recusar a conta\n-- e, com ela, todo movimento que a cita.\n--\n-- A distinção importa e está no código: referência AUSENTE (0) vira\n-- \\`unidentified\\`; referência PRESENTE cuja pessoa ainda não migrou continua\n-- caindo em quarentena de \\`fk\\`, porque aí a pessoa existe e a ordem é que está\n-- errada — e a segunda passada resolve.\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_ref_id bigint := nullif(v_meta #>> '{v1,reference_id}', '')::bigint;\n v_ref_table text := v_meta #>> '{v1,reference_table}';\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n\n -- O sentinela ANTES da checagem de vocabulário: \\`clients#0\\` não é uma conta de\n -- pessoa mal formada, é uma conta sem contraparte.\n IF v_kind = 'person' AND coalesce(v_ref_id, 0) = 0 THEN\n v_kind := 'unidentified';\n END IF;\n\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit',\n 'unidentified') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L — V1 account_type does not map onto the ledger''s ten', v_kind));\n END IF;\n\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = v_ref_table\n AND l.source_numeric_id = v_ref_id\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda não migrou (%s #%s)',\n coalesce(v_ref_table, '<sem tabela>'), coalesce(v_ref_id::text, '<sem id>')));\n END IF;\n\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_ref_table, 'companies')\n AND l.source_numeric_id = v_ref_id\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda não migrou (%s #%s)',\n coalesce(v_ref_table, 'companies'), coalesce(v_ref_id::text, '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n UPDATE public.plg_financial_accounts\n SET is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true)\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta única da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code', p_row ->> 'name', p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true), v_person, v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\n--\n-- Função nova nasce executável por PUBLIC, e \\`anon\\`/\\`authenticated\\` herdam\n-- dali. Estas são de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcançável pela chave anônima é a base inteira\n-- alcançável pela chave anônima. Só o service_role, que é quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) TO service_role;\n`\n\nexport const MIGRATION_067_O_RECIBO_DA_CONTA_ATRAVESSA = `-- O recibo da conta atravessa.\n--\n-- \\`bill_attachments\\` — o comprovante que alguém anexou a uma conta a pagar —\n-- estava em \\`migration.excluded_tables\\` com esta razão:\n--\n-- Not a row destination: bill attachments are FILES. 091 pointed them at\n-- \\`storage\\`, which is object storage and not something a row writer\n-- reaches. Recorded as a decision rather than left pending forever (#113).\n--\n-- A frase está certa sobre os BYTES e errada sobre o resto. Um anexo tem duas\n-- partes: o arquivo, que mora em object storage e nenhum writer de linha\n-- alcança, e o REGISTRO — nome, tamanho, tipo, quem subiu, quando, e a que\n-- conta pertence. O registro é linha, e é ele que faz o clipe aparecer na tela\n-- e o \\`make parity\\` conseguir contar anexo.\n--\n-- Medido em 19/09/2026, tenant Pertinho do Céu: **1.192 arquivos em 854 contas**,\n-- nenhum atravessou. E a asserção de paridade não pegou, porque a fatura que eu\n-- escolhi para testar tem zero anexos dos dois lados — passou comparando nada\n-- com nada.\n--\n-- ── Onde eles moram no V2 ───────────────────────────────────────────────────\n--\n-- \\`public.documents\\`, que é o arquivo de documento do core e já tem 1.382\n-- linhas neste tenant. Ele foi feito exatamente para isto: \\`subject_type\\` +\n-- \\`subject_id\\` dizem a que o documento pertence, e as colunas de storage\n-- (\\`storage_bucket\\`, \\`storage_path\\`, \\`checksum\\`) esperam os bytes.\n--\n-- O \\`kind\\` é \\`attachment\\`, que já está registrado em \\`document_kinds\\` — a FK\n-- existe e recusa espécie inventada, o que é o certo: um vocabulário de\n-- documento que cresce sozinho vira um depósito.\n--\n-- ── O que esta migration NÃO faz ────────────────────────────────────────────\n--\n-- Não move um byte. \\`storage_path\\` recebe o caminho DO V1 e \\`storage_provider\\`\n-- fica marcado como pendente, para que o estágio de bytes (\\`make mig-assets\\`)\n-- saiba o que buscar e para que ninguém confunda \"o registro chegou\" com \"o\n-- arquivo abre\". Um documento cujo link não abre é pior que um documento\n-- ausente: o ausente manda procurar no V1.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. O writer\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_document(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n -- \\`writer_args\\` diz a que ESPÉCIE e a que SUJEITO estas linhas pertencem, e é\n -- por isso que o mesmo writer serve anexo de conta, foto de ponto e contrato\n -- de pessoa sem um \\`IF\\` por origem aqui dentro.\n v_kind text := coalesce(p_args ->> 'kind', 'attachment');\n v_subject_type text := p_args ->> 'subject_type';\n v_subject uuid := (p_row ->> 'subject_id')::uuid;\n v_path text := nullif(btrim(coalesce(p_row ->> 'storage_path', '')), '');\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'um documento precisa de id');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.document_kinds k WHERE k.kind = v_kind) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('espécie de documento %L não registrada em document_kinds', v_kind));\n END IF;\n -- O anexo existe PARA estar preso a alguma coisa. Sem sujeito ele entra no\n -- arquivo e não aparece em tela nenhuma — o que é a mesma coisa que não\n -- entrar, com a diferença de ninguém ficar sabendo.\n IF v_subject_type IS NOT NULL AND v_subject IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('anexo cujo %s ainda não atravessou', v_subject_type));\n END IF;\n IF v_path IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'anexo sem caminho no storage — não há o que buscar depois');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.documents WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.documents\n (id, tenant_id, kind, title, file_name, file_size, mime_type,\n storage_provider, storage_path, subject_type, subject_id, status, is_active, metadata)\n VALUES (p_id, p_tenant, v_kind,\n coalesce(nullif(btrim(p_row ->> 'file_name'), ''), 'Anexo'),\n p_row ->> 'file_name',\n (p_row ->> 'file_size')::integer,\n p_row ->> 'mime_type',\n -- \\`v1-pending\\`: o REGISTRO chegou, o arquivo não. Quem lê a coluna\n -- sabe que o link ainda não abre, e o estágio de bytes sabe o que\n -- ainda tem para buscar.\n 'v1-pending',\n v_path, v_subject_type, v_subject,\n 'active', true, v_meta);\n v_created := true;\n ELSE\n UPDATE public.documents SET\n kind = v_kind,\n file_name = CASE WHEN p_row ? 'file_name' THEN p_row ->> 'file_name' ELSE file_name END,\n file_size = CASE WHEN p_row ? 'file_size' THEN (p_row ->> 'file_size')::integer ELSE file_size END,\n mime_type = CASE WHEN p_row ? 'mime_type' THEN p_row ->> 'mime_type' ELSE mime_type END,\n subject_type = coalesce(v_subject_type, subject_type),\n subject_id = coalesce(v_subject, subject_id),\n -- O caminho só se reescreve enquanto os bytes não vieram: depois de o\n -- arquivo subir, quem manda no caminho é o storage do V2.\n storage_path = CASE WHEN storage_provider = 'v1-pending' THEN v_path ELSE storage_path END,\n metadata = migration._merge_meta(metadata, v_meta),\n updated_at = now()\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.documents', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O vocabulário de writers aceita o nome novo\n-- ─────────────────────────────────────────────────────────────────────────────\n\nDO $$\nDECLARE v_def text;\nBEGIN\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n IF v_def IS NULL OR strpos(v_def, 'document') > 0 THEN RETURN; END IF;\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''person''::text',\n '''person''::text, ''document''::text');\nEND $$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 3. Sai da lista de excluídas e entra no plano\n-- ─────────────────────────────────────────────────────────────────────────────\n\nDELETE FROM migration.excluded_tables WHERE source_table = 'bill_attachments';\n\n-- A allowlist é CONGELADA (\\`trg_allowlist_frozen\\`): mudança de core-A é\n-- arquivo de migration, não escrita solta — e este é o arquivo. Sem a\n-- escotilha, a PRIMEIRA passada passa (a linha ainda não existia) e a\n-- SEGUNDA falha: é assim que uma migration deixa de ser replay-safe sem\n-- ninguém ver. \\`true\\` no terceiro argumento: vale só nesta transação.\nSELECT set_config('migration.allowlist_unlock', 'on', true);\n\nSELECT migration._allow(\n 'bill_attachments', 'public.documents', 'financial',\n 'document',\n jsonb_build_object('kind', 'attachment', 'subject_type', 'financial_invoice'),\n ARRAY[]::text[],\n jsonb_build_object(\n 'uuid', 'id',\n -- O sujeito: a conta a que o recibo pertence. \\`invoice_numeric_id\\` é o id\n -- do V1; o ledger devolve o título do V2.\n 'invoice_numeric_id', 'subject_id@invoices:public.plg_financial_invoices',\n 'file_name', 'file_name',\n 'file_size', 'file_size',\n 'content_type', 'mime_type',\n 'file_path', 'storage_path',\n 'uploaded_at', 'jsonb:v1.uploaded_at',\n 'uploaded_by', 'jsonb:v1.uploaded_by',\n 'scope', 'jsonb:v1.scope',\n 'bill_id', 'jsonb:v1.bill_ref',\n 'invoice_id', '-'\n ),\n 'O recibo da conta. A LINHA vem por aqui; os BYTES são o estágio de assets — '\n '\\`storage_provider = ''v1-pending''\\` é o que os separa, para que ninguém '\n 'confunda \"o registro chegou\" com \"o arquivo abre\".'\n);\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\n--\n-- Função nova nasce executável por PUBLIC, e \\`anon\\`/\\`authenticated\\` herdam\n-- dali. Estas são de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcançável pela chave anônima é a base inteira\n-- alcançável pela chave anônima. Só o service_role, que é quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_document(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_document(uuid, uuid, jsonb, jsonb) TO service_role;\n\nSELECT set_config('migration.allowlist_unlock', 'off', true);\n`\n\nexport const MIGRATION_068_A_UNIDADE_ENTRA_COM_A_LINHA_NAO_DEPOIS_DELA = `-- A unidade entra COM a linha, não num UPDATE depois dela.\n--\n-- Todo writer deste motor termina do mesmo jeito:\n--\n-- INSERT INTO <destino> (… sem unit_id …);\n-- PERFORM migration._set_unit('<destino>', p_id, <unidade>);\n--\n-- e \\`_set_unit\\` era um UPDATE incondicional. Duas coisas erradas na mesma\n-- linha: ele reescreve uma linha que acabou de ser escrita, e reescreve\n-- mesmo quando o valor não muda.\n--\n-- ── O preço ─────────────────────────────────────────────────────────────────\n--\n-- Medido em 19/09/2026 no tenant Espaço Facial, com EXPLAIN ANALYZE de um\n-- \\`UPDATE orders SET unit_id = unit_id\\` — um UPDATE que, por construção, não\n-- muda valor nenhum:\n--\n-- Update on orders rows=0\n-- Trigger for constraint orders_kind_registered: time=39.738\n-- Trigger for constraint orders_party_id_fkey: time=398.910\n-- Trigger for constraint orders_tenant_id_fkey: time=646.595\n-- Trigger for constraint orders_unit_fk: time=275.950\n-- Execution Time: 1371.357 ms\n--\n-- **1,37 segundo para não mudar nada.** O trabalho não está na escrita: está\n-- na revalidação das chaves estrangeiras e na reescrita dos onze índices de\n-- \\`orders\\`, porque uma nova versão da linha precisa aparecer em todos eles.\n--\n-- E o custo das FKs é de CONTENÇÃO, não de busca: a checagem de\n-- \\`orders_tenant_id_fkey\\` tranca a MESMA linha de \\`public.tenants\\` a cada\n-- escrita do tenant. Uma sondagem de índice que deveria custar microssegundos\n-- passa a esperar.\n--\n-- São 166.432 faturas no Espaço Facial. Só esse UPDATE são 63 horas.\n--\n-- ── O conserto, em dois lugares ─────────────────────────────────────────────\n--\n-- 1. \\`_set_unit\\` só escreve quando o valor MUDA (\\`IS DISTINCT FROM\\`). Isso vale\n-- para todos os writers de uma vez, e faz da reoferta de uma linha já\n-- migrada o que ela deveria ser: leitura.\n--\n-- 2. \\`upsert_order\\` leva \\`unit_id\\` no próprio INSERT. Assim a linha nova nasce\n-- completa e o \\`_set_unit\\` logo abaixo vira o no-op do item 1. A chamada\n-- FICA, de propósito: ela continua sendo o caminho do UPDATE (quando a\n-- unidade muda no V1) e de qualquer writer que ainda não carregue a coluna.\n--\n-- Nada muda no resultado: a linha termina com a mesma unidade. O que muda é o\n-- número de versões dela.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. O UPDATE que só acontece quando há o que mudar\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration._set_unit(p_table text, p_id uuid, p_unit uuid)\n RETURNS void\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nBEGIN\n IF p_unit IS NULL OR NOT migration._has_column(p_table, 'unit_id') THEN RETURN; END IF;\n -- \\`IS DISTINCT FROM\\` e não \\`<>\\`: a coluna é anulável, e \\`NULL <> 'x'\\` é NULL,\n -- que não é verdadeiro — a linha sem unidade nunca receberia a dela.\n EXECUTE format('UPDATE %s SET unit_id = $1 WHERE id = $2 AND unit_id IS DISTINCT FROM $1', p_table)\n USING p_unit, p_id;\nEND $function$;\n\nCOMMENT ON FUNCTION migration._set_unit(text, uuid, uuid) IS\n 'Carimba a unidade no destino, e só quando ela muda. Sem o qual de guarda '\n 'este UPDATE rodava em toda linha de todo writer: 1,37 s medidos em '\n 'public.orders para não mudar valor nenhum, quase tudo em revalidação de '\n 'chave estrangeira e reescrita de índice.';\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. A Venda nasce com a unidade\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_order(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_map jsonb := coalesce(p_args -> 'status_map', '{}'::jsonb);\n v_kind_map jsonb := coalesce(p_args -> 'kind_map', '{}'::jsonb);\n v_src_status text := p_row ->> 'status';\n v_status text;\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_src_kind text;\n v_kind text;\n v_direction text;\n v_party uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_invoice jsonb;\n v_invoice_id uuid;\n v_extra jsonb := '[]'::jsonb;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an order needs an id');\n END IF;\n\n -- A espécie sai do \\`type\\` da origem quando o de-para declarou o mapa; sem\n -- mapa, nada muda em relação a antes.\n v_src_kind := v_meta #>> '{financial,v1_type}';\n v_kind := coalesce(v_kind_map ->> coalesce(v_src_kind, ''), p_args ->> 'kind', 'sale');\n IF NOT EXISTS (SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('order kind %L is not declared in public.order_kinds', v_kind));\n END IF;\n -- Compra é documento de despesa: o título dela é a pagar. É o que a própria\n -- descrição de \\`order_kinds.purchase\\` diz.\n v_direction := CASE WHEN v_kind = 'purchase' THEN 'payable' ELSE 'receivable' END;\n\n v_status := coalesce(v_map ->> coalesce(v_src_status, ''), v_map ->> '*', v_src_status, 'completed');\n v_meta := migration._deep_set(v_meta, ARRAY['migration', 'source_status'], to_jsonb(v_src_status));\n\n -- A contraparte. \\`party_id\\` já vem resolvido quando é cliente; quando é\n -- fornecedor ou profissional, quem resolve é o ledger — as três tabelas do\n -- V1 viram \\`public.people\\`, e o ledger sabe qual virou qual.\n BEGIN\n v_party := (p_row ->> 'party_id')::uuid;\n EXCEPTION WHEN OTHERS THEN v_party := NULL; END;\n IF v_party IS NULL AND (v_meta #>> '{financial,v1_ref_table}') IS NOT NULL THEN\n SELECT l.target_id INTO v_party\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = (v_meta #>> '{financial,v1_ref_table}')\n AND l.source_numeric_id = nullif(v_meta #>> '{financial,v1_ref_id}', '')::bigint\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.orders WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order %s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n -- \\`unit_id\\` entra JUNTO, e não num UPDATE logo depois. O UPDATE que vinha\n -- em seguida (via \\`_set_unit\\`) criava uma segunda versão da linha, reescrevia\n -- os onze índices de \\`orders\\` e refazia as checagens de chave estrangeira —\n -- medido em 19/09/2026: 1.371 ms para um UPDATE que não mudava valor nenhum,\n -- quase tudo em revalidação de FK (tenant 646 ms, party 398 ms, unit 275 ms).\n -- Por linha, e são 166.432 faturas.\n INSERT INTO public.orders (id, tenant_id, unit_id, kind, reference_number, status, party_id, subtotal, discount, tax, total, currency, completed_at, notes, metadata)\n VALUES (p_id, p_tenant, (p_row ->> 'unit_id')::uuid, v_kind, p_row ->> 'reference_number', v_status, v_party,\n (p_row ->> 'subtotal')::numeric, coalesce((p_row ->> 'discount')::numeric, 0), coalesce((p_row ->> 'tax')::numeric, 0),\n (p_row ->> 'total')::numeric, coalesce(p_row ->> 'currency', 'BRL'), (p_row ->> 'completed_at')::timestamptz,\n p_row ->> 'notes', v_meta);\n v_created := true;\n ELSE\n -- financial columns are guarded upstream (upsert_row) and not touched here\n UPDATE public.orders SET\n kind = v_kind,\n reference_number = CASE WHEN p_row ? 'reference_number' THEN p_row ->> 'reference_number' ELSE reference_number END,\n status = CASE WHEN p_row ? 'status' THEN v_status ELSE status END,\n party_id = coalesce(v_party, party_id),\n completed_at = CASE WHEN p_row ? 'completed_at' THEN (p_row ->> 'completed_at')::timestamptz ELSE completed_at END,\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n PERFORM migration._set_unit('public.orders', p_id, (p_row ->> 'unit_id')::uuid);\n\n IF coalesce(p_args ->> 'split_invoice', 'false') = 'true' THEN\n v_invoice_id := md5('migration:invoice:' || p_id::text)::uuid;\n v_invoice := migration.upsert_invoice(p_tenant, v_invoice_id,\n jsonb_build_object('order_id', p_id, 'party_id', to_jsonb(v_party), 'unit_id', p_row -> 'unit_id', 'total', p_row -> 'total',\n 'competence_date', p_row -> 'completed_at', 'status', 'open',\n -- a fatura leva o texto da origem: sem isto a lista do\n -- financeiro mostra a coluna Descrição vazia em tudo\n 'description', p_row -> 'notes',\n 'metadata', v_meta),\n jsonb_build_object('direction', v_direction, 'kind', v_direction));\n v_extra := v_extra || jsonb_build_object('target_table', 'public.plg_financial_invoices', 'target_id', v_invoice_id,\n 'status', CASE WHEN v_invoice ->> 'status' = 'migrated' THEN 'migrated' ELSE 'skipped' END,\n 'reason', v_invoice ->> 'reason',\n 'financial', CASE WHEN v_invoice ->> 'status' = 'migrated' THEN jsonb_build_object('total', p_row -> 'total') ELSE NULL END);\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.orders', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END, 'extra', v_extra);\nEND $function$\n\n;\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\n--\n-- Função nova nasce executável por PUBLIC, e \\`anon\\`/\\`authenticated\\` herdam\n-- dali. Estas são de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcançável pela chave anônima é a base inteira\n-- alcançável pela chave anônima. Só o service_role, que é quem roda a onda.\nREVOKE ALL ON FUNCTION migration._set_unit(text, uuid, uuid) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration._set_unit(text, uuid, uuid) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_order(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_order(uuid, uuid, jsonb, jsonb) TO service_role;\n`\n\nexport const MIGRATION_069_O_TITULO_QUE_JA_EXISTE_ESTA_MIGRADO = `-- O título que já existe ESTÁ migrado.\n--\n-- \\`upsert_invoice\\` recusa reescrever um documento financeiro que já está no\n-- destino. A recusa está certa e continua. O que estava errado era o nome que\n-- ela dava ao resultado:\n--\n-- RETURN jsonb_build_object('status', 'skipped', 'target_id', p_id,\n-- 'reason', 'invoice already exists: financial documents are never rewritten')\n--\n-- \\`skipped\\` quer dizer \"esta linha ainda não tem destino\". Mas ela tem: está\n-- lá, com aquele id, vinda daquela linha da origem. O estado é \\`migrated\\`.\n--\n-- ── Por que a palavra custava caro ──────────────────────────────────────────\n--\n-- \\`migration._upsert_row_impl\\` reexecuta o writer quando alguma IRMÃ da linha\n-- — o mesmo registro da origem gravado com OUTRO destino — está \\`pending\\` ou\n-- \\`skipped\\`. É o mecanismo que faz a fatura de uma Venda nascer numa segunda\n-- passada, quando o writer dela não existia na primeira. Bom mecanismo.\n--\n-- Só que uma Venda do V1 produz DUAS linhas de ledger: \\`public.orders\\` e\n-- \\`public.plg_financial_invoices\\`. Com a segunda marcada \\`skipped\\` para\n-- sempre, a condição nunca deixava de ser verdadeira. Toda rodada, para cada\n-- título já migrado:\n--\n-- sonda a irmã → encontra \\`skipped\\` → reexecuta upsert_order inteiro\n-- → upsert_invoice → \"já existe\" → \\`skipped\\` de novo\n--\n-- Medido em 20/09/2026 no tenant Espaço Facial: **7.893 títulos** nesse laço.\n-- A etapa \\`invoices\\` passava minutos sem produzir uma linha — parecia travada,\n-- e era só trabalho refeito. E como ela vem ANTES de \\`financial_movements\\` no\n-- plano, o que a tela de Contas a Receber precisa nunca chegava a ser lido.\n--\n-- ── O conserto, e o reparo do que ficou escrito errado ──────────────────────\n--\n-- 1. O writer devolve \\`migrated\\` quando a linha já está lá, com a razão\n-- registrando que ela não foi reescrita. Nada é atualizado — a recusa é a\n-- mesma; muda o relato.\n--\n-- 2. As linhas de ledger que já carregam o relato antigo são corrigidas, e só\n-- elas: o UPDATE exige que a linha de destino EXISTA de fato. Uma que não\n-- exista não é corrigida aqui — ela é uma linha que não migrou, e continuar\n-- chamando-a de \\`skipped\\` é o correto.\n--\n-- Idempotente.\n\nCREATE OR REPLACE FUNCTION migration.upsert_invoice(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_table text := 'public.plg_financial_invoices';\n v_dir text := coalesce(p_args ->> 'direction', p_row ->> 'direction', 'receivable');\n v_total numeric := round(coalesce((p_row ->> 'total')::numeric, -1), 2);\n v_party uuid;\n v_number text;\n v_insts jsonb;\n v_sum numeric := 0;\n v_i jsonb;\n v_n integer := 0;\n v_due date;\n v_exists boolean;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF to_regclass(v_table) IS NULL THEN\n RETURN jsonb_build_object('status', 'skipped', 'reason', 'destination table absent: ' || v_table);\n END IF;\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n EXECUTE format('SELECT EXISTS (SELECT 1 FROM %s WHERE id = $1)', v_table) INTO v_exists USING p_id;\n IF v_exists THEN\n -- \\`migrated\\`, e nao \\`skipped\\`. A regra de nao reescrever documento\n -- financeiro continua valendo - nada e atualizado aqui. O que muda e o\n -- NOME que se da ao resultado: a linha existe no destino, com este id,\n -- vinda desta linha da origem. Isso e o estado migrado.\n --\n -- Chama-lo de \\`skipped\\` tinha uma consequencia que nao era de vocabulario.\n -- \\`_upsert_row_impl\\` reexecuta o writer quando alguma IRMA da linha (o\n -- mesmo registro da origem com outro destino) esta \\`pending\\` ou \\`skipped\\` -\n -- e assim que a fatura de uma Venda nasce numa segunda passada, quando o\n -- writer dela nao existia na primeira. Com o titulo marcado \\`skipped\\` para\n -- sempre, essa condicao nunca deixava de ser verdadeira: toda rodada\n -- reexecutava o writer inteiro para chegar de novo aqui.\n --\n -- Medido em 20/09/2026 no tenant Espaco Facial: 7.893 titulos nesse estado,\n -- refeitos a cada rodada antes de o motor sequer chegar nas linhas novas. A\n -- etapa \\`invoices\\` passava minutos sem produzir uma linha, e parecia travada.\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', 'already present; financial documents are never rewritten by the migration');\n END IF;\n\n IF v_dir NOT IN ('receivable', 'payable') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown invoice direction %L', v_dir));\n END IF;\n IF v_total < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'an invoice needs a total >= 0');\n END IF;\n\n -- the counterparty is a Person. A reference that does not resolve is a broken\n -- link, not a column to drop — which is what the old candidate-name loop did.\n BEGIN\n v_party := coalesce((p_row ->> 'counterparty_person_id')::uuid, (p_row ->> 'party_id')::uuid);\n EXCEPTION WHEN OTHERS THEN v_party := NULL; END;\n IF v_party IS NOT NULL AND NOT EXISTS (SELECT 1 FROM public.people p WHERE p.id = v_party AND p.tenant_id = p_tenant) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('counterparty %s is not a Person of this tenant', v_party));\n END IF;\n\n v_due := coalesce((p_row ->> 'due_date')::date, (p_row ->> 'competence_date')::date, current_date);\n\n -- installments: as offered, or one for the whole total. An invoice with no\n -- installment is a document nobody can pay, and the balance views derive\n -- everything from them.\n v_insts := CASE WHEN jsonb_typeof(p_row -> 'installments') = 'array' AND jsonb_array_length(p_row -> 'installments') > 0\n THEN p_row -> 'installments'\n ELSE jsonb_build_array(jsonb_build_object('number', 1, 'due_date', v_due, 'amount', v_total)) END;\n FOR v_i IN SELECT * FROM jsonb_array_elements(v_insts) LOOP\n v_sum := v_sum + round(coalesce((v_i ->> 'amount')::numeric, 0), 2);\n END LOOP;\n IF v_sum <> v_total THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('installments sum to %s but the invoice total is %s — which one is wrong is not a migration''s call', v_sum, v_total));\n END IF;\n\n -- same series as an invoice created in V2: two numbering schemes in one tenant\n -- is the kind of thing nobody notices until an auditor does\n BEGIN\n v_number := public.plg_financial_next_invoice_number(p_tenant, v_dir);\n EXCEPTION WHEN OTHERS THEN v_number := p_row ->> 'number'; END;\n\n INSERT INTO public.plg_financial_invoices\n (id, tenant_id, unit_id, direction, counterparty_person_id, order_id, number,\n competence_date, issue_date, total, description, notes, metadata)\n VALUES (p_id, p_tenant, (p_row ->> 'unit_id')::uuid, v_dir, v_party, (p_row ->> 'order_id')::uuid, v_number,\n coalesce((p_row ->> 'competence_date')::date, v_due), coalesce((p_row ->> 'issue_date')::date, v_due),\n v_total, p_row ->> 'description', p_row ->> 'notes',\n coalesce(p_row -> 'metadata', '{}'::jsonb));\n\n FOR v_i IN SELECT * FROM jsonb_array_elements(v_insts) LOOP\n v_n := v_n + 1;\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount, metadata)\n VALUES (p_tenant, (p_row ->> 'unit_id')::uuid, p_id,\n coalesce((v_i ->> 'number')::int, v_n),\n coalesce((v_i ->> 'due_date')::date, v_due),\n round(coalesce((v_i ->> 'amount')::numeric, 0), 2),\n CASE WHEN jsonb_typeof(v_i -> 'metadata') = 'object' THEN v_i -> 'metadata' ELSE '{}'::jsonb END);\n END LOOP;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, v_table, p_id, jsonb_build_object('reason', 'created', 'installments', v_n)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created', 'installments', v_n, 'number', v_number);\nEND $function$\n\n;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- O reparo: o ledger passa a dizer o que o destino mostra\n-- ─────────────────────────────────────────────────────────────────────────────\n\nUPDATE migration.ledger l\n SET status = 'migrated',\n reason = 'already present; financial documents are never rewritten by the migration'\n WHERE l.target_table = 'public.plg_financial_invoices'\n AND l.status = 'skipped'\n AND l.reason = 'invoice already exists: financial documents are never rewritten by the migration'\n -- A trava: só corrige o relato de uma linha que REALMENTE está no destino.\n AND EXISTS (SELECT 1 FROM public.plg_financial_invoices t\n WHERE t.id = l.target_id AND t.tenant_id = l.tenant_id);\n`\n\nexport const MIGRATION_070_A_PARCELA_NASCE_ANTES_DO_MOVIMENTO = `-- A parcela nasce antes do movimento que aponta para ela.\n--\n-- No V1 a conta É a parcela: uma linha de \\`financial_movements\\` com\n-- \\`movement_type = 'bill'\\` é, ao mesmo tempo, o lançamento e o vencimento. O\n-- porte reproduz isso gravando as duas coisas com o MESMO id — o movimento\n-- aponta para a parcela por \\`financial_invoice_installment_id = p_id\\`.\n--\n-- E \\`plg_financial_movements_installment_fk\\` não é adiável:\n--\n-- FOREIGN KEY (financial_invoice_installment_id)\n-- REFERENCES plg_financial_invoice_installments(id) -- NOT DEFERRABLE\n--\n-- \\`upsert_fin_movement\\` inseria o movimento primeiro e criava a parcela\n-- depois. Ou seja: apontava para uma linha que ainda não existia.\n--\n-- 23503: insert or update on table \"plg_financial_movements\" violates\n-- foreign key constraint \"plg_financial_movements_installment_fk\"\n--\n-- ── O que isso custou ───────────────────────────────────────────────────────\n--\n-- Medido em 20/09/2026, tenant Espaço Facial, numa única rodada:\n-- **14.928 contas recusadas**. E o estrago não para nelas: a conta que não\n-- entra não vira parcela, a baixa que quitaria essa parcela também é recusada,\n-- e \\`v_financial_invoice_lines_due\\` — que é o que a tela de Contas a Receber\n-- lê — não tem o que mostrar. A tela aparecia vazia e o motivo estava três\n-- camadas abaixo.\n--\n-- Não apareceu antes porque no Pertinho do Céu os movimentos já existiam\n-- quando a 042 entrou: eles pegavam o caminho do UPDATE, e o UPDATE tolera o\n-- ponteiro porque a parcela daquele porte já tinha sido criada. Um tenant novo,\n-- carregado do zero, encontra a ordem errada na primeira linha.\n--\n-- ── O conserto: duas chamadas, e cada uma no seu lugar ──────────────────────\n--\n-- A ordem tem duas exigências que se contradizem se houver uma chamada só:\n--\n-- · a PARCELA precisa existir antes do movimento — a FK exige.\n-- · o SINCRONIZADOR (\\`plg_financial_sync_mirror\\`) procura o movimento pela\n-- parcela — logo, precisa do movimento.\n--\n-- Então a parcela é criada antes, na forma de sete argumentos, que não\n-- sincroniza. Depois de o movimento existir, a chamada de oito argumentos\n-- completa o cancelamento e sincroniza o espelho. A segunda é um\n-- \\`ON CONFLICT DO UPDATE\\` sobre a linha que a primeira criou — barata, e é ela\n-- que fecha o ciclo.\n--\n-- Idempotente.\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_movement(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'movement_kind', ''))), '');\n v_kind text := CASE v_kind_raw\n WHEN 'bill' THEN 'bill' WHEN 'payment' THEN 'payment'\n WHEN 'transfer' THEN 'transfer' WHEN 'commission' THEN 'commission'\n WHEN 'conta' THEN 'bill' WHEN 'titulo' THEN 'bill' WHEN 'título' THEN 'bill'\n WHEN 'pagamento' THEN 'payment' WHEN 'recebimento' THEN 'payment'\n WHEN 'transferencia' THEN 'transfer' WHEN 'transferência' THEN 'transfer'\n WHEN 'comissao' THEN 'commission' WHEN 'comissão' THEN 'commission'\n END;\n v_status_raw text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text := CASE v_status_raw\n WHEN 'canceled' THEN 'cancelled'\n WHEN 'cancelado' THEN 'cancelled'\n WHEN 'cancelada' THEN 'cancelled'\n WHEN 'pago' THEN 'paid'\n WHEN 'paga' THEN 'paid'\n WHEN 'pendente' THEN 'pending'\n WHEN 'vencido' THEN 'overdue'\n WHEN 'vencida' THEN 'overdue'\n WHEN 'atrasado' THEN 'overdue'\n WHEN 'parcial' THEN 'partial'\n ELSE coalesce(v_status_raw, 'pending')\n END;\n v_dir text := nullif(btrim(lower(coalesce(p_row ->> 'direction', ''))), '');\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_invoice uuid := (p_row ->> 'financial_invoice_id')::uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_due date;\n v_legs record;\n v_debit uuid := (p_row ->> 'debit_account_id')::uuid;\n v_credit uuid := (p_row ->> 'credit_account_id')::uuid;\n v_exists boolean;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a movement needs an id');\n END IF;\n IF v_kind IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown movement kind %L (expected bill|payment|transfer|commission, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));\n END IF;\n IF v_dir IS NOT NULL AND v_dir NOT IN ('debit', 'credit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown direction %L (expected debit|credit)', v_dir));\n END IF;\n IF v_dir IS NULL AND v_invoice IS NOT NULL THEN\n SELECT CASE t.direction WHEN 'receivable' THEN 'credit' WHEN 'payable' THEN 'debit' END INTO v_dir\n FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n IF v_dir IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'the movement carries no direction and settles no invoice — which way the money went cannot be derived, and must not be defaulted');\n END IF;\n IF v_amount < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a money event cannot carry a negative amount — direction is the direction, not the sign');\n END IF;\n\n IF v_unit IS NULL AND v_invoice IS NOT NULL THEN\n SELECT t.unit_id INTO v_unit FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n\n v_due := coalesce((p_row ->> 'due_date')::date, (p_row ->> 'paid_at')::date, current_date);\n\n IF v_debit IS NULL OR v_credit IS NULL THEN\n SELECT * INTO v_legs FROM public.plg_financial_legacy_legs(\n p_tenant, v_kind, v_dir, (p_row ->> 'bank_account_id')::uuid, (p_row ->> 'cash_session_id')::uuid,\n v_invoice, v_meta);\n IF v_legs.o_mirror THEN\n v_meta := v_meta || jsonb_build_object('legacy_mirror', true);\n ELSE\n v_debit := coalesce(v_debit, v_legs.o_debit);\n v_credit := coalesce(v_credit, v_legs.o_credit);\n END IF;\n END IF;\n\n -- ── A parcela nasce ANTES do movimento ────────────────────────────────────\n --\n -- O movimento de conta aponta para a parcela dele\n -- (\\`financial_invoice_installment_id = p_id\\`, porque no V1 a conta É a\n -- parcela), e \\`plg_financial_movements_installment_fk\\` NÃO é adiável. Criar a\n -- parcela depois do INSERT é apontar para uma linha que ainda não existe:\n --\n -- 23503: insert or update on table \"plg_financial_movements\" violates\n -- foreign key constraint \"plg_financial_movements_installment_fk\"\n --\n -- Medido em 20/09/2026 no tenant Espaço Facial: 14.928 contas recusadas por\n -- isso numa só rodada — e como a conta não entrava, a baixa dela também não,\n -- e Contas a Receber ficava vazia.\n --\n -- Aqui a parcela é criada SEM sincronizar o espelho: o sincronizador procura\n -- o movimento pela parcela, e o movimento ainda não existe. A sincronização\n -- acontece na segunda chamada, no fim desta função, quando as duas linhas já\n -- estão no lugar. É por isso que são duas chamadas e não uma.\n IF v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN\n PERFORM migration._fin_installment_from_bill(\n p_tenant, p_id, v_invoice, v_unit,\n (p_row ->> 'installment_number')::integer, v_due, v_amount);\n END IF;\n\n SELECT EXISTS (SELECT 1 FROM public.plg_financial_movements WHERE id = p_id) INTO v_exists;\n IF NOT v_exists THEN\n INSERT INTO public.plg_financial_movements\n (id, tenant_id, unit_id, financial_invoice_id, financial_invoice_installment_id,\n direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, notes, metadata, debit_account_id, credit_account_id)\n VALUES (p_id, p_tenant, v_unit, v_invoice,\n CASE WHEN v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN p_id END,\n v_dir, v_kind, v_amount,\n round(coalesce((p_row ->> 'paid_amount')::numeric, 0), 2),\n v_status, v_due, (p_row ->> 'paid_at')::date,\n (p_row ->> 'installment_number')::integer, p_row ->> 'notes', v_meta, v_debit, v_credit);\n ELSE\n UPDATE public.plg_financial_movements SET\n unit_id = coalesce(unit_id, v_unit),\n debit_account_id = coalesce(debit_account_id, v_debit),\n credit_account_id = coalesce(credit_account_id, v_credit),\n financial_invoice_installment_id = coalesce(\n financial_invoice_installment_id,\n CASE WHEN v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN p_id END)\n WHERE id = p_id;\n END IF;\n\n -- DEPOIS do movimento existir: agora sim o cancelamento e a sincronia do\n -- espelho, que procura o movimento por \\`financial_invoice_installment_id\\`.\n -- A parcela em si já foi criada lá em cima — esta chamada completa o que\n -- depende do movimento.\n IF v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN\n PERFORM migration._fin_installment_from_bill(\n p_tenant, p_id, v_invoice, v_unit,\n (p_row ->> 'installment_number')::integer, v_due, v_amount,\n v_status = 'cancelled');\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_movements', p_id, jsonb_build_object('reason', CASE WHEN v_exists THEN 'updated' ELSE 'created' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_exists THEN 'already in the ledger (append-only; legs, unit, installment and cancellation completed)' ELSE 'created' END);\nEND $function$\n\n;\n`\n\nexport const MIGRATION_071_A_PARCELA_TEM_UMA_ASSINATURA_SO = `-- A parcela tem uma assinatura só.\n--\n-- A 055 passou a chamar \\`migration._fin_installment_from_bill\\` com SETE\n-- argumentos antes de o movimento existir (para a FK ter para onde apontar) e\n-- com OITO depois (para o cancelamento e a sincronia do espelho). A intenção\n-- estava certa; a resolução de sobrecarga do Postgres, não:\n--\n-- 42725: function migration._fin_installment_from_bill(uuid, uuid, uuid,\n-- uuid, integer, date, numeric) is not unique\n--\n-- Existiam duas versões da função — uma de sete argumentos e uma de oito com\n-- \\`p_cancelled boolean DEFAULT false\\`. Com o DEFAULT, uma chamada de sete\n-- argumentos casa com as DUAS, e o Postgres recusa em vez de escolher. Está\n-- certo em recusar: adivinhar qual delas o autor queria seria pior.\n--\n-- Medido em 20/09/2026, tenant Espaço Facial: **57.220 contas** recusadas por\n-- esta ambiguidade, numa única passada.\n--\n-- ── O conserto ──────────────────────────────────────────────────────────────\n--\n-- Sai a versão de sete argumentos. Fica UMA assinatura, com \\`p_sync\\` explícito:\n--\n-- p_sync = false cria a parcela e NÃO sincroniza — é a chamada de antes\n-- do movimento, quando o espelho ainda não teria o que\n-- procurar.\n-- p_sync = true completa o cancelamento e sincroniza — é a de depois.\n--\n-- Sem DEFAULT no parâmetro novo, de propósito: foi um DEFAULT que criou a\n-- ambiguidade, e uma função que decide coisas diferentes conforme o número de\n-- argumentos merece que cada chamada diga qual delas quer.\n--\n-- Idempotente.\n\nDROP FUNCTION IF EXISTS migration._fin_installment_from_bill(uuid, uuid, uuid, uuid, integer, date, numeric);\nDROP FUNCTION IF EXISTS migration._fin_installment_from_bill(uuid, uuid, uuid, uuid, integer, date, numeric, boolean);\n\n-- \\`OR REPLACE\\`, e não só \\`CREATE\\`: as duas assinaturas velhas caem acima, mas\n-- a nova sobrevive à própria migração — e o encadeamento replica tudo uma\n-- segunda vez para provar que replicar é seguro. Sem isso a segunda passada\n-- morre com \"function already exists with same argument types\", que é o mesmo\n-- que dizer \"esta migração não pode ser reaplicada\".\nCREATE OR REPLACE FUNCTION migration._fin_installment_from_bill(\n p_tenant uuid, p_id uuid, p_invoice uuid, p_unit uuid,\n p_number integer, p_due date, p_amount numeric,\n p_cancelled boolean, p_sync boolean\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_number integer := greatest(coalesce(p_number, 1), 1);\n -- \\`now()\\` seria a hora desta migration, não a do cancelamento. O V1 não\n -- guarda quando a conta foi cancelada — guarda só que foi —, então o carimbo\n -- fica na data de VENCIMENTO: é a data que a linha já tem e a única que não\n -- inventa um fato novo. Quem precisar do instante exato vai ao V1.\n v_cancel timestamptz := CASE WHEN coalesce(p_cancelled, false)\n THEN coalesce(p_due, current_date)::timestamptz END;\nBEGIN\n IF p_invoice IS NULL THEN RETURN; END IF;\n\n -- A parcela genérica do título sai para dar lugar à do V1 — mas só a que não\n -- veio da migração E que ninguém referencia. Uma parcela com baixa fica onde\n -- está: apagá-la levaria a baixa junto (ON DELETE CASCADE) e o dinheiro\n -- sumiria sem ninguém pedir.\n DELETE FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice\n AND i.id <> p_id\n AND NOT (i.metadata ? 'migration')\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations a WHERE a.installment_id = i.id)\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_movements m\n WHERE m.financial_invoice_installment_id = i.id);\n\n BEGIN\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)\n VALUES (p_id, p_tenant, p_unit, p_invoice, v_number, p_due,\n greatest(coalesce(p_amount, 0), 0), v_cancel,\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements')))\n ON CONFLICT (id) DO UPDATE SET\n number = EXCLUDED.number,\n due_date = EXCLUDED.due_date,\n amount = EXCLUDED.amount,\n -- O cancelamento é da ORIGEM e ela manda nos dois sentidos: uma conta que\n -- voltou a valer no V1 volta a valer aqui.\n cancelled_at = EXCLUDED.cancelled_at,\n unit_id = coalesce(public.plg_financial_invoice_installments.unit_id, EXCLUDED.unit_id),\n metadata = public.plg_financial_invoice_installments.metadata || EXCLUDED.metadata;\n EXCEPTION\n -- \\`UNIQUE (invoice_id, number)\\`: duas contas do V1 com o mesmo número de\n -- parcela no mesmo título existem — a fatura #1869 tem duas com vencimento\n -- 19/08. Recusar a segunda perderia dinheiro; ela entra no próximo número\n -- livre, que é o que a tela precisa para somar certo.\n WHEN unique_violation THEN\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)\n SELECT p_id, p_tenant, p_unit, p_invoice, coalesce(max(i.number), 0) + 1, p_due,\n greatest(coalesce(p_amount, 0), 0), v_cancel,\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements', 'renumbered', true))\n FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice\n ON CONFLICT (id) DO NOTHING;\n END;\n\n -- O espelho acompanha agora, e não na próxima baixa. Uma conta cancelada sem\n -- baixa nenhuma nunca receberia a visita do gatilho — e é justamente ela que\n -- não pode ficar contando como viva.\n --\n -- Mas ele só pode ser chamado quando o MOVIMENTO já existe: é por\n -- \\`financial_invoice_installment_id\\` que ele o encontra. Daí o parâmetro.\n IF p_sync THEN\n PERFORM public.plg_financial_sync_mirror(p_id);\n END IF;\nEND $function$;\n\nCOMMENT ON FUNCTION migration._fin_installment_from_bill(uuid, uuid, uuid, uuid, integer, date, numeric, boolean, boolean) IS\n 'A conta do V1 virando parcela do V2, com o mesmo id do movimento. \\`p_sync\\` '\n 'separa as duas metades da operação: a parcela precisa existir ANTES do '\n 'movimento (a FK não é adiável) e o espelho precisa do movimento para '\n 'sincronizar. Uma assinatura só, sem DEFAULT — foi um DEFAULT que tornou a '\n 'chamada de sete argumentos ambígua e recusou 57.220 contas.';\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- E o writer chama a assinatura nova nas duas pontas\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_movement(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'movement_kind', ''))), '');\n v_kind text := CASE v_kind_raw\n WHEN 'bill' THEN 'bill' WHEN 'payment' THEN 'payment'\n WHEN 'transfer' THEN 'transfer' WHEN 'commission' THEN 'commission'\n WHEN 'conta' THEN 'bill' WHEN 'titulo' THEN 'bill' WHEN 'título' THEN 'bill'\n WHEN 'pagamento' THEN 'payment' WHEN 'recebimento' THEN 'payment'\n WHEN 'transferencia' THEN 'transfer' WHEN 'transferência' THEN 'transfer'\n WHEN 'comissao' THEN 'commission' WHEN 'comissão' THEN 'commission'\n END;\n v_status_raw text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text := CASE v_status_raw\n WHEN 'canceled' THEN 'cancelled'\n WHEN 'cancelado' THEN 'cancelled'\n WHEN 'cancelada' THEN 'cancelled'\n WHEN 'pago' THEN 'paid'\n WHEN 'paga' THEN 'paid'\n WHEN 'pendente' THEN 'pending'\n WHEN 'vencido' THEN 'overdue'\n WHEN 'vencida' THEN 'overdue'\n WHEN 'atrasado' THEN 'overdue'\n WHEN 'parcial' THEN 'partial'\n ELSE coalesce(v_status_raw, 'pending')\n END;\n v_dir text := nullif(btrim(lower(coalesce(p_row ->> 'direction', ''))), '');\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_invoice uuid := (p_row ->> 'financial_invoice_id')::uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_due date;\n v_legs record;\n v_debit uuid := (p_row ->> 'debit_account_id')::uuid;\n v_credit uuid := (p_row ->> 'credit_account_id')::uuid;\n v_exists boolean;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a movement needs an id');\n END IF;\n IF v_kind IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown movement kind %L (expected bill|payment|transfer|commission, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));\n END IF;\n IF v_dir IS NOT NULL AND v_dir NOT IN ('debit', 'credit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown direction %L (expected debit|credit)', v_dir));\n END IF;\n IF v_dir IS NULL AND v_invoice IS NOT NULL THEN\n SELECT CASE t.direction WHEN 'receivable' THEN 'credit' WHEN 'payable' THEN 'debit' END INTO v_dir\n FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n IF v_dir IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'the movement carries no direction and settles no invoice — which way the money went cannot be derived, and must not be defaulted');\n END IF;\n IF v_amount < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a money event cannot carry a negative amount — direction is the direction, not the sign');\n END IF;\n\n IF v_unit IS NULL AND v_invoice IS NOT NULL THEN\n SELECT t.unit_id INTO v_unit FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n\n v_due := coalesce((p_row ->> 'due_date')::date, (p_row ->> 'paid_at')::date, current_date);\n\n IF v_debit IS NULL OR v_credit IS NULL THEN\n SELECT * INTO v_legs FROM public.plg_financial_legacy_legs(\n p_tenant, v_kind, v_dir, (p_row ->> 'bank_account_id')::uuid, (p_row ->> 'cash_session_id')::uuid,\n v_invoice, v_meta);\n IF v_legs.o_mirror THEN\n v_meta := v_meta || jsonb_build_object('legacy_mirror', true);\n ELSE\n v_debit := coalesce(v_debit, v_legs.o_debit);\n v_credit := coalesce(v_credit, v_legs.o_credit);\n END IF;\n END IF;\n\n -- ── A parcela nasce ANTES do movimento ────────────────────────────────────\n --\n -- O movimento de conta aponta para a parcela dele\n -- (\\`financial_invoice_installment_id = p_id\\`, porque no V1 a conta É a\n -- parcela), e \\`plg_financial_movements_installment_fk\\` NÃO é adiável. Criar a\n -- parcela depois do INSERT é apontar para uma linha que ainda não existe:\n --\n -- 23503: insert or update on table \"plg_financial_movements\" violates\n -- foreign key constraint \"plg_financial_movements_installment_fk\"\n --\n -- Medido em 20/09/2026 no tenant Espaço Facial: 14.928 contas recusadas por\n -- isso numa só rodada — e como a conta não entrava, a baixa dela também não,\n -- e Contas a Receber ficava vazia.\n --\n -- Aqui a parcela é criada SEM sincronizar o espelho: o sincronizador procura\n -- o movimento pela parcela, e o movimento ainda não existe. A sincronização\n -- acontece na segunda chamada, no fim desta função, quando as duas linhas já\n -- estão no lugar. É por isso que são duas chamadas e não uma.\n IF v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN\n PERFORM migration._fin_installment_from_bill(\n p_tenant, p_id, v_invoice, v_unit,\n (p_row ->> 'installment_number')::integer, v_due, v_amount,\n v_status = 'cancelled', false);\n END IF;\n\n SELECT EXISTS (SELECT 1 FROM public.plg_financial_movements WHERE id = p_id) INTO v_exists;\n IF NOT v_exists THEN\n INSERT INTO public.plg_financial_movements\n (id, tenant_id, unit_id, financial_invoice_id, financial_invoice_installment_id,\n direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, notes, metadata, debit_account_id, credit_account_id)\n VALUES (p_id, p_tenant, v_unit, v_invoice,\n CASE WHEN v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN p_id END,\n v_dir, v_kind, v_amount,\n round(coalesce((p_row ->> 'paid_amount')::numeric, 0), 2),\n v_status, v_due, (p_row ->> 'paid_at')::date,\n (p_row ->> 'installment_number')::integer, p_row ->> 'notes', v_meta, v_debit, v_credit);\n ELSE\n UPDATE public.plg_financial_movements SET\n unit_id = coalesce(unit_id, v_unit),\n debit_account_id = coalesce(debit_account_id, v_debit),\n credit_account_id = coalesce(credit_account_id, v_credit),\n financial_invoice_installment_id = coalesce(\n financial_invoice_installment_id,\n CASE WHEN v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN p_id END)\n WHERE id = p_id;\n END IF;\n\n -- DEPOIS do movimento existir: agora sim o cancelamento e a sincronia do\n -- espelho, que procura o movimento por \\`financial_invoice_installment_id\\`.\n -- A parcela em si já foi criada lá em cima — esta chamada completa o que\n -- depende do movimento.\n IF v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN\n PERFORM migration._fin_installment_from_bill(\n p_tenant, p_id, v_invoice, v_unit,\n (p_row ->> 'installment_number')::integer, v_due, v_amount,\n v_status = 'cancelled', true);\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_movements', p_id, jsonb_build_object('reason', CASE WHEN v_exists THEN 'updated' ELSE 'created' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_exists THEN 'already in the ledger (append-only; legs, unit, installment and cancellation completed)' ELSE 'created' END);\nEND $function$\n\n;\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\n--\n-- Auxiliar de PORTE: SECURITY DEFINER, escreve parcela de qualquer inquilino.\n-- Só o service_role, que é quem roda a onda. (A assinatura antiga foi\n-- derrubada acima; esta concessão é da nova, e é por isso que ela vem aqui e\n-- não junto da criação.)\nREVOKE ALL ON FUNCTION migration._fin_installment_from_bill(uuid, uuid, uuid, uuid, integer, date, numeric, boolean, boolean) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration._fin_installment_from_bill(uuid, uuid, uuid, uuid, integer, date, numeric, boolean, boolean) TO service_role;\n`\n\nexport const MIGRATION_072_O_CAIXA_DO_V1_GUARDA_QUEM_O_ABRIU = `-- O caixa do V1 guarda quem o abriu.\n--\n-- \\`cash_register_sessions\\` estava em \\`migration.excluded_tables\\`, e o motivo\n-- registrado era bom:\n--\n-- a V2 cash session is ATTRIBUTED — user_id is NOT NULL and only one may\n-- be open per user and unit, because the session is the accountability\n-- record for a drawer. […] every migrated session would have to be\n-- attributed to somebody who did not open it.\n--\n-- A objeção está certa e continua valendo. O que mudou é que ela partia de uma\n-- premissa falsa: a de que \\`user_id\\` teria que apontar para um login do V2.\n--\n-- ── A medição que desfez a premissa ────────────────────────────────────────\n--\n-- \\`public.plg_financial_cash_register_sessions.user_id\\` é \\`NOT NULL\\` e **não\n-- tem chave estrangeira**. Conferido em 20/09/2026: zero constraints de FK\n-- sobre essa coluna. Ela exige uma identidade, não exige que seja do V2.\n--\n-- Então a sessão migrada carrega o uuid do PRÓPRIO abridor no V1\n-- (\\`opened_by_user_id\\`). Ninguém do V2 recebe a gaveta de ninguém — que era\n-- exatamente o que a decisão anterior queria evitar. E a restrição de \"um\n-- caixa aberto por pessoa e unidade\" passa a valer sobre a pessoa certa.\n--\n-- Isso também é o que a tela precisa: ela mostra \\`opened_by_name\\`, e o V1\n-- guarda ali o nome ou o e-mail de quem abriu — 12 pessoas distintas em 4\n-- unidades, nenhuma com login no V2 (medido: 0 de 12 casam por e-mail). Sem\n-- este caminho, o histórico de caixa do Espaço Facial não teria como existir.\n--\n-- ── O recorte ───────────────────────────────────────────────────────────────\n--\n-- 65 sessões, 64 fechadas e 1 aberta, de 10/07 a 19/09 de 2026. Todas com\n-- unidade e com conta. O vocabulário de status do V1 é \\`open\\`/\\`closed\\` — o\n-- mesmo do CHECK do V2, sem tradução.\n--\n-- A conferência (\\`review_status\\`, \\`reviewed_by_*\\`, \\`review_notes\\`) vai para\n-- \\`metadata\\`, e não para \\`plg_financial_cash_session_reviewers\\`: a tabela de\n-- mesmo nome no V1 é outra coisa — é a lista de QUEM PODE conferir, por\n-- unidade, sem vínculo com sessão nenhuma. Chamar as duas de \"reviewers\" foi o\n-- que fez a exclusão anterior tratá-las como par.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. O vocabulário fechado aprende a palavra\n-- ─────────────────────────────────────────────────────────────────────────────\n\n-- Acréscimo, e não lista literal. A restrição é um vocabulário FECHADO: quem a\n-- reescreve por extenso APAGA toda palavra acrescentada por um passo posterior\n-- — e o encadeamento replica a corrente inteira uma segunda vez, quando \\`crm\\`\n-- já registrou \\`crm_deal\\`, \\`quote\\` e \\`quote_item\\`. Na replicagem a lista\n-- literal daqui os apagava e as linhas que já existiam com eles passavam a\n-- violar a restrição. É a mesma correção que \\`agenda/008\\` recebeu, pelo mesmo\n-- motivo, e o padrão vem de \\`inventory/004\\`.\nDO $$\nDECLARE v_def text;\nBEGIN\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n IF v_def IS NULL OR strpos(v_def, 'fin_cash_session') > 0 THEN RETURN; END IF;\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''person''::text',\n '''person''::text, ''fin_cash_session''::text');\nEND $$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O writer\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_cash_session(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_status text := coalesce(p_row ->> 'status', 'closed');\n v_opened timestamptz;\n v_user uuid;\n v_unit uuid;\n v_conta uuid;\n v_existing uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a cash session needs an id');\n END IF;\n\n v_opened := (p_row ->> 'opened_at')::timestamptz;\n IF v_opened IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a cash session needs opened_at');\n END IF;\n\n IF v_status NOT IN ('open', 'closed') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('cash session status %L is not open|closed', v_status));\n END IF;\n\n -- Quem abriu, do V1. \\`user_id\\` é NOT NULL e não tem FK: ele exige uma\n -- identidade, não um login do V2. Sem o abridor não há registro de\n -- responsabilidade, e uma gaveta sem dono não é um caixa.\n BEGIN v_user := (p_row ->> 'opened_by_user_id')::uuid; EXCEPTION WHEN OTHERS THEN v_user := NULL; END;\n IF v_user IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', 'a cash session needs the person who opened it; the V2 record is an accountability record');\n END IF;\n\n BEGIN v_unit := (p_row ->> 'unit_id')::uuid; EXCEPTION WHEN OTHERS THEN v_unit := NULL; END;\n BEGIN v_conta := (p_row ->> 'account_id')::uuid; EXCEPTION WHEN OTHERS THEN v_conta := NULL; END;\n\n SELECT tenant_id INTO v_existing FROM public.plg_financial_cash_register_sessions WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n format('cash session %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.plg_financial_cash_register_sessions\n (id, tenant_id, unit_id, account_id, user_id, status,\n opened_at, opened_by_user_id, opened_by_name, opening_balance,\n closed_at, closed_by_user_id, closed_by_name, closing_balance,\n expected_balance, difference, expected_cash, counted_cash, difference_cash,\n expected_card, counted_card, difference_card, notes, metadata, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_unit, v_conta, v_user, v_status,\n v_opened, v_user, p_row ->> 'opened_by_name',\n coalesce(nullif(p_row ->> 'opening_balance','')::numeric, 0),\n nullif(p_row ->> 'closed_at','')::timestamptz,\n nullif(p_row ->> 'closed_by_user_id','')::uuid, p_row ->> 'closed_by_name',\n nullif(p_row ->> 'closing_balance','')::numeric,\n nullif(p_row ->> 'expected_balance','')::numeric,\n nullif(p_row ->> 'difference','')::numeric,\n nullif(p_row ->> 'expected_cash','')::numeric,\n nullif(p_row ->> 'counted_cash','')::numeric,\n nullif(p_row ->> 'difference_cash','')::numeric,\n nullif(p_row ->> 'expected_card','')::numeric,\n nullif(p_row ->> 'counted_card','')::numeric,\n nullif(p_row ->> 'difference_card','')::numeric,\n p_row ->> 'notes', v_meta,\n coalesce(nullif(p_row ->> 'created_at','')::timestamptz, v_opened),\n coalesce(nullif(p_row ->> 'updated_at','')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.plg_financial_cash_register_sessions SET\n unit_id = coalesce(v_unit, unit_id),\n account_id = coalesce(v_conta, account_id),\n status = v_status,\n closed_at = nullif(p_row ->> 'closed_at','')::timestamptz,\n closed_by_name = p_row ->> 'closed_by_name',\n closing_balance = nullif(p_row ->> 'closing_balance','')::numeric,\n expected_balance = nullif(p_row ->> 'expected_balance','')::numeric,\n difference = nullif(p_row ->> 'difference','')::numeric,\n counted_cash = nullif(p_row ->> 'counted_cash','')::numeric,\n counted_card = nullif(p_row ->> 'counted_card','')::numeric,\n difference_cash = nullif(p_row ->> 'difference_cash','')::numeric,\n difference_card = nullif(p_row ->> 'difference_card','')::numeric,\n notes = p_row ->> 'notes',\n metadata = migration._merge_meta(metadata, v_meta),\n updated_at = coalesce(nullif(p_row ->> 'updated_at','')::timestamptz, now())\n WHERE id = p_id;\n END IF;\n\n PERFORM migration._set_unit('public.plg_financial_cash_register_sessions', p_id, v_unit);\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.plg_financial_cash_register_sessions', p_id,\n jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 3. O de-para, e a readmissão por tenant\n-- ─────────────────────────────────────────────────────────────────────────────\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'cash_register_sessions', 'public.plg_financial_cash_register_sessions',\n 'financial', 'fin_cash_session', '{}'::jsonb, NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'status', 'status',\n 'opened_at', 'opened_at',\n 'opened_by_user_id', 'opened_by_user_id',\n 'opened_by_name', 'opened_by_name',\n 'opening_balance', 'opening_balance',\n 'closed_at', 'closed_at',\n 'closed_by_user_id', 'closed_by_user_id',\n 'closed_by_name', 'closed_by_name',\n 'closing_balance', 'closing_balance',\n 'expected_balance', 'expected_balance',\n 'difference', 'difference',\n 'expected_cash', 'expected_cash',\n 'counted_cash', 'counted_cash',\n 'difference_cash', 'difference_cash',\n 'expected_card', 'expected_card',\n 'counted_card', 'counted_card',\n 'difference_card', 'difference_card',\n 'notes', 'notes',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'unit_id', 'unit_id@companies',\n 'account_central_id', 'account_id@account_central',\n 'tenant_id', '-',\n 'review_status', 'jsonb:review.status',\n 'reviewed_by_user_id', 'jsonb:review.by_user',\n 'reviewed_by_name', 'jsonb:review.by_name',\n 'reviewed_at', 'jsonb:review.at',\n 'review_notes', 'jsonb:review.notes'),\n 'O caixa do V1. \\`user_id\\` recebe o uuid de quem ABRIU no V1 — a coluna é '\n 'NOT NULL e não tem FK, então ela exige identidade, não um login do V2. '\n 'Assim ninguém do V2 herda a gaveta de ninguém, que era a objeção que '\n 'mantinha esta tabela fora. A conferência vai para metadata.review.'\n);\n\nRESET migration.allowlist_unlock;\n\n-- A tabela de \"reviewers\" do V1 é a lista de QUEM PODE conferir, por unidade —\n-- não tem session_id e não é o par de \\`plg_financial_cash_session_reviewers\\`,\n-- que registra UMA conferência de UMA sessão. O motivo antigo tratava as duas\n-- como par; este corrige o registro.\nUPDATE migration.excluded_tables\n SET reason = 'Homônima, não equivalente: \\`cash_session_reviewers\\` do V1 é a '\n 'lista de quem PODE conferir caixa (unit_id + user_id + active), '\n 'sem vínculo com sessão. O destino de mesmo nome registra UMA '\n 'conferência de UMA sessão. A conferência que o V1 de fato tem '\n 'mora na própria sessão e atravessa em metadata.review (057).'\n WHERE source_table = 'cash_session_reviewers';\n\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- A decisão antiga sai quando a nova entra\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- A 041 registrou \\`cash_register_sessions\\` em \\`excluded_tables\\` com a razão da\n-- época: \"portar resolveria a quarentena e criaria dois problemas maiores\".\n-- Este arquivo é a decisão contrária — a sessão de caixa passa a atravessar —,\n-- e as duas não podem coexistir. A bancada diz isso em uma linha:\n--\n-- not ok 38 - no table is both allowlisted and excluded have: 1\n--\n-- E ela tem razão sobre o que é pior: uma tabela nos dois lugares não é uma\n-- contradição inofensiva, é o painel de cobertura mostrando \"decidido fora\"\n-- para algo que está atravessando. Some a linha antiga; a razão dela fica no\n-- cabeçalho da 041, que é onde história mora.\nDELETE FROM migration.excluded_tables\n WHERE source_table = 'cash_register_sessions'\n AND tenant_id IS NULL;\n\n-- ── quem pode executar ────────────────────────────────────────────────────\n--\n-- Função nova nasce executável por PUBLIC, e \\`anon\\`/\\`authenticated\\` herdam\n-- dali. Estas são de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcançável pela chave anônima é a base inteira\n-- alcançável pela chave anônima. Só o service_role, que é quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_cash_session(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_cash_session(uuid, uuid, jsonb, jsonb) TO service_role;\n`\n\nexport const MIGRATION_073_O_CAIXA_PERTENCE_A_CONTA_QUE_O_V1_LHE_DEU = `-- O caixa pertence à conta que o V1 lhe deu.\n--\n-- A 057 mapeou \\`account_central_id\\` para \\`account_id\\` e parou aí. O destino\n-- tem DUAS colunas de conta, e a segunda também é obrigatória:\n--\n-- bank_account_id uuid NOT NULL -- constraint \\`…_cofre_fk\\`\n-- account_id uuid -- anulável\n--\n-- 23502: null value in column \"bank_account_id\" … violates not-null\n--\n-- 65 de 65 sessões recusadas por isso.\n--\n-- ── Qual conta vai em qual coluna ───────────────────────────────────────────\n--\n-- No V2 as duas existem porque o caixa tem dois lados: a conta em que o\n-- dinheiro do dia é contado (\\`account_id\\`) e o COFRE para onde ele vai\n-- (\\`bank_account_id\\`). O V1 não faz essa distinção — ele tem uma referência\n-- só, \\`account_central_id\\`.\n--\n-- Então as duas recebem a mesma conta. Não é preguiça: é o que a origem tem.\n-- Inventar uma segunda conta faria o V2 afirmar um trajeto de dinheiro que\n-- ninguém registrou, e essa é uma mentira cara num sistema de caixa. Quando o\n-- Espaço Facial passar a usar cofre no V2, ele separa as duas lá — e aí a\n-- informação é dele, não uma invenção do porte.\n--\n-- A sessão sem conta nenhuma é recusada, e não completada por um palpite: um\n-- caixa que não sabe de que conta é não é um caixa.\n--\n-- Idempotente.\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_cash_session(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_status text := coalesce(p_row ->> 'status', 'closed');\n v_opened timestamptz;\n v_user uuid;\n v_unit uuid;\n v_conta uuid;\n v_existing uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a cash session needs an id');\n END IF;\n\n v_opened := (p_row ->> 'opened_at')::timestamptz;\n IF v_opened IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a cash session needs opened_at');\n END IF;\n\n IF v_status NOT IN ('open', 'closed') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('cash session status %L is not open|closed', v_status));\n END IF;\n\n -- Quem abriu, do V1. \\`user_id\\` é NOT NULL e não tem FK: ele exige uma\n -- identidade, não um login do V2. Sem o abridor não há registro de\n -- responsabilidade, e uma gaveta sem dono não é um caixa.\n BEGIN v_user := (p_row ->> 'opened_by_user_id')::uuid; EXCEPTION WHEN OTHERS THEN v_user := NULL; END;\n IF v_user IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', 'a cash session needs the person who opened it; the V2 record is an accountability record');\n END IF;\n\n BEGIN v_unit := (p_row ->> 'unit_id')::uuid; EXCEPTION WHEN OTHERS THEN v_unit := NULL; END;\n BEGIN v_conta := (p_row ->> 'account_id')::uuid; EXCEPTION WHEN OTHERS THEN v_conta := NULL; END;\n\n -- \\`bank_account_id\\` é NOT NULL. O V1 tem uma referência de conta só, então\n -- ela serve às duas — ver o cabeçalho. Sem conta nenhuma, recusa.\n IF v_conta IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'a cash session needs the account it belongs to; the destination requires it for the drawer and for the vault');\n END IF;\n\n SELECT tenant_id INTO v_existing FROM public.plg_financial_cash_register_sessions WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('cash session %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.plg_financial_cash_register_sessions\n (id, tenant_id, unit_id, account_id, bank_account_id, user_id, status,\n opened_at, opened_by_user_id, opened_by_name, opening_balance,\n closed_at, closed_by_user_id, closed_by_name, closing_balance,\n expected_balance, difference, expected_cash, counted_cash, difference_cash,\n expected_card, counted_card, difference_card, notes, metadata, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_unit, v_conta, v_conta, v_user, v_status,\n v_opened, v_user, p_row ->> 'opened_by_name',\n coalesce(nullif(p_row ->> 'opening_balance','')::numeric, 0),\n nullif(p_row ->> 'closed_at','')::timestamptz,\n nullif(p_row ->> 'closed_by_user_id','')::uuid, p_row ->> 'closed_by_name',\n nullif(p_row ->> 'closing_balance','')::numeric,\n nullif(p_row ->> 'expected_balance','')::numeric,\n nullif(p_row ->> 'difference','')::numeric,\n nullif(p_row ->> 'expected_cash','')::numeric,\n nullif(p_row ->> 'counted_cash','')::numeric,\n nullif(p_row ->> 'difference_cash','')::numeric,\n nullif(p_row ->> 'expected_card','')::numeric,\n nullif(p_row ->> 'counted_card','')::numeric,\n nullif(p_row ->> 'difference_card','')::numeric,\n p_row ->> 'notes', v_meta,\n coalesce(nullif(p_row ->> 'created_at','')::timestamptz, v_opened),\n coalesce(nullif(p_row ->> 'updated_at','')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.plg_financial_cash_register_sessions SET\n unit_id = coalesce(v_unit, unit_id),\n account_id = coalesce(v_conta, account_id),\n bank_account_id = coalesce(bank_account_id, v_conta),\n status = v_status,\n closed_at = nullif(p_row ->> 'closed_at','')::timestamptz,\n closed_by_name = p_row ->> 'closed_by_name',\n closing_balance = nullif(p_row ->> 'closing_balance','')::numeric,\n expected_balance = nullif(p_row ->> 'expected_balance','')::numeric,\n difference = nullif(p_row ->> 'difference','')::numeric,\n counted_cash = nullif(p_row ->> 'counted_cash','')::numeric,\n counted_card = nullif(p_row ->> 'counted_card','')::numeric,\n difference_cash = nullif(p_row ->> 'difference_cash','')::numeric,\n difference_card = nullif(p_row ->> 'difference_card','')::numeric,\n notes = p_row ->> 'notes',\n metadata = migration._merge_meta(metadata, v_meta),\n updated_at = coalesce(nullif(p_row ->> 'updated_at','')::timestamptz, now())\n WHERE id = p_id;\n END IF;\n\n PERFORM migration._set_unit('public.plg_financial_cash_register_sessions', p_id, v_unit);\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.plg_financial_cash_register_sessions', p_id,\n jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n`\n\nexport const MIGRATION_074_O_MOVIMENTO_VOLTA_A_APONTAR_PARA_O_CAIXA = `-- O movimento volta a apontar para o caixa.\n--\n-- A migration 035 mandou \\`financial_movements.cash_session_id\\` para\n-- \\`metadata.v1_cash_session_ref\\` — uma referência opaca. Era a decisão certa\n-- naquele dia: o caixa não atravessava, e um movimento apontando para uma\n-- sessão inexistente cairia em quarentena de \\`fk\\`. Guardar o número em\n-- metadata liberou 24 movimentos que estavam presos.\n--\n-- Com a 057 o caixa atravessa. A referência deixa de ser opaca e volta a ser\n-- o que sempre foi: uma ligação.\n--\n-- ── O tamanho real disso ────────────────────────────────────────────────────\n--\n-- Medido no V1, tenant 1, em 20/09/2026:\n--\n-- movimentos 277.567\n-- com cash_session_id 268 (49 sessões distintas)\n--\n-- Menos de um por mil. O V1 não associa a gaveta ao movimento como regra — ele\n-- o faz quando o dinheiro passou pelo caixa de fato. A tela\n-- \\`#/financial/cash/registers\\` não depende desse vínculo para listar as\n-- sessões; ele é o que permite abrir uma sessão e ver o que passou por ela.\n--\n-- ── Por que o writer não precisa mudar ──────────────────────────────────────\n--\n-- \\`upsert_fin_movement\\` já escreve \\`cash_session_id\\` — sempre soube. O que\n-- faltava era o de-para entregar um uuid resolvido em vez de um número cru em\n-- metadata. A mudança é de uma linha do mapa, e o backfill abaixo alcança o\n-- que já entrou com a forma antiga.\n--\n-- Idempotente.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nUPDATE migration.allowlist\n SET columns = jsonb_set(columns, '{cash_session_id}',\n '\"cash_session_id@cash_register_sessions\"'::jsonb)\n WHERE source_table = 'financial_movements';\n\nRESET migration.allowlist_unlock;\n\n-- ── O que já entrou NÃO é corrigido, e isso é do destino ────────────────────\n--\n-- \\`trg_plg_financial_movements_append_only\\` recusa mudar \\`cash_session_id\\` de\n-- um evento de dinheiro que já existe:\n--\n-- plg_financial_movements is append-only: money event … cannot be edited\n--\n-- A regra está certa e não tem escotilha de migração, de propósito: depois de\n-- registrado, de qual gaveta saiu um pagamento não se reescreve. Um backfill\n-- aqui não seria conserto, seria reescrita de caixa.\n--\n-- Então os movimentos que já atravessaram com a forma antiga mantêm a gaveta\n-- só em \\`metadata.v1_cash_session_ref\\` — recuperável, e honesto sobre quando\n-- a ligação foi estabelecida. Os que ainda não entraram nascem ligados.\n--\n-- Movimento de conta (\\`bill\\`/\\`commission\\`) é espelho de obrigação, não evento\n-- de dinheiro: o gatilho deixa suas colunas derivadas se moverem, e esses se\n-- ligam na reoferta normalmente.\n`\n\nexport const MIGRATION_075_A_VENDA_CANCELADA_CANCELA_O_TITULO_DELA = `-- A venda cancelada cancela o título dela.\n--\n-- Uma fatura do V1 vira DUAS linhas no V2: a Venda (\\`public.orders\\`) e o\n-- título (\\`public.plg_financial_invoices\\`). O cancelamento chegava só na\n-- primeira.\n--\n-- Medido em 20/09/2026, tenant Espaço Facial:\n--\n-- public.orders com status 'cancelled' 12.678\n-- plg_financial_invoices com cancelled_at 161\n--\n-- Doze mil e quinhentas vendas canceladas cujo título nasceu vivo. E é o\n-- título que o DRE soma — então **item de venda cancelada estava contando como\n-- receita**. A asserção de paridade mediu o tamanho:\n--\n-- dre A (V1) 71.952.346,78 B (V2) 73.518.877,84 Δ +1.566.531,06\n--\n-- Do outro lado, o V1 tem 1.170 faturas \\`canceled\\` em 2026 somando\n-- R$ 1.659.523,42 em itens. Os dois números dizem a mesma coisa.\n--\n-- ── Por que passou ──────────────────────────────────────────────────────────\n--\n-- \\`upsert_order\\` monta a chamada do título com \\`'status', 'open'\\` fixo e mais\n-- nada sobre cancelamento. \\`upsert_invoice\\`, por sua vez, não continha a\n-- palavra \"cancel\" em lugar nenhum — ele nunca soube que um documento podia\n-- nascer cancelado. O dado estava ali o tempo todo, em\n-- \\`metadata.cancellation.at\\`, que o de-para grava desde sempre.\n--\n-- ── A exceção que o destino já tinha escrito ────────────────────────────────\n--\n-- \\`trg_plg_financial_invoices_immutable\\` recusa alterar fatura emitida, com\n-- uma exceção declarada no próprio corpo:\n--\n-- -- The named exception: cancelling, and the bookkeeping that goes with it.\n-- IF NEW.cancelled_at IS NOT NULL AND OLD.cancelled_at IS NULL THEN RETURN NEW;\n--\n-- Ou seja: carimbar o cancelamento que faltava é exatamente o que o destino\n-- permite. Não é reescrever documento financeiro — é completá-lo, e uma vez\n-- só. Uma fatura já cancelada fica como está.\n--\n-- ── O que muda ──────────────────────────────────────────────────────────────\n--\n-- 1. \\`upsert_order\\` desce \\`cancelled_at\\`, \\`cancel_reason\\` e o status da origem.\n-- 2. \\`upsert_invoice\\` grava o carimbo ao criar, e PREENCHE quando a fatura já\n-- existe e está sem ele.\n-- 3. O backfill alcança as 12.517 que já entraram. A verdade vem da Venda, que\n-- é a mesma linha do V1 e já a carrega.\n--\n-- A data do carimbo é a da origem; quando o V1 não guardou a hora, fica na\n-- COMPETÊNCIA — a data que a linha já tem. Mesma regra da 043, pelo mesmo\n-- motivo: \\`now()\\` seria a hora desta migration, não a do cancelamento.\n--\n-- Idempotente.\n\nCREATE OR REPLACE FUNCTION migration.upsert_invoice(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_table text := 'public.plg_financial_invoices';\n v_dir text := coalesce(p_args ->> 'direction', p_row ->> 'direction', 'receivable');\n v_total numeric := round(coalesce((p_row ->> 'total')::numeric, -1), 2);\n v_party uuid;\n v_number text;\n v_insts jsonb;\n v_sum numeric := 0;\n v_i jsonb;\n v_n integer := 0;\n v_due date;\n v_exists boolean;\n v_cancel timestamptz;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF to_regclass(v_table) IS NULL THEN\n RETURN jsonb_build_object('status', 'skipped', 'reason', 'destination table absent: ' || v_table);\n END IF;\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n EXECUTE format('SELECT EXISTS (SELECT 1 FROM %s WHERE id = $1)', v_table) INTO v_exists USING p_id;\n -- Quando a origem diz cancelada, a data vem dela; se ela não guardou a hora,\n -- o carimbo fica na COMPETÊNCIA — a data que a linha já tem, e a única que\n -- não inventa um fato novo. Mesma regra da 043 para a parcela.\n v_cancel := coalesce(\n nullif(p_row ->> 'cancelled_at', '')::timestamptz,\n CASE WHEN coalesce(p_row ->> 'source_status', '') = 'cancelled'\n THEN coalesce(nullif(p_row ->> 'competence_date','')::timestamptz, now()) END);\n\n IF v_exists THEN\n -- Documento financeiro não se reescreve — e não é o que acontece aqui. O\n -- gatilho de imutabilidade do destino abre uma exceção NOMEADA, \"the named\n -- exception: cancelling, and the bookkeeping that goes with it\", e ela\n -- existe para isto. Só PREENCHE: uma fatura já cancelada fica como está.\n IF v_cancel IS NOT NULL THEN\n UPDATE public.plg_financial_invoices\n SET cancelled_at = v_cancel,\n cancel_reason = coalesce(cancel_reason, nullif(p_row ->> 'cancel_reason', ''))\n WHERE id = p_id AND cancelled_at IS NULL;\n END IF;\n -- \\`migrated\\`, e nao \\`skipped\\`. A regra de nao reescrever documento\n -- financeiro continua valendo - nada e atualizado aqui. O que muda e o\n -- NOME que se da ao resultado: a linha existe no destino, com este id,\n -- vinda desta linha da origem. Isso e o estado migrado.\n --\n -- Chama-lo de \\`skipped\\` tinha uma consequencia que nao era de vocabulario.\n -- \\`_upsert_row_impl\\` reexecuta o writer quando alguma IRMA da linha (o\n -- mesmo registro da origem com outro destino) esta \\`pending\\` ou \\`skipped\\` -\n -- e assim que a fatura de uma Venda nasce numa segunda passada, quando o\n -- writer dela nao existia na primeira. Com o titulo marcado \\`skipped\\` para\n -- sempre, essa condicao nunca deixava de ser verdadeira: toda rodada\n -- reexecutava o writer inteiro para chegar de novo aqui.\n --\n -- Medido em 20/09/2026 no tenant Espaco Facial: 7.893 titulos nesse estado,\n -- refeitos a cada rodada antes de o motor sequer chegar nas linhas novas. A\n -- etapa \\`invoices\\` passava minutos sem produzir uma linha, e parecia travada.\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', 'already present; financial documents are never rewritten by the migration');\n END IF;\n\n IF v_dir NOT IN ('receivable', 'payable') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown invoice direction %L', v_dir));\n END IF;\n IF v_total < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'an invoice needs a total >= 0');\n END IF;\n\n -- the counterparty is a Person. A reference that does not resolve is a broken\n -- link, not a column to drop — which is what the old candidate-name loop did.\n BEGIN\n v_party := coalesce((p_row ->> 'counterparty_person_id')::uuid, (p_row ->> 'party_id')::uuid);\n EXCEPTION WHEN OTHERS THEN v_party := NULL; END;\n IF v_party IS NOT NULL AND NOT EXISTS (SELECT 1 FROM public.people p WHERE p.id = v_party AND p.tenant_id = p_tenant) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('counterparty %s is not a Person of this tenant', v_party));\n END IF;\n\n v_due := coalesce((p_row ->> 'due_date')::date, (p_row ->> 'competence_date')::date, current_date);\n\n -- installments: as offered, or one for the whole total. An invoice with no\n -- installment is a document nobody can pay, and the balance views derive\n -- everything from them.\n v_insts := CASE WHEN jsonb_typeof(p_row -> 'installments') = 'array' AND jsonb_array_length(p_row -> 'installments') > 0\n THEN p_row -> 'installments'\n ELSE jsonb_build_array(jsonb_build_object('number', 1, 'due_date', v_due, 'amount', v_total)) END;\n FOR v_i IN SELECT * FROM jsonb_array_elements(v_insts) LOOP\n v_sum := v_sum + round(coalesce((v_i ->> 'amount')::numeric, 0), 2);\n END LOOP;\n IF v_sum <> v_total THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('installments sum to %s but the invoice total is %s — which one is wrong is not a migration''s call', v_sum, v_total));\n END IF;\n\n -- same series as an invoice created in V2: two numbering schemes in one tenant\n -- is the kind of thing nobody notices until an auditor does\n BEGIN\n v_number := public.plg_financial_next_invoice_number(p_tenant, v_dir);\n EXCEPTION WHEN OTHERS THEN v_number := p_row ->> 'number'; END;\n\n INSERT INTO public.plg_financial_invoices\n (id, tenant_id, unit_id, direction, counterparty_person_id, order_id, number,\n competence_date, issue_date, total, description, notes, metadata,\n cancelled_at, cancel_reason)\n VALUES (p_id, p_tenant, (p_row ->> 'unit_id')::uuid, v_dir, v_party, (p_row ->> 'order_id')::uuid, v_number,\n coalesce((p_row ->> 'competence_date')::date, v_due), coalesce((p_row ->> 'issue_date')::date, v_due),\n v_total, p_row ->> 'description', p_row ->> 'notes',\n coalesce(p_row -> 'metadata', '{}'::jsonb),\n v_cancel, nullif(p_row ->> 'cancel_reason', ''));\n\n FOR v_i IN SELECT * FROM jsonb_array_elements(v_insts) LOOP\n v_n := v_n + 1;\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount, metadata)\n VALUES (p_tenant, (p_row ->> 'unit_id')::uuid, p_id,\n coalesce((v_i ->> 'number')::int, v_n),\n coalesce((v_i ->> 'due_date')::date, v_due),\n round(coalesce((v_i ->> 'amount')::numeric, 0), 2),\n CASE WHEN jsonb_typeof(v_i -> 'metadata') = 'object' THEN v_i -> 'metadata' ELSE '{}'::jsonb END);\n END LOOP;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, v_table, p_id, jsonb_build_object('reason', 'created', 'installments', v_n)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created', 'installments', v_n, 'number', v_number);\nEND $function$\n\n;\nCREATE OR REPLACE FUNCTION migration.upsert_order(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_map jsonb := coalesce(p_args -> 'status_map', '{}'::jsonb);\n v_kind_map jsonb := coalesce(p_args -> 'kind_map', '{}'::jsonb);\n v_src_status text := p_row ->> 'status';\n v_status text;\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_src_kind text;\n v_kind text;\n v_direction text;\n v_party uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_invoice jsonb;\n v_invoice_id uuid;\n v_extra jsonb := '[]'::jsonb;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an order needs an id');\n END IF;\n\n -- A espécie sai do \\`type\\` da origem quando o de-para declarou o mapa; sem\n -- mapa, nada muda em relação a antes.\n v_src_kind := v_meta #>> '{financial,v1_type}';\n v_kind := coalesce(v_kind_map ->> coalesce(v_src_kind, ''), p_args ->> 'kind', 'sale');\n IF NOT EXISTS (SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('order kind %L is not declared in public.order_kinds', v_kind));\n END IF;\n -- Compra é documento de despesa: o título dela é a pagar. É o que a própria\n -- descrição de \\`order_kinds.purchase\\` diz.\n v_direction := CASE WHEN v_kind = 'purchase' THEN 'payable' ELSE 'receivable' END;\n\n v_status := coalesce(v_map ->> coalesce(v_src_status, ''), v_map ->> '*', v_src_status, 'completed');\n v_meta := migration._deep_set(v_meta, ARRAY['migration', 'source_status'], to_jsonb(v_src_status));\n\n -- A contraparte. \\`party_id\\` já vem resolvido quando é cliente; quando é\n -- fornecedor ou profissional, quem resolve é o ledger — as três tabelas do\n -- V1 viram \\`public.people\\`, e o ledger sabe qual virou qual.\n BEGIN\n v_party := (p_row ->> 'party_id')::uuid;\n EXCEPTION WHEN OTHERS THEN v_party := NULL; END;\n IF v_party IS NULL AND (v_meta #>> '{financial,v1_ref_table}') IS NOT NULL THEN\n SELECT l.target_id INTO v_party\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = (v_meta #>> '{financial,v1_ref_table}')\n AND l.source_numeric_id = nullif(v_meta #>> '{financial,v1_ref_id}', '')::bigint\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.orders WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order %s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n -- \\`unit_id\\` entra JUNTO, e não num UPDATE logo depois. O UPDATE que vinha\n -- em seguida (via \\`_set_unit\\`) criava uma segunda versão da linha, reescrevia\n -- os onze índices de \\`orders\\` e refazia as checagens de chave estrangeira —\n -- medido em 19/09/2026: 1.371 ms para um UPDATE que não mudava valor nenhum,\n -- quase tudo em revalidação de FK (tenant 646 ms, party 398 ms, unit 275 ms).\n -- Por linha, e são 166.432 faturas.\n INSERT INTO public.orders (id, tenant_id, unit_id, kind, reference_number, status, party_id, subtotal, discount, tax, total, currency, completed_at, notes, metadata)\n VALUES (p_id, p_tenant, (p_row ->> 'unit_id')::uuid, v_kind, p_row ->> 'reference_number', v_status, v_party,\n (p_row ->> 'subtotal')::numeric, coalesce((p_row ->> 'discount')::numeric, 0), coalesce((p_row ->> 'tax')::numeric, 0),\n (p_row ->> 'total')::numeric, coalesce(p_row ->> 'currency', 'BRL'), (p_row ->> 'completed_at')::timestamptz,\n p_row ->> 'notes', v_meta);\n v_created := true;\n ELSE\n -- financial columns are guarded upstream (upsert_row) and not touched here\n UPDATE public.orders SET\n kind = v_kind,\n reference_number = CASE WHEN p_row ? 'reference_number' THEN p_row ->> 'reference_number' ELSE reference_number END,\n status = CASE WHEN p_row ? 'status' THEN v_status ELSE status END,\n party_id = coalesce(v_party, party_id),\n completed_at = CASE WHEN p_row ? 'completed_at' THEN (p_row ->> 'completed_at')::timestamptz ELSE completed_at END,\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n PERFORM migration._set_unit('public.orders', p_id, (p_row ->> 'unit_id')::uuid);\n\n IF coalesce(p_args ->> 'split_invoice', 'false') = 'true' THEN\n v_invoice_id := md5('migration:invoice:' || p_id::text)::uuid;\n v_invoice := migration.upsert_invoice(p_tenant, v_invoice_id,\n jsonb_build_object('order_id', p_id, 'party_id', to_jsonb(v_party), 'unit_id', p_row -> 'unit_id', 'total', p_row -> 'total',\n 'competence_date', p_row -> 'completed_at', 'status', 'open',\n -- O cancelamento desce para a fatura. Sem isto a Venda\n -- nasce cancelada e o TÍTULO dela nasce vivo — e é o\n -- título que o DRE soma.\n 'cancelled_at', to_jsonb(nullif(v_meta #>> '{cancellation,at}', '')),\n 'cancel_reason', to_jsonb(nullif(v_meta #>> '{cancellation,reason}', '')),\n 'source_status', to_jsonb(v_status),\n -- a fatura leva o texto da origem: sem isto a lista do\n -- financeiro mostra a coluna Descrição vazia em tudo\n 'description', p_row -> 'notes',\n 'metadata', v_meta),\n jsonb_build_object('direction', v_direction, 'kind', v_direction));\n v_extra := v_extra || jsonb_build_object('target_table', 'public.plg_financial_invoices', 'target_id', v_invoice_id,\n 'status', CASE WHEN v_invoice ->> 'status' = 'migrated' THEN 'migrated' ELSE 'skipped' END,\n 'reason', v_invoice ->> 'reason',\n 'financial', CASE WHEN v_invoice ->> 'status' = 'migrated' THEN jsonb_build_object('total', p_row -> 'total') ELSE NULL END);\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.orders', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END, 'extra', v_extra);\nEND $function$\n\n;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- O backfill: o título herda o cancelamento da Venda de onde nasceu\n-- ─────────────────────────────────────────────────────────────────────────────\n\nUPDATE public.plg_financial_invoices fi\n SET cancelled_at = coalesce(\n nullif(o.metadata #>> '{cancellation,at}', '')::timestamptz,\n fi.competence_date::timestamptz),\n cancel_reason = coalesce(fi.cancel_reason, nullif(o.metadata #>> '{cancellation,reason}', ''))\n FROM public.orders o\n WHERE o.id = fi.order_id\n AND o.status = 'cancelled'\n AND fi.cancelled_at IS NULL;\n`\n\nexport const MIGRATION_076_O_PLANO_DE_CONTAS_DO_CLIENTE_VENCE_O_DO_MODELO = `-- O plano de contas do cliente vence o do modelo.\n--\n-- O tenant do Espaço Facial nasceu no V2 com um plano de contas SEMENTE, do\n-- template da vertical. O plano real do cliente veio depois, do V1 — e quatro\n-- códigos colidiram:\n--\n-- código semente do V2 o que é no V1\n-- 3.01 Receita de serviços Insumos Utilizados nos Procedimentos\n-- 3.02 Receita de produtos Comissão dos Profissionais de Saúde\n-- 4.01 Custo dos produtos Royalties da Franqueadora\n-- 4.02 Comissões Taxas de Meios de Pagamento\n--\n-- \\`uq_plg_financial_chart_of_accounts_code (tenant_id, code)\\` recusou as\n-- quatro do V1:\n--\n-- 23505: duplicate key value violates unique constraint\n--\n-- E o efeito aparecia longe dali: o item que aponta para a conta 3.01 chegava\n-- SEM classificação, e o DRE mostrava o valor em \"(sem conta)\". A asserção de\n-- paridade mediu R$ 41.191 sob essas quatro contas, em 2026.\n--\n-- ── A medição que decide ────────────────────────────────────────────────────\n--\n-- Quem tem razão sobre o código não é quem chegou primeiro: é quem o USA.\n-- Medido em 20/09/2026, nas quatro contas semente:\n--\n-- itens classificados nelas 0\n-- movimentos nas pernas delas 0\n--\n-- Zero. São linhas de template que ninguém tocou — o tenant é novo no V2 e sua\n-- contabilidade toda vive no V1. Das 120 contas do tenant, 109 vieram do V1 e\n-- 11 são semente.\n--\n-- ── O que se faz com a semente ──────────────────────────────────────────────\n--\n-- Ela não é apagada. O código ganha o prefixo \\`(modelo)\\` e a conta é\n-- desativada — o índice único libera, o plano do cliente entra com o código\n-- dele, e a linha do template continua legível para quem for entender depois\n-- por que havia duas. Apagar resolveria o conflito e esconderia a história.\n--\n-- Só toca em conta semente E sem uso: a condição exige ausência no\n-- \\`migration.ledger\\` e zero referências. Uma conta de template que alguém já\n-- tenha usado no V2 é dado, não modelo, e fica onde está — nesse caso a do V1\n-- continua em quarentena, que é a resposta certa para um conflito real.\n--\n-- Idempotente.\n\nUPDATE public.plg_financial_chart_of_accounts c\n SET code = '(modelo) ' || c.code,\n is_active = false\n WHERE c.tenant_id = 'af800ef8-853d-543f-9940-1e9a5663c061'\n AND c.code IN ('3.01', '3.02', '4.01', '4.02')\n -- semente: não tem linha no ledger\n AND NOT EXISTS (SELECT 1 FROM migration.ledger l\n WHERE l.target_id = c.id AND l.source_table = 'chart_of_accounts')\n -- e sem uso: nem item, nem perna de movimento\n AND NOT EXISTS (SELECT 1 FROM public.items it WHERE it.chart_account_id = c.id)\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_movements m\n WHERE m.debit_account_id = c.id OR m.credit_account_id = c.id);\n`\n\nexport const MIGRATION_077_O_RESUMO_FINANCEIRO_SE_SOMA_NO_BANCO = `-- O resumo financeiro se soma no banco, não no navegador.\n--\n-- O cartão de resumo do painel falhava com 500 e o usuário via\n-- \"Não foi possível carregar o resumo financeiro\". A causa está em\n-- \\`getSummary\\`, e é de forma, não de dado:\n--\n-- pub.from(T.movements).select('direction, movement_kind, amount,\n-- paid_amount, status, due_date, payment_date')\n--\n-- Sem filtro e sem limite. Ele baixa **todos os movimentos do tenant** e soma\n-- em JavaScript com \\`.filter().reduce()\\`. Funcionava com alguns milhares; o\n-- Espaço Facial tem **276.360**, e o que era uma lista virou um pedido\n-- impossível — tempo de consulta, tamanho de resposta e memória do navegador,\n-- os três ao mesmo tempo.\n--\n-- E nenhum dos nove números que a tela mostra precisa das linhas. Todos são\n-- agregados: saldo, a receber, a pagar, entrada e saída do período, contagem e\n-- valor do vencido. Somar no banco devolve uma linha em vez de 276 mil.\n--\n-- ── Onde a regra foi lida ───────────────────────────────────────────────────\n--\n-- Cada expressão abaixo é a tradução literal do \\`.reduce()\\` que ela substitui,\n-- inclusive as decisões que pareciam detalhe:\n--\n-- · \"em aberto\" é \\`status IN (pending, partial, overdue)\\` — não é \\`<> paid\\`,\n-- porque cancelado não está em aberto, está morto.\n-- · \"vencido\" é \\`status = 'overdue' OR due_date < hoje\\` — a tela não confia\n-- só no carimbo, e faz bem: quem vence hoje à noite ainda está pendente no\n-- banco.\n-- · o saldo do período conta só \\`movement_kind = 'payment'\\` — evento de\n-- dinheiro, nunca a obrigação que ele quita. Somar os dois contaria a mesma\n-- venda duas vezes.\n-- · a janela padrão é MÓVEL de 30 dias, e não o mês corrente: no dia 2 um\n-- \"mês até agora\" mostra quase zero e parece defeito.\n--\n-- \\`SECURITY INVOKER\\` de propósito: a função enxerga exatamente o que quem\n-- chamou enxergaria, então a cerca de unidade vale aqui como vale na lista. Um\n-- resumo que somasse o que a tela não mostra seria pior que um resumo que falha.\n--\n-- Idempotente.\n\nCREATE OR REPLACE FUNCTION public.financial_summary(\n p_from date DEFAULT NULL,\n p_to date DEFAULT NULL\n) RETURNS TABLE (\n total_balance numeric,\n total_receivable numeric,\n total_payable numeric,\n monthly_inflow numeric,\n monthly_outflow numeric,\n overdue_receivable_count integer,\n overdue_receivable_amount numeric,\n overdue_payable_count integer,\n overdue_payable_amount numeric\n)\nLANGUAGE sql\nSTABLE\nSECURITY INVOKER\nSET search_path TO ''\nAS $function$\n WITH janela AS (\n SELECT coalesce(p_from, (current_date - 30)) AS de,\n coalesce(p_to, current_date) AS ate\n ),\n saldo AS (\n SELECT coalesce(sum(b.balance), 0) AS v\n FROM public.v_financial_account_balances b\n WHERE b.is_active\n AND b.kind IN ('bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit')\n ),\n aberto AS (\n SELECT\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'), 0) AS receber,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'), 0) AS pagar,\n count(*) FILTER (WHERE m.direction = 'credit'\n AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_receber_n,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'\n AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_receber_v,\n count(*) FILTER (WHERE m.direction = 'debit'\n AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_pagar_n,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'\n AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_pagar_v\n FROM public.plg_financial_movements m\n WHERE m.status IN ('pending', 'partial', 'overdue')\n ),\n caixa AS (\n SELECT\n coalesce(sum(c.paid_amount) FILTER (WHERE c.direction = 'credit'), 0) AS entrada,\n coalesce(sum(c.paid_amount) FILTER (WHERE c.direction = 'debit'), 0) AS saida\n FROM public.v_financial_cash_movements c, janela j\n WHERE c.movement_kind = 'payment'\n AND c.payment_date >= j.de AND c.payment_date <= j.ate\n )\n SELECT saldo.v, aberto.receber, aberto.pagar, caixa.entrada, caixa.saida,\n aberto.venc_receber_n::integer, aberto.venc_receber_v,\n aberto.venc_pagar_n::integer, aberto.venc_pagar_v\n FROM saldo, aberto, caixa;\n$function$;\n\nCOMMENT ON FUNCTION public.financial_summary(date, date) IS\n 'Os nove números do cartão de resumo, somados no banco. Substitui um '\n 'getSummary que baixava TODOS os movimentos do tenant para reduzir em '\n 'JavaScript — 276.360 linhas no Espaço Facial, e um 500 em vez do cartão.';\n\nGRANT EXECUTE ON FUNCTION public.financial_summary(date, date) TO authenticated, service_role;\n`\n\nexport const MIGRATION_078_O_SALDO_DA_CONTA_SE_CALCULA_SO_NA_CONTA_PEDIDA = `-- O saldo da conta se calcula só na conta pedida.\n--\n-- Terceira vez que este erro aparece neste pool, e sempre com a mesma forma:\n-- uma CTE que agrega tudo antes de o filtro existir.\n--\n-- WITH debits AS (SELECT ... GROUP BY debit_account_id),\n-- credits AS (SELECT ... GROUP BY credit_account_id)\n-- SELECT ... FROM plg_financial_accounts account\n-- LEFT JOIN debits ON debits.account_id = account.id\n-- LEFT JOIN credits ON credits.account_id = account.id;\n--\n-- Uma CTE é materializada INTEIRA antes de o \\`WHERE\\` de quem a consome\n-- aparecer. Então quem pede o saldo de 156 contas paga a agregação de todas.\n--\n-- ── A conta, medida ─────────────────────────────────────────────────────────\n--\n-- Tenant Espaço Facial, 20/09/2026:\n--\n-- contas totais 120.516\n-- das quais kind = 'person' 120.290 (o razão de cliente do V1)\n-- das quais o resumo do painel usa 156 (bank, cash_*, card_*)\n--\n-- somar o saldo dessas 156 21.849 ms\n--\n-- Vinte e dois segundos para somar cento e cinquenta e seis contas. O porte do\n-- V1 trouxe uma conta de razão por cliente, e foi isso que expôs a forma —\n-- antes eram dezenas de contas e ninguém sentia.\n--\n-- ── O conserto ──────────────────────────────────────────────────────────────\n--\n-- \\`LEFT JOIN LATERAL\\` correlacionado em \\`account.id\\`. A correlação deixa de ser\n-- condição de junção e vira PARÂMETRO: o planejador só soma os movimentos das\n-- contas que sobreviveram ao filtro, pelos índices que já existem\n-- (\\`plg_financial_movements_debit_idx\\` e \\`_credit_idx\\`).\n--\n-- Nada muda no que a view devolve: as mesmas colunas, a mesma aritmética, a\n-- mesma regra de sinal por espécie de conta. Muda quantas contas ela visita.\n--\n-- Idempotente.\n\n-- \\`v_financial_customer_credit\\` pende desta view — conferido em \\`pg_depend\\`\n-- antes de derrubar, que é a lição que já custou sete views num CASCADE. O\n-- CASCADE a levava junto e ninguém percebia até\n-- \\`financial_credit_customer()\\` procurar por ela:\n--\n-- ERROR: relation \"public.v_financial_customer_credit\" does not exist\n--\n-- Cai junto, de propósito e por escrito, e volta logo abaixo com a mesma\n-- definição do baseline.\nDROP VIEW IF EXISTS public.v_financial_customer_credit;\nDROP VIEW IF EXISTS public.v_financial_account_balances CASCADE;\n\nCREATE VIEW public.v_financial_account_balances WITH (security_invoker = true) AS\nSELECT\n account.id AS account_id,\n account.tenant_id,\n account.unit_id,\n account.kind,\n account.code,\n account.name,\n account.person_id,\n account.is_active,\n account.account_type,\n account.bank_name,\n account.agency_number,\n account.account_number,\n account.metadata,\n COALESCE(d.total, 0::numeric)::numeric(14,2) AS debit_total,\n COALESCE(cr.total, 0::numeric)::numeric(14,2) AS credit_total,\n (COALESCE(cr.total, 0::numeric) - COALESCE(d.total, 0::numeric))::numeric(14,2) AS net_credit,\n -- A regra de sinal não muda: na conta de dinheiro o débito SOMA (entrou no\n -- caixa); nas outras é o crédito que soma.\n CASE WHEN account.kind = ANY (ARRAY['bank','cash_physical','cash_session',\n 'card_receivable_credit','card_receivable_debit'])\n THEN (COALESCE(d.total, 0::numeric) - COALESCE(cr.total, 0::numeric))::numeric(14,2)\n ELSE (COALESCE(cr.total, 0::numeric) - COALESCE(d.total, 0::numeric))::numeric(14,2)\n END AS balance,\n (COALESCE(d.n, 0::bigint) + COALESCE(cr.n, 0::bigint))::integer AS movement_count,\n account.created_at,\n account.updated_at\n FROM public.plg_financial_accounts account\n LEFT JOIN LATERAL (\n SELECT sum(m.amount) AS total, count(*) AS n\n FROM public.v_financial_cash_movements m\n WHERE m.debit_account_id = account.id\n AND m.status <> 'cancelled'\n AND m.movement_kind = ANY (ARRAY['payment','transfer'])\n ) d ON true\n LEFT JOIN LATERAL (\n SELECT sum(m.amount) AS total, count(*) AS n\n FROM public.v_financial_cash_movements m\n WHERE m.credit_account_id = account.id\n AND m.status <> 'cancelled'\n AND m.movement_kind = ANY (ARRAY['payment','transfer'])\n ) cr ON true;\n\nCOMMENT ON VIEW public.v_financial_account_balances IS\n 'Saldo de cada conta, dos movimentos dela. LATERAL correlacionado em '\n 'account.id para que a soma aconteça só nas contas que a consulta pediu — a '\n 'forma anterior agregava as 120.290 contas de razão de cliente para somar as '\n '156 do caixa, e o resumo do painel levava 22 s.';\n\nGRANT SELECT ON public.v_financial_account_balances TO authenticated, service_role;\n\nCREATE VIEW public.v_financial_customer_credit WITH (security_invoker='true') AS\n SELECT b.tenant_id,\n b.person_id,\n b.account_id,\n p.name AS person_name,\n b.credit_total AS credited,\n b.debit_total AS consumed,\n b.balance AS credit,\n b.movement_count,\n b.updated_at\n FROM (public.v_financial_account_balances b\n LEFT JOIN public.people p ON ((p.id = b.person_id)))\n WHERE (b.kind = 'person'::text);\n\nGRANT SELECT ON public.v_financial_customer_credit TO authenticated, service_role;\n`\n\nexport const MIGRATION_079_AS_PORTAS_DE_PORTE_NOVAS_NAO_NASCEM_ABERTAS = `-- As portas de porte novas não nascem abertas\n--\n-- As 077–079 trouxeram três funções para o schema \\`migration\\`, e toda função\n-- no Postgres nasce com EXECUTE para PUBLIC. O contrato do ledger mede isso e\n-- não abre exceção, pelo motivo certo: as funções de \\`migration\\` são escrita\n-- em massa no banco do cliente, por fora da cerca da tela. Uma delas chamável\n-- por \\`authenticated\\` é uma porta de escrita em massa aberta a quem só tinha\n-- direito de ler a própria conta.\n--\n-- Idempotente.\n\nREVOKE ALL ON FUNCTION migration.upsert_invoice(uuid, uuid, jsonb, jsonb)\n FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION migration.upsert_fin_movement(uuid, uuid, jsonb, jsonb)\n FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION migration._fin_installment_from_bill(uuid, uuid, uuid, uuid, integer, date, numeric, boolean, boolean)\n FROM PUBLIC, anon, authenticated;\n`\n\nexport const MIGRATION_080_A_REMESSA_E_A_COMISSAO_A_PAGAR_TEM_ONDE_MORAR = `-- A remessa bancária e a comissão a pagar têm onde morar.\n--\n-- Cinco tabelas do V1 sem destino, dois assuntos:\n--\n-- cnab_remessas 59 o arquivo de remessa enviado ao banco\n-- cnab_remessa_items 700 o pagamento dentro dele\n-- commission_payables 1 a comissão apurada virando conta a pagar\n-- commission_payable_items 3 a linha da apuração dentro dela\n-- commission_payee_prefs 1 para qual conta contábil vai a comissão\n--\n-- O volume é pequeno — CNAB é do Pertinho do Céu, comissão a pagar mal começou\n-- no Espaço Facial. Mas nenhuma das duas é derivável: a remessa é um arquivo\n-- que já saiu para o banco com um número de sequência que não pode repetir, e a\n-- comissão a pagar é a ponte entre o que foi APURADO e o Título que alguém vai\n-- pagar. Reconstituir qualquer uma seria inventar.\n--\n-- ── O que o V2 já tinha, e por isso não se repete aqui ──────────────────────\n--\n-- \\`plg_financial_commission_entries\\`, \\`_generations\\`, \\`_rules\\`,\n-- \\`_rule_criteria\\` e \\`_functions\\` já existem. O que faltava era o PAGÁVEL — o\n-- elo entre a apuração e o contas a pagar. Por isso estas duas tabelas e não um\n-- domínio inteiro.\n--\n-- ── O arquivo de remessa é arquivo ──────────────────────────────────────────\n--\n-- \\`cnab_remessas.file_content\\` guarda o CNAB inteiro numa coluna de texto.\n-- Mesma decisão da caixa de entrada da NF-e: arquivo mora em\n-- \\`public.documents\\`, e aqui fica o ponteiro. O que NÃO sai da linha é\n-- \\`sequence_number\\`: ele é do banco, não do arquivo — dois arquivos com o mesmo\n-- número são uma remessa rejeitada.\n--\n-- Idempotente.\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_cnab_batches (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n bank_account_id uuid,\n layout text,\n -- O número que o banco conta. Único por conta: repetir é remessa rejeitada.\n sequence_number integer,\n total_amount numeric(14,2) DEFAULT 0 NOT NULL,\n items_count integer DEFAULT 0 NOT NULL,\n file_name text,\n -- O CNAB em si vive em public.documents; aqui fica o ponteiro.\n document_id uuid,\n generated_at timestamp with time zone DEFAULT now() NOT NULL,\n owner_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_financial_cnab_batches IS\n 'O arquivo de remessa enviado ao banco. V1: cnab_remessas. O arquivo mora em public.documents; o número de sequência mora aqui, porque é do banco.';\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_cnab_items (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n batch_id uuid NOT NULL,\n movement_id uuid,\n payment_form text,\n payment_date date,\n amount numeric(14,2) DEFAULT 0 NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_financial_cnab_items IS\n 'O pagamento dentro da remessa, ligado ao movimento financeiro que ele quita. V1: cnab_remessa_items.';\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_commission_payables (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n -- O Título que a apuração gerou. É este elo que faltava no V2.\n invoice_id uuid,\n beneficiary_party_id uuid,\n chart_account_id uuid,\n due_date date,\n total_amount numeric(14,2) DEFAULT 0 NOT NULL,\n entries_count integer DEFAULT 0 NOT NULL,\n status text DEFAULT 'pending'::text NOT NULL,\n launched_at timestamp with time zone,\n canceled_at timestamp with time zone,\n -- Quem cancelou: a tela, o estorno do Título, ou a reapuração. O V1 guarda\n -- a palavra e ela explica por que a comissão sumiu do relatório.\n cancel_source text,\n owner_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_financial_commission_payables IS\n 'A comissão apurada virando conta a pagar — o elo entre plg_financial_commission_entries e o Título. V1: commission_payables.';\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_commission_payable_items (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n payable_id uuid NOT NULL,\n -- A linha da venda que gerou a comissão, e a entrada de apuração dela.\n source_item_id uuid,\n item_id uuid,\n aggregate_entry_id uuid,\n description text,\n amount numeric(14,2) DEFAULT 0 NOT NULL,\n -- \\`entry_ids\\` do V1 é um array de apurações agregadas numa linha só. Vem\n -- como jsonb de referências: são ids do V1 que só resolvem depois que\n -- commission_entries atravessar, e um array de uuid faria a linha mentir\n -- enquanto isso.\n entry_refs jsonb DEFAULT '[]'::jsonb NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_commission_payee_prefs (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n party_id uuid NOT NULL,\n chart_account_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_financial_commission_payee_prefs IS\n 'Para qual conta contábil a comissão daquela pessoa vai. Uma por pessoa. V1: commission_payee_prefs.';\n\n-- ── chaves ──────────────────────────────────────────────────────────────────\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_financial_cnab_batches', 'plg_financial_cnab_items',\n 'plg_financial_commission_payables', 'plg_financial_commission_payable_items',\n 'plg_financial_commission_payee_prefs'] LOOP\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = ('public.' || t)::regclass AND contype = 'p') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I PRIMARY KEY (id)', t, t || '_pkey');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_id_id_key') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I UNIQUE (tenant_id, id)', t, t || '_tenant_id_id_key');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_fkey') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE', t, t || '_tenant_fkey');\n END IF;\n END LOOP;\nEND $$;\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_batches_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_cnab_batches ADD CONSTRAINT plg_financial_cnab_batches_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_batches_account_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_cnab_batches ADD CONSTRAINT plg_financial_cnab_batches_account_fkey\n FOREIGN KEY (bank_account_id) REFERENCES public.plg_financial_bank_accounts(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_batches_document_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_cnab_batches ADD CONSTRAINT plg_financial_cnab_batches_document_fkey\n FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_items_batch_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_cnab_items ADD CONSTRAINT plg_financial_cnab_items_batch_fkey\n FOREIGN KEY (tenant_id, batch_id) REFERENCES public.plg_financial_cnab_batches(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_items_movement_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_cnab_items ADD CONSTRAINT plg_financial_cnab_items_movement_fkey\n FOREIGN KEY (movement_id) REFERENCES public.plg_financial_movements(id) ON DELETE SET NULL;\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_commission_payables ADD CONSTRAINT plg_financial_commission_payables_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_invoice_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_commission_payables ADD CONSTRAINT plg_financial_commission_payables_invoice_fkey\n FOREIGN KEY (invoice_id) REFERENCES public.plg_financial_invoices(id) ON DELETE SET NULL;\n END IF;\n -- Só enquanto a coluna existe com ESTE nome. A 082 renomeia\n -- \\`beneficiary_party_id\\` para \\`beneficiary_account_id\\` (o beneficiário da\n -- comissão é uma CONTA, não uma pessoa), e num replay este arquivo roda de\n -- novo sobre a tabela já corrigida: sem a guarda, ele estoura com \"column\n -- beneficiary_party_id referenced in foreign key constraint does not exist\" e\n -- derruba a corrente inteira.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_party_fkey')\n AND EXISTS (SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'plg_financial_commission_payables'\n AND column_name = 'beneficiary_party_id') THEN\n ALTER TABLE ONLY public.plg_financial_commission_payables ADD CONSTRAINT plg_financial_commission_payables_party_fkey\n FOREIGN KEY (beneficiary_party_id) REFERENCES public.people(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_chart_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_commission_payables ADD CONSTRAINT plg_financial_commission_payables_chart_fkey\n FOREIGN KEY (chart_account_id) REFERENCES public.plg_financial_chart_of_accounts(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payable_items_payable_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_commission_payable_items ADD CONSTRAINT plg_financial_commission_payable_items_payable_fkey\n FOREIGN KEY (tenant_id, payable_id) REFERENCES public.plg_financial_commission_payables(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payable_items_item_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_commission_payable_items ADD CONSTRAINT plg_financial_commission_payable_items_item_fkey\n FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE SET NULL;\n END IF;\n -- Mesma história: a 082 renomeia \\`party_id\\` para \\`account_id\\` aqui.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payee_prefs_party_fkey')\n AND EXISTS (SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'plg_financial_commission_payee_prefs'\n AND column_name = 'party_id') THEN\n ALTER TABLE ONLY public.plg_financial_commission_payee_prefs ADD CONSTRAINT plg_financial_commission_payee_prefs_party_fkey\n FOREIGN KEY (party_id) REFERENCES public.people(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payee_prefs_chart_fkey') THEN\n ALTER TABLE ONLY public.plg_financial_commission_payee_prefs ADD CONSTRAINT plg_financial_commission_payee_prefs_chart_fkey\n FOREIGN KEY (chart_account_id) REFERENCES public.plg_financial_chart_of_accounts(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\n-- Dois arquivos de remessa com o mesmo número na mesma conta é remessa\n-- rejeitada pelo banco. O índice é a trava que o V1 não tinha.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_financial_cnab_batches_sequence_uidx\n ON public.plg_financial_cnab_batches USING btree (tenant_id, bank_account_id, sequence_number)\n WHERE bank_account_id IS NOT NULL AND sequence_number IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_financial_cnab_items_batch_idx\n ON public.plg_financial_cnab_items USING btree (tenant_id, batch_id);\nDO $$ BEGIN\n IF EXISTS (SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'plg_financial_commission_payables'\n AND column_name = 'beneficiary_party_id') THEN\n CREATE INDEX IF NOT EXISTS plg_financial_commission_payables_party_idx\n ON public.plg_financial_commission_payables USING btree (tenant_id, beneficiary_party_id, due_date DESC);\n END IF;\nEND $$;\nCREATE INDEX IF NOT EXISTS plg_financial_commission_payables_open_idx\n ON public.plg_financial_commission_payables USING btree (tenant_id, unit_id, due_date)\n WHERE canceled_at IS NULL;\nCREATE INDEX IF NOT EXISTS plg_financial_commission_payable_items_payable_idx\n ON public.plg_financial_commission_payable_items USING btree (tenant_id, payable_id);\n-- Uma preferência por pessoa: duas seriam duas respostas para \"onde lançar\".\n-- Guardado pelo mesmo motivo da FK acima: a 082 renomeia esta coluna.\nDO $$ BEGIN\n IF EXISTS (SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'plg_financial_commission_payee_prefs'\n AND column_name = 'party_id') THEN\n CREATE UNIQUE INDEX IF NOT EXISTS plg_financial_commission_payee_prefs_party_uidx\n ON public.plg_financial_commission_payee_prefs USING btree (tenant_id, party_id);\n END IF;\nEND $$;\n\n-- ── gatilho de updated_at ───────────────────────────────────────────────────\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_financial_cnab_batches', 'plg_financial_cnab_items',\n 'plg_financial_commission_payables', 'plg_financial_commission_payable_items',\n 'plg_financial_commission_payee_prefs'] LOOP\n EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', t || '_updated_at', t);\n EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()', t || '_updated_at', t);\n END LOOP;\nEND $$;\n\n-- ── quem enxerga: o scaffold, e não policy escrita à mão ────────────────────\n--\n-- \\`app.scaffold_table\\` é a porta única de autorização do financeiro: ele\n-- registra a tabela em \\`app.scaffold_registry\\`, gera as políticas com o nome\n-- que o gate cobra (\\`*_authz_*\\`) e liga a permissão do plugin junto com as\n-- escotilhas de compartilhamento. Escrever a policy à mão criaria a SEGUNDA\n-- regra de autorização do financeiro — e foi exatamente o que este arquivo fez\n-- na primeira versão: 41 asserções de \\`004_negative_authz\\` viraram 40, porque\n-- as cinco tabelas tinham política e não tinham declaração.\n--\n-- \\`unit_optional\\` nas que têm \\`unit_id\\` anulável (a remessa pode ser da rede, a\n-- comissão pode não ter filial); \\`tenant\\` nas três que não têm unidade nenhuma\n-- — declarar \\`unit\\` numa coluna que não existe esconde a tabela inteira.\nSELECT app.scaffold_table('public.plg_financial_cnab_batches'::regclass,\n 'financial.cnab_batch', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_cnab_batches'::regclass, 'unit_optional', true);\n\nSELECT app.scaffold_table('public.plg_financial_cnab_items'::regclass,\n 'financial.cnab_item', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_cnab_items'::regclass, 'tenant', true);\n\nSELECT app.scaffold_table('public.plg_financial_commission_payables'::regclass,\n 'financial.commission_payable', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_commission_payables'::regclass, 'unit_optional', true);\n\nSELECT app.scaffold_table('public.plg_financial_commission_payable_items'::regclass,\n 'financial.commission_payable_item', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_commission_payable_items'::regclass, 'tenant', true);\n\nSELECT app.scaffold_table('public.plg_financial_commission_payee_prefs'::regclass,\n 'financial.commission_payee_pref', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_commission_payee_prefs'::regclass, 'tenant', true);\n`\n\nexport const MIGRATION_081_A_REMESSA_E_A_COMISSAO_ATRAVESSAM = `-- A remessa e a comissão atravessam.\n--\n-- A 080 fez as cinco tabelas. Esta escreve o de-para, com o writer genérico\n-- \\`by_shape\\` (spine 123) — as colunas do destino têm o nome que o de-para\n-- produz, então o que falta é a tradução, não uma função por tabela.\n--\n-- ── O arquivo de remessa não vem por aqui ───────────────────────────────────\n--\n-- \\`cnab_remessas.file_content\\` guarda o CNAB inteiro em texto. Sai com \\`-\\`:\n-- arquivo atravessa pelo estágio de assets e vira \\`public.documents\\`. O que\n-- FICA na linha é \\`sequence_number\\`, porque ele é do banco — dois arquivos com\n-- o mesmo número são uma remessa rejeitada, e isso é dado, não anexo.\n--\n-- ── As apurações agregadas ficam como referência ────────────────────────────\n--\n-- \\`commission_payable_items.entry_ids\\` é um array de ids de apuração agregados\n-- numa linha só. Vem como \\`entry_refs\\` jsonb com os ids do V1 dentro: eles só\n-- resolvem depois que \\`commission_entries\\` atravessar, e um array de uuid faria\n-- a linha mentir enquanto isso.\n--\n-- Idempotente.\n\n-- ── \\`by_shape\\` volta ao vocabulário ────────────────────────────────────────\n--\n-- O spine 123 acrescenta \\`by_shape\\` ao \\`allowlist_writer_check\\`. Ele NÃO\n-- sobrevive até aqui: \\`financial/061\\` reescreve a restrição com uma lista\n-- LITERAL, e a lista dele foi escrita antes de o spine 123 existir. Toda\n-- palavra que o spine acrescenta morre na passagem por 061 — foi assim que a\n-- corrente do zero parou nesta linha com\n-- \\`new row for relation \"allowlist\" violates check constraint\\`.\n--\n-- A ironia está no cabeçalho do próprio 061: \"A LISTA É A UNIÃO, E NÃO UMA\n-- LISTA LITERAL\". Ele descreve a regra e faz o contrário.\n--\n-- O acréscimo é refeito aqui porque este é o PRIMEIRO consumidor de \\`by_shape\\`\n-- depois de 061 — fiscal_br/003, inventory/007 e tables/007 vêm adiante e\n-- herdam o conserto. A correção de verdade é converter 061 para o splice, e\n-- isso é do dono daquele arquivo.\nDO $$\nDECLARE v_def text;\nBEGIN\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n IF v_def IS NULL OR strpos(v_def, 'by_shape') > 0 THEN RETURN; END IF;\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''person''::text', '''person''::text, ''by_shape''::text');\nEND $$;\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'cnab_remessas', 'public.plg_financial_cnab_batches', 'financial', 'by_shape',\n jsonb_build_object('target', 'public.plg_financial_cnab_batches'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'unit_id', 'unit_id@companies',\n 'bank_account_id', 'bank_account_id@bank_accounts',\n 'layout', 'layout',\n 'sequence_number', 'sequence_number',\n 'total_amount', 'total_amount',\n 'items_count', 'items_count',\n 'file_name', 'file_name',\n 'created_at', 'generated_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'generated_by', '-',\n 'file_content', '-',\n 'id', 'jsonb:financial.v1_ref'),\n 'O arquivo de remessa enviado ao banco. O CNAB em si vai pelo estágio de '\n 'assets; o número de sequência fica na linha porque é do banco.'\n);\n\nSELECT migration._allow(\n 'cnab_remessa_items', 'public.plg_financial_cnab_items', 'financial', 'by_shape',\n jsonb_build_object('target', 'public.plg_financial_cnab_items'),\n NULL::text[],\n jsonb_build_object(\n 'remessa_id', 'batch_id@cnab_remessas',\n 'financial_movement_id', 'movement_id@financial_movements',\n 'payment_form', 'payment_form',\n 'payment_date', 'payment_date',\n 'amount', 'amount',\n 'created_at', 'created_at',\n 'tenant_id', '-',\n 'id', 'jsonb:financial.v1_ref'),\n 'O pagamento dentro da remessa. Sem \\`uuid\\` na origem — o id do destino nasce '\n 'determinístico da chave numérica.'\n);\n\nSELECT migration._allow(\n 'commission_payables', 'public.plg_financial_commission_payables', 'financial', 'by_shape',\n jsonb_build_object('target', 'public.plg_financial_commission_payables'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'unit_id', 'unit_id@companies',\n 'invoice_id', 'invoice_id@invoices',\n 'beneficiary_account_central_id', 'beneficiary_party_id@account_central',\n 'chart_account_id', 'chart_account_id@chart_of_accounts',\n 'due_date', 'due_date',\n 'total_amount', 'total_amount',\n 'entries_count', 'entries_count',\n 'status', 'status',\n 'launched_at', 'launched_at',\n 'canceled_at', 'canceled_at',\n 'cancel_source', 'cancel_source',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'invoice_uuid', '-',\n 'launched_by', '-',\n 'id', 'jsonb:financial.v1_ref'),\n 'A comissão apurada virando conta a pagar — o elo entre a apuração e o '\n 'Título, que era o que faltava no V2. \\`cancel_source\\` vem junto porque é ele '\n 'que explica por que a comissão sumiu do relatório.'\n);\n\nSELECT migration._allow(\n 'commission_payable_items', 'public.plg_financial_commission_payable_items', 'financial', 'by_shape',\n jsonb_build_object('target', 'public.plg_financial_commission_payable_items'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'payable_id', 'payable_id@commission_payables',\n 'source_invoice_item_id', 'source_item_id@invoice_items',\n 'invoice_item_id', 'item_id@invoice_items',\n 'description', 'description',\n 'amount', 'amount',\n 'entry_ids', 'entry_refs',\n 'aggregate_entry_id', 'jsonb:financial.v1_aggregate_entry_ref',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:financial.v1_ref'),\n 'A linha da apuração dentro do pagável. \\`entry_ids\\` vira \\`entry_refs\\` jsonb '\n 'com os ids do V1: eles só resolvem quando commission_entries atravessar.'\n);\n\nSELECT migration._allow(\n 'commission_payee_prefs', 'public.plg_financial_commission_payee_prefs', 'financial', 'by_shape',\n jsonb_build_object('target', 'public.plg_financial_commission_payee_prefs'),\n NULL::text[],\n jsonb_build_object(\n 'account_central_id', 'party_id@account_central',\n 'chart_account_id', 'chart_account_id@chart_of_accounts',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:financial.v1_ref'),\n 'Para qual conta contábil a comissão daquela pessoa vai. Uma por pessoa — '\n 'duas seriam duas respostas para \"onde lançar\".'\n);\n\nDELETE FROM migration.excluded_tables\n WHERE source_table IN ('cnab_remessas', 'cnab_remessa_items', 'commission_payables',\n 'commission_payable_items', 'commission_payee_prefs');\n\nRESET migration.allowlist_unlock;\n`\n\nexport const MIGRATION_082_O_BENEFICIARIO_DA_COMISSAO_E_UMA_CONTA = `-- O beneficiário da comissão é uma CONTA, não uma pessoa.\n--\n-- A 080 declarou \\`beneficiary_party_id uuid → public.people\\` e a 081 mapeou\n-- \\`commission_payables.beneficiary_account_central_id\\` para ela. As duas\n-- estavam erradas, e a FK cobrou na primeira onda:\n--\n-- 23503: insert or update on table \"plg_financial_commission_payables\"\n-- violates foreign key constraint \"plg_financial_commission_payables_party_fkey\"\n--\n-- \\`account_central\\` do V1 não é pessoa: é o razão de QUEM RECEBE, e ele\n-- atravessa para \\`public.plg_financial_accounts\\` (writer \\`fin_account\\`). O id\n-- que o ledger devolveu é de uma conta, e a coluna exigia uma pessoa.\n--\n-- Não é um detalhe de nome. Uma comissão pode ser paga a uma conta que não é de\n-- ninguém — um centro, um fundo, a própria casa — e forçar uma pessoa ali faria\n-- essas linhas não existirem. A mesma correção vale para\n-- \\`commission_payee_prefs\\`, que responde \"para qual conta contábil a comissão\n-- daquele beneficiário vai\".\n--\n-- São 5 linhas no Espaço Facial. O volume é irrelevante; o modelo, não.\n--\n-- Idempotente.\n\nDO $$ BEGIN\n IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_party_fkey') THEN\n ALTER TABLE public.plg_financial_commission_payables\n DROP CONSTRAINT plg_financial_commission_payables_party_fkey;\n END IF;\n IF EXISTS (SELECT 1 FROM pg_attribute a\n WHERE a.attrelid = 'public.plg_financial_commission_payables'::regclass\n AND a.attname = 'beneficiary_party_id' AND NOT a.attisdropped) THEN\n ALTER TABLE public.plg_financial_commission_payables\n RENAME COLUMN beneficiary_party_id TO beneficiary_account_id;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_account_fkey') THEN\n ALTER TABLE public.plg_financial_commission_payables\n ADD CONSTRAINT plg_financial_commission_payables_account_fkey\n FOREIGN KEY (beneficiary_account_id) REFERENCES public.plg_financial_accounts(id) ON DELETE SET NULL;\n END IF;\n\n IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payee_prefs_party_fkey') THEN\n ALTER TABLE public.plg_financial_commission_payee_prefs\n DROP CONSTRAINT plg_financial_commission_payee_prefs_party_fkey;\n END IF;\n IF EXISTS (SELECT 1 FROM pg_attribute a\n WHERE a.attrelid = 'public.plg_financial_commission_payee_prefs'::regclass\n AND a.attname = 'party_id' AND NOT a.attisdropped) THEN\n ALTER TABLE public.plg_financial_commission_payee_prefs\n RENAME COLUMN party_id TO account_id;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payee_prefs_account_fkey') THEN\n ALTER TABLE public.plg_financial_commission_payee_prefs\n ADD CONSTRAINT plg_financial_commission_payee_prefs_account_fkey\n FOREIGN KEY (account_id) REFERENCES public.plg_financial_accounts(id) ON DELETE CASCADE;\n END IF;\nEND $$;\n\nDROP INDEX IF EXISTS public.plg_financial_commission_payables_party_idx;\nDROP INDEX IF EXISTS public.plg_financial_commission_payee_prefs_party_uidx;\nCREATE INDEX IF NOT EXISTS plg_financial_commission_payables_account_idx\n ON public.plg_financial_commission_payables USING btree (tenant_id, beneficiary_account_id, due_date DESC);\nCREATE UNIQUE INDEX IF NOT EXISTS plg_financial_commission_payee_prefs_account_uidx\n ON public.plg_financial_commission_payee_prefs USING btree (tenant_id, account_id);\n\nCOMMENT ON COLUMN public.plg_financial_commission_payables.beneficiary_account_id IS\n 'V1: commission_payables.beneficiary_account_central_id — o razão de quem recebe, que pode não ser uma pessoa.';\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nUPDATE migration.allowlist\n SET columns = columns\n - 'beneficiary_account_central_id'\n || jsonb_build_object('beneficiary_account_central_id', 'beneficiary_account_id@account_central')\n WHERE source_table = 'commission_payables';\n\nUPDATE migration.allowlist\n SET columns = columns\n - 'account_central_id'\n || jsonb_build_object('account_central_id', 'account_id@account_central')\n WHERE source_table = 'commission_payee_prefs';\n\nRESET migration.allowlist_unlock;\n`\n\nexport const MIGRATION_083_A_CONTA_DO_BANCO_E_UMA_CONTA_E_A_FATURA_TEM_DOIS_DESTINOS = `-- A conta do banco é uma conta, e a fatura tem dois destinos.\n--\n-- Duas correções que a primeira onda cobrou, das mesmas 59 + 1 linhas.\n-- (A terceira, do índice de grupo fiscal, mudou de arquivo: ela toca uma\n-- tabela do fiscal_br, e o fiscal_br roda DEPOIS do financeiro na corrente —\n-- um arquivo do financeiro que mexe na tabela de outro plugin quebra do zero.\n-- Ver fiscal_br/004.)\n--\n-- ── 1. \\`bank_accounts\\` do V1 não vira \\`plg_financial_bank_accounts\\` ─────────\n--\n-- A 080 declarou \\`plg_financial_cnab_batches.bank_account_id\\` apontando para\n-- \\`public.plg_financial_bank_accounts\\`. Mas o de-para manda \\`bank_accounts\\` do\n-- V1 para \\`public.plg_financial_accounts\\` — o mesmo destino de\n-- \\`account_central\\`, porque no V2 a conta bancária é uma CONTA do razão, não\n-- uma tabela paralela. As 59 remessas do Pertinho do Céu bateram na FK.\n--\n-- É o mesmo erro da 082, no arquivo ao lado: supor o destino pelo nome da\n-- tabela de origem em vez de ler a allowlist.\n--\n-- ── 2. \\`invoices\\` resolve para DOIS destinos ────────────────────────────────\n--\n-- \\`invoices\\` do V1 tem duas linhas de de-para: \\`public.orders\\` (a Venda) e\n-- \\`public.plg_financial_invoices\\` (o Título). \\`migration.resolve_ref\\` sem alvo\n-- declarado lê \\`SELECT target_table ... WHERE source_table = 'invoices'\\` e pega\n-- UMA das duas, sem ordem — então \\`invoice_id@invoices\\` devolvia às vezes o id\n-- de uma Venda para uma coluna que exige um Título.\n--\n-- O de-para já tem a sintaxe para isso e a 081 não a usou:\n-- \\`coluna@tabela_de_origem:tabela_de_destino\\`. Uma referência a uma origem de\n-- dois destinos SEM o alvo é ambígua por construção, e acerta por acidente\n-- enquanto houver sorte.\n--\n-- Idempotente.\n\nDO $$ BEGIN\n IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_batches_account_fkey') THEN\n ALTER TABLE public.plg_financial_cnab_batches DROP CONSTRAINT plg_financial_cnab_batches_account_fkey;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_batches_ledger_account_fkey') THEN\n ALTER TABLE public.plg_financial_cnab_batches\n ADD CONSTRAINT plg_financial_cnab_batches_ledger_account_fkey\n FOREIGN KEY (bank_account_id) REFERENCES public.plg_financial_accounts(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\nCOMMENT ON COLUMN public.plg_financial_cnab_batches.bank_account_id IS\n 'A conta do razão de onde a remessa sai. V1: cnab_remessas.bank_account_id → bank_accounts, que atravessa para plg_financial_accounts.';\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\n-- \\`:public.plg_financial_invoices\\` é o alvo declarado. Sem ele a resolução\n-- escolhe entre a Venda e o Título, e escolher não é resolver.\nUPDATE migration.allowlist\n SET columns = columns - 'invoice_id'\n || jsonb_build_object('invoice_id', 'invoice_id@invoices:public.plg_financial_invoices')\n WHERE source_table = 'commission_payables';\n\nRESET migration.allowlist_unlock;\n`\n\nexport const MIGRATION_084_A_NOTA_DE_ENTRADA_TEM_UM_TITULO_A_PAGAR = `-- A nota de entrada tem um título a pagar.\n--\n-- \\`danfe_payments\\` é o plano de pagamento que o operador montou ao importar a\n-- DANFE: 895 linhas (657 no Pertinho do Céu, 238 no Espaço Facial),\n-- R$ 1.293.490,86, e nenhuma delas atravessava. É a última das quatro tabelas\n-- da entrada de notas; as outras três vão para o inventário\n-- (plugin-inventory/010). Esta mora aqui porque o destino dela é uma tabela do\n-- FINANCEIRO, e o financeiro roda ANTES do inventário na corrente\n-- (packages/db/chain.json) — um arquivo do inventário que criasse a parcela\n-- quebraria a aplicação do zero.\n--\n-- ── O título não existe no V1, e a parcela não vive sem ele ────────────────\n--\n-- \\`plg_financial_invoice_installments.invoice_id\\` é NOT NULL, e no V1 não há\n-- nada para resolver: \\`danfe_payments\\` aponta para \\`danfe_imports\\`, e nenhuma\n-- linha de \\`invoices\\` referencia uma nota de entrada — as três FKs que chegam\n-- em \\`danfe_imports\\` são \\`danfe_import_items\\`, \\`danfe_payments\\` e \\`dfe_inbox\\`.\n-- No V1 a conta a pagar do fornecedor simplesmente não é um título: é uma lista\n-- de vencimentos pendurada na tela de importação.\n--\n-- Então o título nasce aqui, um por nota, determinístico:\n--\n-- id md5('danfe-invoice:' || uuid da nota)::uuid\n-- direction payable\n-- idempotency_key danfe:<chave de acesso>\n--\n-- É o mesmo padrão de id derivado que o porte já usa para o anexo (spine 054).\n-- Determinístico importa duas vezes: a segunda parcela da mesma nota encontra o\n-- título que a primeira criou, e a segunda ONDA encontra o mesmo id.\n--\n-- ── O total do título é a soma das PARCELAS, não o total da nota ───────────\n--\n-- Medido em 21/09/2026: em 38 das 607 notas (6,3%) a soma das parcelas não bate\n-- com \\`danfe_imports.total_value\\` — R$ 926.668,06 de notas contra R$ 850.454,65\n-- de parcelas no tenant 8. Um título cujo total não é a soma das parcelas nasce\n-- com saldo em aberto errado em toda tela que faz \"total − pago\", e o total da\n-- nota continua legível em \\`metadata.danfe.total_value\\` e na caixa de entrada\n-- fiscal. A soma vem do extrator (\\`note_payments_total\\`), já pronta na primeira\n-- parcela, porque \\`plg_financial_invoices\\` é IMUTÁVEL depois de emitido: o\n-- gatilho recusa alterar \\`total\\`, e acumular parcela a parcela não é opção.\n--\n-- ── \\`number\\` do título fica nulo de propósito ──────────────────────────────\n--\n-- \\`plg_financial_invoices_number_uq\\` é UNIQUE (tenant_id, number). O número da\n-- NF-e do fornecedor não é único dentro do tenant — dois fornecedores emitem a\n-- nota 1234 no mesmo ano, e ele ainda colidiria com o número das vendas. O\n-- número vai para \\`description\\` e para \\`metadata\\`, e a identidade única é a\n-- chave de acesso, em \\`idempotency_key\\`.\n--\n-- ── \\`number\\` da PARCELA é a ordem dentro da nota ───────────────────────────\n--\n-- \\`(invoice_id, number)\\` é UNIQUE, e 251 das 895 parcelas não têm\n-- \\`installment_number\\` no V1 (o pagamento à vista — pix, dinheiro — não numera).\n-- Usar \\`coalesce(installment_number, 1)\\` colidiria na nota que tem pix E\n-- boleto 1/2. O extrator traz a ordem pelo id da própria origem\n-- (\\`payment_rank\\`), que é única por construção, e o \\`installment_number\\` do V1\n-- fica em metadata.\n--\n-- E 58 das 895 não têm vencimento: o pagamento à vista vence no dia da nota, e\n-- é a data de emissão que entra — \\`due_date\\` é NOT NULL no destino.\n--\n-- Idempotente.\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 1. O writer\n-- ─────────────────────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_danfe_installment(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_danfe jsonb := coalesce(v_meta -> 'danfe', '{}'::jsonb);\n v_note uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_invoice uuid;\n v_issue date;\n v_due date;\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_number integer := coalesce((p_row ->> 'number')::integer, 1);\n v_total numeric;\n v_desc text;\n v_existing uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma parcela precisa de id');\n END IF;\n\n BEGIN v_note := (v_danfe ->> 'import_uuid')::uuid; EXCEPTION WHEN OTHERS THEN v_note := NULL; END;\n IF v_note IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'parcela de nota de entrada sem a nota — o título dela não tem de que nascer');\n END IF;\n\n -- A emissão da nota é timestamptz na origem e date no destino. Data que não\n -- converte vira nulo aqui e o vencimento cai para hoje, em vez de derrubar a\n -- parcela inteira: o valor e a nota são o dado, a data é o enfeite.\n BEGIN v_issue := (v_danfe ->> 'issue_date')::timestamptz::date; EXCEPTION WHEN OTHERS THEN v_issue := NULL; END;\n BEGIN v_due := (p_row ->> 'due_date')::date; EXCEPTION WHEN OTHERS THEN v_due := NULL; END;\n v_due := coalesce(v_due, v_issue, current_date);\n\n IF v_amount < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('parcela de valor negativo (%s) — o destino exige amount >= 0', v_amount));\n END IF;\n\n -- O TÍTULO. \\`total\\` é a soma das parcelas da nota (vem do extrator); sem ela,\n -- o valor desta parcela, que é o mínimo verdadeiro.\n v_total := round(coalesce((v_danfe ->> 'payments_total')::numeric, v_amount), 2);\n v_desc := btrim('NF-e ' || coalesce(v_danfe ->> 'nfe_number', 's/n')\n || coalesce('/' || (v_danfe ->> 'nfe_series'), '')\n || coalesce(' — ' || (v_danfe ->> 'supplier_name'), ''));\n v_invoice := md5('danfe-invoice:' || v_note::text)::uuid;\n\n INSERT INTO public.plg_financial_invoices\n (id, tenant_id, unit_id, direction, number, competence_date, issue_date, total,\n description, idempotency_key, metadata)\n VALUES (v_invoice, p_tenant, v_unit, 'payable', NULL,\n coalesce(v_issue, v_due), coalesce(v_issue, v_due), v_total,\n nullif(v_desc, ''),\n 'danfe:' || coalesce(v_danfe ->> 'access_key', v_note::text),\n jsonb_build_object('danfe', v_danfe - 'payment'))\n ON CONFLICT (id) DO NOTHING;\n\n SELECT tenant_id INTO v_existing FROM public.plg_financial_invoice_installments WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('a parcela %s já existe em outro tenant', p_id));\n END IF;\n v_created := v_existing IS NULL;\n\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, metadata, created_at)\n VALUES (p_id, p_tenant, v_unit, v_invoice, v_number, v_due, v_amount, v_meta,\n coalesce((p_row ->> 'created_at')::timestamptz, now()))\n ON CONFLICT (id) DO UPDATE SET\n unit_id = coalesce(EXCLUDED.unit_id, public.plg_financial_invoice_installments.unit_id),\n invoice_id = EXCLUDED.invoice_id,\n number = EXCLUDED.number,\n due_date = EXCLUDED.due_date,\n amount = EXCLUDED.amount,\n metadata = migration._merge_meta(public.plg_financial_invoice_installments.metadata, EXCLUDED.metadata),\n updated_at = now();\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.plg_financial_invoice_installments', p_id,\n jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END,\n 'invoice_id', v_invoice));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\nCOMMENT ON FUNCTION migration.upsert_fin_danfe_installment(uuid, uuid, jsonb, jsonb) IS\n 'A parcela do plano de pagamento de uma DANFE importada. Cria, uma vez por nota '\n 'e com id derivado da nota, o título a pagar que o V1 não tem — sem ele a '\n 'parcela não existe, porque invoice_id é NOT NULL.';\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 2. O vocabulário de writers aceita o nome novo\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- Lendo e costurando o CHECK que está lá: reescrevê-lo com lista literal\n-- apagaria as palavras que os outros passos da corrente acrescentaram.\n\nDO $$\nDECLARE v_def text;\nBEGIN\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n IF v_def IS NULL OR strpos(v_def, 'fin_danfe_installment') > 0 THEN RETURN; END IF;\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''person''::text', '''person''::text, ''fin_danfe_installment''::text');\nEND $$;\n\n-- ─────────────────────────────────────────────────────────────────────────────\n-- 3. O de-para\n-- ─────────────────────────────────────────────────────────────────────────────\n--\n-- Tudo que o título precisa chega pelo augment do extrator, a partir da própria\n-- nota. É de propósito: este writer é do financeiro, e o financeiro roda antes\n-- do inventário — ler \\`plg_inventory_operations\\` daqui seria depender de uma\n-- tabela que ainda não existe quando este arquivo é aplicado.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'danfe_payments', 'public.plg_financial_invoice_installments', 'financial',\n 'fin_danfe_installment', '{}'::jsonb, ARRAY['amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'note_unit_id', 'unit_id@companies',\n 'payment_rank', 'number',\n 'due_date', 'due_date',\n 'value', 'amount',\n 'created_at', 'created_at',\n 'tenant_id', '-',\n -- O que o writer lê para montar o título. Vem em metadata e não em coluna:\n -- é a nota, não a parcela, e duplicá-la em coluna daria duas respostas para\n -- \"de que nota é este vencimento\".\n 'note_uuid', 'jsonb:danfe.import_uuid',\n 'note_access_key', 'jsonb:danfe.access_key',\n 'note_number', 'jsonb:danfe.nfe_number',\n 'note_series', 'jsonb:danfe.nfe_series',\n 'note_issue_date', 'jsonb:danfe.issue_date',\n 'note_supplier', 'jsonb:danfe.supplier_name',\n 'note_total', 'jsonb:danfe.total_value',\n 'note_payments_total', 'jsonb:danfe.payments_total',\n 'import_id', 'jsonb:danfe.v1_import_ref',\n 'payment_type', 'jsonb:danfe.payment.type',\n 'payment_type_code', 'jsonb:danfe.payment.type_code',\n 'installment_number', 'jsonb:danfe.payment.v1_installment_number',\n 'installment_ref', 'jsonb:danfe.payment.v1_installment_ref',\n 'barcode', 'jsonb:danfe.payment.barcode',\n 'pix_key', 'jsonb:danfe.payment.pix_key',\n 'raw_json', 'jsonb:danfe.payment.raw',\n 'id', 'jsonb:danfe.payment.v1_ref'),\n 'O plano de pagamento da nota de entrada: 895 parcelas, R$ 1.293.490,86, que '\n 'hoje não chegam a lugar nenhum. O título a pagar nasce com a primeira '\n 'parcela, um por nota, com id derivado do uuid dela — no V1 ele não existe, e '\n 'invoice_id é NOT NULL. O boleto e a chave pix ficam em metadata: cobrança de '\n 'fornecedor é evidência, não mais uma coluna do título.'\n);\n\nDELETE FROM migration.excluded_tables WHERE source_table = 'danfe_payments';\n\nRESET migration.allowlist_unlock;\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\n--\n-- Função nova nasce executável por PUBLIC, e \\`anon\\`/\\`authenticated\\` herdam\n-- dali. Esta é de PORTE: SECURITY DEFINER, escreve dados de qualquer inquilino,\n-- e alcançável pela chave anônima seria a base inteira alcançável pela chave\n-- anônima. Só o service_role, que é quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_danfe_installment(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_danfe_installment(uuid, uuid, jsonb, jsonb) TO service_role;\n`\n\nexport const MIGRATION_085_O_RESUMO_LE_CINCO_MIL_LINHAS_E_NAO_DUZENTAS_E_OITENTA_MIL = `-- O resumo lê cinco mil linhas, não duzentas e oitenta mil.\n--\n-- \\`financial_summary\\` (migração 077) já soma no banco em vez de baixar a tabela\n-- para o navegador, e ainda assim o painel devolvia 500: \"canceling statement\n-- due to statement timeout\". Somar no lugar certo não basta se para somar for\n-- preciso LER tudo.\n--\n-- O que a função pede, e o que a tabela obrigava:\n--\n-- · aberto → \\`status IN ('pending','partial','overdue')\\`: 5.100 linhas de\n-- 281.174. Nenhum índice começa por \\`status\\`, então era varredura\n-- sequencial em 287 MB.\n-- · caixa → \\`movement_kind = 'payment'\\` numa janela de 30 dias: 870 linhas.\n-- O índice \\`(tenant_id, payment_date)\\` existente é parcial e vale\n-- só para conciliação bancária (\\`external_source IS NOT NULL\\`),\n-- então este caminho também varria tudo.\n-- · a view \\`v_financial_cash_movements\\` faz LEFT JOIN da tabela COM ELA MESMA\n-- em \\`reverses_id\\`, e não havia índice nessa coluna — a segunda\n-- varredura dos mesmos 287 MB, por chamada.\n--\n-- E a cerca de RLS cobra por linha: \\`app.has_unit(unit_id)\\` e\n-- \\`app.has_permission('financial.read', unit_id)\\` dependem da LINHA, então não\n-- saem do laço. Com 281 mil linhas a pergunta é feita 281 mil vezes; com 5 mil,\n-- cinco mil. O índice não muda a política, muda quantas vezes ela é consultada\n-- — é a mesma correção que levou /clients de 10s para 15ms.\n--\n-- \\`INCLUDE\\` carrega as colunas que a soma usa (valor, pago, e as duas que a RLS\n-- precisa ler) para a varredura terminar no índice e não voltar à heap 5.100\n-- vezes.\n--\n-- Só índices: nenhuma política, coluna ou dado muda. Idempotente.\n\n-- Títulos em aberto — o \"a receber\", o \"a pagar\" e os dois vencidos.\nCREATE INDEX IF NOT EXISTS plg_financial_movements_open_idx\n ON public.plg_financial_movements (tenant_id, direction, due_date)\n INCLUDE (amount, paid_amount, status, unit_id, owner_id)\n WHERE status IN ('pending', 'partial', 'overdue');\n\n-- Eventos de dinheiro na janela — a entrada e a saída do período.\nCREATE INDEX IF NOT EXISTS plg_financial_movements_payment_idx\n ON public.plg_financial_movements (tenant_id, payment_date, direction)\n INCLUDE (paid_amount, unit_id, owner_id)\n WHERE movement_kind = 'payment';\n\n-- A auto-junção da view de caixa. Parcial porque estorno é exceção: o índice\n-- fica pequeno e o planejador o usa para o lado que de fato tem valor.\nCREATE INDEX IF NOT EXISTS plg_financial_movements_reverses_idx\n ON public.plg_financial_movements (reverses_id)\n WHERE reverses_id IS NOT NULL;\n\n-- O saldo por conta. \\`v_financial_account_balances\\` soma, POR CONTA, os\n-- movimentos que entram e os que saem dela. Os índices existentes são de uma\n-- coluna só (\\`debit_account_id\\`, \\`credit_account_id\\`), então o planejador\n-- cruzava um deles com o índice de \\`tenant_id\\` — 46.862 linhas de bitmap por\n-- conta, vezes o número de contas. Compostos e com as colunas da soma dentro,\n-- cada conta é uma varredura curta do próprio índice.\nCREATE INDEX IF NOT EXISTS plg_financial_movements_debit_balance_idx\n ON public.plg_financial_movements (tenant_id, debit_account_id)\n INCLUDE (paid_amount, amount, unit_id, owner_id, reverses_id)\n WHERE debit_account_id IS NOT NULL\n AND status <> 'cancelled'\n AND movement_kind IN ('payment', 'transfer');\n\nCREATE INDEX IF NOT EXISTS plg_financial_movements_credit_balance_idx\n ON public.plg_financial_movements (tenant_id, credit_account_id)\n INCLUDE (paid_amount, amount, unit_id, owner_id, reverses_id)\n WHERE credit_account_id IS NOT NULL\n AND status <> 'cancelled'\n AND movement_kind IN ('payment', 'transfer');\n`\n\nexport const MIGRATION_086_A_CERCA_SE_PERGUNTA_UMA_VEZ_E_NAO_UMA_VEZ_POR_LINHA = `-- A cerca se pergunta uma vez, não uma vez por linha.\n--\n-- \\`financial_summary\\` (077) já soma no banco, e os índices (085) já a fazem\n-- ler 5.100 linhas em vez de 281.174. Continuava dando 57014 — \"canceling\n-- statement due to statement timeout\" — e o custo que sobrou não é varredura,\n-- é a POLÍTICA.\n--\n-- A política de SELECT de \\`plg_financial_movements\\` faz, POR LINHA:\n--\n-- (unit_id IS NULL OR app.has_unit(unit_id))\n-- AND CASE unit_id … app.has_permission('financial.read', unit_id) END\n-- AND (… app.has_permission('financial.read_all', unit_id) …)\n--\n-- As três dependem da LINHA, então não saem do laço (as que não dependem já\n-- estão içadas em \\`(SELECT …)\\`). \\`has_unit\\` e \\`has_permission\\` são memoizadas\n-- num GUC, mas memoizado não é grátis: cada chamada monta a chave e faz o\n-- parse de um JSONB que cresce. Vezes três, vezes o número de linhas — e o\n-- saldo por conta lê os movimentos de cada conta, então ali são milhões de\n-- avaliações.\n--\n-- A mesma pergunta, feita como CONJUNTO, custa uma vez:\n--\n-- app.visible_units() -- exatamente o que has_unit aceita\n-- × app.has_permission(perm, u) por UNIDADE -- meia dúzia, não 281 mil\n--\n-- Daí em diante o filtro da linha é \\`unit_id = ANY(array)\\`, que é comparação,\n-- não chamada de função. É a correção que levou /clients de 10s para 15ms.\n--\n-- ── O que NÃO muda ─────────────────────────────────────────────────────────\n--\n-- A função passa a \\`SECURITY DEFINER\\`, então a cerca é responsabilidade DELA, e\n-- está escrita abaixo como tradução literal das duas políticas (movimento e\n-- conta) — mesma ordem, mesmos recursos, mesmas permissões:\n--\n-- · tenant → \\`tenant_id = app.current_tenant_id()\\`\n-- · unidade → \\`unit_id IS NULL\\` com \\`financial.read\\` sem unidade, ou\n-- unidade dentro de \\`visible_units()\\` COM \\`financial.read\\` nela\n-- · dono → só quando \\`app.owner_scoped(<recurso>)\\`; aí valem, na ordem da\n-- política, \\`owner_id IS NULL\\`, \\`owner_id = auth.uid()\\`, o\n-- compartilhamento (\\`has_grant\\`, por linha — é o único que não\n-- tem forma de conjunto, e só é avaliado quando o escopo por\n-- dono está ligado, que é a exceção) e \\`financial.read_all\\`.\n--\n-- O estorno (\\`v_financial_cash_movements\\`) é reescrito aqui como uma coluna\n-- calculada, em vez de LEFT JOIN da tabela com ela mesma: mesma regra, sem a\n-- segunda varredura dos mesmos 287 MB por chamada.\n--\n-- Idempotente. Nenhuma política, coluna ou dado muda.\n\nCREATE OR REPLACE FUNCTION public.financial_summary(\n p_from date DEFAULT NULL,\n p_to date DEFAULT NULL\n) RETURNS TABLE (\n total_balance numeric,\n total_receivable numeric,\n total_payable numeric,\n monthly_inflow numeric,\n monthly_outflow numeric,\n overdue_receivable_count integer,\n overdue_receivable_amount numeric,\n overdue_payable_count integer,\n overdue_payable_amount numeric\n)\nLANGUAGE sql\nSTABLE\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\n WITH ctx AS (\n SELECT app.current_tenant_id() AS tenant,\n auth.uid() AS me,\n app.visible_units() AS units,\n app.has_permission('financial.read', NULL::uuid) AS read_null,\n app.has_permission('financial.read_all', NULL::uuid) AS read_all_null,\n app.owner_scoped('financial.movement') AS own_mov,\n app.owner_scoped('financial.account') AS own_acc,\n app.grants_exist('financial.movement', ARRAY['shared_with']) AS grants_mov,\n app.grants_exist('financial.account', ARRAY['shared_with']) AS grants_acc,\n coalesce(p_from, (current_date - 30)) AS de,\n coalesce(p_to, current_date) AS ate\n ),\n -- Uma pergunta por unidade alcançável, não uma por linha.\n u_read AS (\n SELECT u FROM ctx, unnest(ctx.units) u\n WHERE app.has_permission('financial.read', u)\n ),\n u_read_all AS (\n SELECT u FROM ctx, unnest(ctx.units) u\n WHERE app.has_permission('financial.read_all', u)\n ),\n -- Movimentos que esta pessoa veria na lista: o WHERE abaixo é a política de\n -- SELECT de plg_financial_movements, termo por termo.\n -- NOT MATERIALIZED de propósito: a partir do PG12 uma CTE citada mais de uma\n -- vez é materializada, e materializar a cerca inteira é justamente juntar as\n -- 281 mil linhas numa tabela temporária. Inlinada, cada uso leva o próprio\n -- índice (aberto, pagamento, saldo por conta).\n --\n -- \\`caixa_visivel\\` é a regra de estorno de v_financial_cash_movements dita\n -- sem a auto-junção: quando o movimento estorna outro, quem decide é o\n -- estornado; se não estorna ninguém, decide ele mesmo. \\`reverses_id IS NULL\\`\n -- é a esmagadora maioria, então a subconsulta quase nunca roda — e quando\n -- roda, é pela chave primária.\n mov AS NOT MATERIALIZED (\n SELECT m.*,\n CASE\n WHEN m.reverses_id IS NULL THEN\n (m.external_source IS NULL\n OR (m.reconciled_at IS NOT NULL AND m.matched_movement_id IS NULL))\n ELSE coalesce(\n (SELECT r.external_source IS NULL\n OR (r.reconciled_at IS NOT NULL AND r.matched_movement_id IS NULL)\n FROM public.plg_financial_movements r\n WHERE r.id = m.reverses_id),\n (m.external_source IS NULL\n OR (m.reconciled_at IS NOT NULL AND m.matched_movement_id IS NULL)))\n END AS caixa_visivel\n FROM public.plg_financial_movements m, ctx\n WHERE m.tenant_id = ctx.tenant\n AND (CASE WHEN m.unit_id IS NULL THEN ctx.read_null\n ELSE m.unit_id IN (SELECT u FROM u_read) END)\n AND (NOT ctx.own_mov\n OR m.owner_id IS NULL\n OR m.owner_id = ctx.me\n OR (ctx.grants_mov\n AND app.has_grant('financial.movement', m.id, ARRAY['shared_with']))\n OR (CASE WHEN m.unit_id IS NULL THEN ctx.read_all_null\n ELSE m.unit_id IN (SELECT u FROM u_read_all) END))\n ),\n -- O mesmo, para as contas.\n acc AS (\n SELECT a.id, a.kind\n FROM public.plg_financial_accounts a, ctx\n WHERE a.tenant_id = ctx.tenant\n AND a.is_active\n AND a.kind IN ('bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit')\n AND (CASE WHEN a.unit_id IS NULL THEN ctx.read_null\n ELSE a.unit_id IN (SELECT u FROM u_read) END)\n AND (NOT ctx.own_acc\n OR a.owner_id IS NULL\n OR a.owner_id = ctx.me\n OR (ctx.grants_acc\n AND app.has_grant('financial.account', a.id, ARRAY['shared_with']))\n OR (CASE WHEN a.unit_id IS NULL THEN ctx.read_all_null\n ELSE a.unit_id IN (SELECT u FROM u_read_all) END))\n ),\n -- O saldo de uma conta é o que entrou menos o que saiu dela, e só conta\n -- pagamento e transferência — a obrigação não é dinheiro. Para os cinco\n -- \\`kind\\` de caixa que o cartão soma, v_financial_account_balances calcula\n -- \\`debit_total - credit_total\\`; é esse sinal que está aqui.\n saldo AS (\n SELECT coalesce(sum(\n coalesce((SELECT sum(d.amount) FROM mov d\n WHERE d.debit_account_id = acc.id\n AND d.status <> 'cancelled'\n AND d.movement_kind IN ('payment', 'transfer')\n AND d.caixa_visivel), 0)\n - coalesce((SELECT sum(c.amount) FROM mov c\n WHERE c.credit_account_id = acc.id\n AND c.status <> 'cancelled'\n AND c.movement_kind IN ('payment', 'transfer')\n AND c.caixa_visivel), 0)\n ), 0) AS v\n FROM acc\n ),\n aberto AS (\n SELECT\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'), 0) AS receber,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'), 0) AS pagar,\n count(*) FILTER (WHERE m.direction = 'credit'\n AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_receber_n,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'\n AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_receber_v,\n count(*) FILTER (WHERE m.direction = 'debit'\n AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_pagar_n,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'\n AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_pagar_v\n FROM mov m\n WHERE m.status IN ('pending', 'partial', 'overdue')\n ),\n caixa AS (\n SELECT\n coalesce(sum(m.paid_amount) FILTER (WHERE m.direction = 'credit'), 0) AS entrada,\n coalesce(sum(m.paid_amount) FILTER (WHERE m.direction = 'debit'), 0) AS saida\n FROM mov m, ctx\n WHERE m.movement_kind = 'payment'\n AND m.payment_date >= ctx.de AND m.payment_date <= ctx.ate\n AND m.caixa_visivel\n )\n SELECT saldo.v, aberto.receber, aberto.pagar, caixa.entrada, caixa.saida,\n aberto.venc_receber_n::integer, aberto.venc_receber_v,\n aberto.venc_pagar_n::integer, aberto.venc_pagar_v\n FROM saldo, aberto, caixa;\n$function$;\n\nCOMMENT ON FUNCTION public.financial_summary(date, date) IS\n 'Os nove numeros do cartao de resumo, somados no banco, com a cerca de '\n 'tenant/unidade/dono perguntada UMA vez em vez de uma vez por linha. '\n 'SECURITY DEFINER: a politica de plg_financial_movements e de '\n 'plg_financial_accounts esta reescrita aqui, termo por termo, em forma de '\n 'conjunto - ver o cabecalho da migracao 086.';\n\nGRANT EXECUTE ON FUNCTION public.financial_summary(date, date) TO authenticated, service_role;\n`\n\nexport const MIGRATION_087_O_SALDO_DE_UMA_FATURA_NAO_SOMA_O_RAZAO_INTEIRO = `-- O saldo de uma fatura não soma o razão inteiro.\n--\n-- \\`v_financial_installment_balances\\` abre com uma CTE que agrega TUDO:\n--\n-- WITH paid AS (\n-- SELECT a.installment_id, sum(a.amount)\n-- FROM plg_financial_allocations a\n-- JOIN plg_financial_movements m ON m.id = a.payment_movement_id\n-- WHERE m.status <> 'cancelled' AND m.reversed_by_id IS NULL\n-- GROUP BY a.installment_id)\n--\n-- Nenhum predicado do chamador alcança essa CTE: ela agrupa por\n-- \\`installment_id\\`, e o \\`LEFT JOIN paid p ON p.installment_id = i.id\\` só\n-- acontece DEPOIS. O planejador não tem como empurrar \\`invoice_id = X\\` para\n-- dentro, então materializa o razão inteiro — toda vez, para qualquer pergunta.\n--\n-- Medido no cluster, pedindo UMA fatura:\n--\n-- Index Scan ... plg_financial_invoice_installments 1 linha 0,077 ms\n-- Seq Scan plg_financial_movements 281.113 linhas\n-- Seq Scan plg_financial_allocations 140.073 linhas\n-- HashAggregate 9 lotes, 7.288 kB em disco\n-- ─────────────────────────────────────────────────────────────\n-- CTE paid 2.251 ms\n-- total 2.627 ms\n--\n-- 2.627 ms como superusuário. Pelo PostgREST, com RLS por cima, passa dos 8 s\n-- e a tela recebe 500 / 57014. A parcela que a pessoa pediu custou 0,077 ms; o\n-- resto é o razão de dois tenants sendo somado para ser jogado fora.\n--\n-- ── O conserto ─────────────────────────────────────────────────────────────\n--\n-- LATERAL correlacionado em \\`i.id\\`. A soma passa a acontecer POR PARCELA, e\n-- \\`plg_financial_allocations_installment_idx\\` — que já existe — a resolve com\n-- um index scan de poucas linhas. Uma fatura com 8 parcelas faz 8 buscas\n-- pequenas em vez de uma agregação de 140 mil linhas.\n--\n-- É o mesmo movimento da 085 e da 086: a pergunta é de UMA linha, e a resposta\n-- não pode ter custo de tabela inteira.\n--\n-- ── O que NÃO muda ─────────────────────────────────────────────────────────\n--\n-- As colunas, na mesma ordem e com os mesmos tipos — \\`CREATE OR REPLACE VIEW\\`\n-- exige isso, e quem lê a vista não percebe diferença nenhuma além do tempo.\n-- \\`security_invoker\\` continua ligado: a vista é uma lente, a RLS é das tabelas.\n--\n-- Idempotente.\n\nCREATE OR REPLACE VIEW public.v_financial_installment_balances\nWITH (security_invoker = true) AS\nSELECT i.id AS installment_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n t.number AS invoice_number,\n t.direction,\n t.counterparty_person_id,\n t.order_id,\n i.number,\n i.due_date,\n i.amount,\n COALESCE(p.paid, 0::numeric)::numeric(14,2) AS paid,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(i.amount - COALESCE(p.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'::text\n WHEN COALESCE(p.paid, 0::numeric) >= i.amount THEN 'paid'::text\n WHEN COALESCE(p.paid, 0::numeric) > 0::numeric THEN 'partial'::text\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'::text\n ELSE 'pending'::text\n END AS status,\n i.cancelled_at,\n i.metadata,\n i.created_at,\n i.updated_at\n FROM plg_financial_invoice_installments i\n JOIN plg_financial_invoices t ON t.id = i.invoice_id\n -- A soma é POR PARCELA. \\`ON true\\` mantém o LEFT: parcela sem pagamento\n -- continua aparecendo, com \\`paid\\` nulo, que o COALESCE zera.\n LEFT JOIN LATERAL (\n SELECT sum(a.amount) AS paid\n FROM plg_financial_allocations a\n JOIN plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = i.id\n AND m.status <> 'cancelled'::text\n AND m.reversed_by_id IS NULL\n ) p ON true;\n\n-- O movimento é olhado uma vez por alocação, pelo id — e o filtro de vivo\n-- (\\`status\\`, \\`reversed_by_id\\`) cabe no índice, então a visita à heap só\n-- acontece para o que de fato soma.\nCREATE INDEX IF NOT EXISTS plg_financial_movements_alive_idx\n ON public.plg_financial_movements (id)\n WHERE status <> 'cancelled'::text AND reversed_by_id IS NULL;\n`\n\nexport const MIGRATION_088_A_CONTA_SEM_PESSOA_E_DO_SISTEMA_NAO_DE_NINGUEM = `-- A conta sem pessoa é do sistema, não de ninguém.\n--\n-- \\`migration.upsert_fin_account\\` inventa um valor que a tabela não aceita:\n--\n-- IF v_kind = 'person' AND coalesce(v_ref_id, 0) = 0 THEN\n-- v_kind := 'unidentified';\n--\n-- Uma conta de PESSOA sem pessoa referenciada vira \\`'unidentified'\\`. Só que\n-- \\`plg_financial_accounts_kind_check\\` declara dez espécies e essa não é uma\n-- delas — a migration 063 recria o CHECK e ele estoura com \\`23514\\` em cima das\n-- linhas que o porte gravou. O efeito é pior do que parece: 063 é a 64ª de 88\n-- na cadeia do financeiro, então DUAS linhas travam a aplicação de todo o resto\n-- do plugin, migrations de desempenho inclusive.\n--\n-- ── Quem são as duas ───────────────────────────────────────────────────────\n--\n-- Pertinho do Céu, vindas de \\`account_central\\`:\n--\n-- 653309 Cliente não identificado\n-- 654242 Múltiplos participantes\n--\n-- 94 movimentos creditam nelas. Não são erro de dado: são o balcão anônimo e a\n-- conta dividida — dois baldes que a CASA mantém. Nenhuma das duas é pessoa, e\n-- nenhuma é banco.\n--\n-- ── Por que \\`system\\` e não mais um valor no CHECK ──────────────────────────\n--\n-- \\`system\\` já existe no vocabulário e significa exatamente isto: conta que o\n-- estabelecimento mantém e que não é de ninguém. Acrescentar \\`unidentified\\`\n-- seria criar um décimo primeiro nome para um conceito que já tem o seu, e\n-- alargar um CHECK que outro dono fechou de propósito.\n--\n-- O nome continua legível — \"Cliente não identificado\" está em \\`name\\`, que é o\n-- que a tela mostra. O que muda é a ESPÉCIE, que é o que o razão classifica,\n-- mais um \\`code\\` estável, que é como conta de sistema se procura.\n--\n-- ── Ordem ──────────────────────────────────────────────────────────────────\n--\n-- Numa cadeia do zero isto nunca acontece: sem porte não há linha, e a 063\n-- passa. O conserto aqui é para o cluster onde o porte já rodou — normaliza o\n-- que existe e, sobretudo, corrige o writer para não produzir mais.\n--\n-- Idempotente.\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 1. O que já está gravado\n-- ═══════════════════════════════════════════════════════════════════════════\n\n-- \\`plg_financial_accounts_check1\\` cobra \\`code\\` de toda conta de sistema, e\n-- cobra com razão: conta de sistema se endereça por código estável, não por\n-- nome — o nome é o que a tela mostra, o código é o que o razão procura.\n-- Os dois baldes ganham código no estilo dos que já existem (\\`legacy\\`,\n-- \\`payables\\`, \\`receivables\\`).\nUPDATE public.plg_financial_accounts\n SET kind = 'system',\n code = coalesce(code, CASE\n WHEN name ILIKE '%%ltiplos%%' OR name ILIKE '%%multiple%%' THEN 'multiple_parties'\n ELSE 'unidentified_party'\n END),\n metadata = jsonb_set(coalesce(metadata, '{}'::jsonb), '{v1,account_kind}', '\"unidentified\"'::jsonb, true)\n WHERE kind = 'unidentified';\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 2. O writer para de produzir\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Duas trocas no corpo, nada mais: a atribuição e a lista de aceitos. Reescrever\n-- a função inteira aqui duplicaria 60 linhas que a 061 já define — e a cópia\n-- envelheceria na próxima vez que alguém mexesse lá.\n\nDO $$\nDECLARE\n v_src text;\n v_new text;\nBEGIN\n SELECT pg_get_functiondef(p.oid) INTO v_src\n FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'migration' AND p.proname = 'upsert_fin_account'\n LIMIT 1;\n\n IF v_src IS NULL THEN\n RAISE NOTICE 'migration.upsert_fin_account não existe neste cluster — nada a corrigir';\n RETURN;\n END IF;\n\n IF position('''unidentified''' in v_src) = 0 THEN\n RAISE NOTICE 'migration.upsert_fin_account já não produz unidentified';\n RETURN;\n END IF;\n\n v_new := replace(v_src, 'v_kind := ''unidentified'';', 'v_kind := ''system'';');\n -- A mesma palavra aparece na lista de espécies aceitas pelo writer; lá ela\n -- some, porque \\`system\\` já está na lista.\n v_new := replace(v_new, ',' || chr(10) || ' ''unidentified'')', ')');\n v_new := replace(v_new, ', ''unidentified'')', ')');\n\n IF v_new = v_src THEN\n RAISE EXCEPTION 'upsert_fin_account mudou de forma: o conserto de unidentified não casou. Conferir à mão.';\n END IF;\n\n EXECUTE v_new;\nEND $$;\n\n-- A prova: nenhuma linha fora do vocabulário, e o writer sem a palavra.\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM public.plg_financial_accounts WHERE kind = 'unidentified') THEN\n RAISE EXCEPTION 'ainda há conta com kind=unidentified';\n END IF;\n IF EXISTS (\n SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'migration' AND p.proname = 'upsert_fin_account'\n AND position('''unidentified''' in pg_get_functiondef(p.oid)) > 0\n ) THEN\n RAISE EXCEPTION 'upsert_fin_account ainda produz unidentified';\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_089_O_FORNECEDOR_APONTA_PARA_O_PLANO_MIGRADO = `-- A migrated supplier points to the current chart of accounts.\n--\n-- V1 stored supplier defaults and allowlists with legacy numeric IDs. The\n-- migration deliberately preserved those values in people.metadata because\n-- contacts can arrive before chart accounts. The invoice form cannot compare\n-- those values with the UUID primary keys used by V2, so the policy appeared\n-- configured but never matched an option.\n--\n-- This read-only resolver crosses that boundary through migration.ledger. It\n-- accepts current UUIDs too, keeping the runtime contract useful after the\n-- migration is over. Tenant and permission checks stay inside the definer.\n\nCREATE OR REPLACE FUNCTION public.financial_resolve_chart_account_refs(p_refs text[])\nRETURNS TABLE(source_ref text, chart_account_id uuid)\nLANGUAGE sql\nSTABLE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n WITH wanted AS (\n SELECT DISTINCT btrim(ref) AS ref\n FROM unnest(coalesce(p_refs, ARRAY[]::text[])) AS ref\n WHERE nullif(btrim(ref), '') IS NOT NULL\n ),\n candidates AS (\n SELECT w.ref AS source_ref, c.id AS chart_account_id, 0 AS priority\n FROM wanted w\n JOIN public.plg_financial_chart_of_accounts c\n ON c.tenant_id = app.current_tenant_id()\n AND c.id::text = w.ref\n UNION ALL\n SELECT w.ref AS source_ref, l.target_id AS chart_account_id, 1 AS priority\n FROM wanted w\n JOIN migration.ledger l\n ON l.tenant_id = app.current_tenant_id()\n AND l.source_table = 'chart_of_accounts'\n AND l.target_table = 'public.plg_financial_chart_of_accounts'\n AND (l.source_numeric_id::text = w.ref OR l.source_uuid::text = w.ref)\n JOIN public.plg_financial_chart_of_accounts c\n ON c.tenant_id = l.tenant_id\n AND c.id = l.target_id\n )\n SELECT DISTINCT ON (c.source_ref) c.source_ref, c.chart_account_id\n FROM candidates c\n WHERE auth.role() = 'service_role'\n OR app.has_permission('financial.read', NULL::uuid)\n ORDER BY c.source_ref, c.priority;\n$$;\n\nREVOKE ALL ON FUNCTION public.financial_resolve_chart_account_refs(text[]) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_resolve_chart_account_refs(text[]) TO authenticated, service_role;\n\nCOMMENT ON FUNCTION public.financial_resolve_chart_account_refs(text[]) IS\n 'Resolves current UUIDs and migrated V1 chart-account references for the active tenant.';\n`\n\nexport const MIGRATION_090_O_DRE_RECORTA_ANTES_DE_SOMAR = `-- Filter the income statement before aggregating it.\n--\n-- The historical view aggregates every fiscal year before PostgREST applies\n-- the requested range. On large tenants that exceeds the authenticated role\n-- timeout. This function accepts the range, resolves tenant/unit/permission\n-- once, and only then joins line items. It is additive and retains the view as\n-- the analytics contract for existing consumers.\n\nCREATE OR REPLACE FUNCTION public.financial_dre(\n p_from date,\n p_to date,\n p_unit_id uuid DEFAULT NULL\n) RETURNS TABLE (\n unit_id uuid,\n competence_month date,\n account_id uuid,\n account_code text,\n account_name text,\n account_type text,\n account_parent_id uuid,\n statement_code text,\n statement_name text,\n unclassified boolean,\n line_count bigint,\n amount numeric,\n discount numeric\n)\nLANGUAGE sql\nSTABLE\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\n WITH ctx AS MATERIALIZED (\n SELECT app.current_tenant_id() AS tenant,\n app.visible_units() AS units,\n app.has_permission('financial.read', NULL::uuid) AS read_null,\n p_from AS de,\n p_to AS ate,\n p_unit_id AS unidade\n ),\n u_read AS MATERIALIZED (\n SELECT u\n FROM ctx, unnest(ctx.units) AS u\n WHERE app.has_permission('financial.read', u)\n ),\n inv AS MATERIALIZED (\n SELECT f.*\n FROM public.plg_financial_invoices f, ctx\n WHERE f.tenant_id = ctx.tenant\n AND f.cancelled_at IS NULL\n AND f.competence_date >= ctx.de\n AND f.competence_date <= ctx.ate\n AND (CASE WHEN ctx.unidade IS NOT NULL THEN f.unit_id = ctx.unidade\n WHEN f.unit_id IS NULL THEN ctx.read_null\n ELSE f.unit_id IN (SELECT u FROM u_read) END)\n ),\n line AS (\n SELECT f.tenant_id, f.unit_id, f.competence_date, f.direction,\n i.id AS item_id, i.total, i.discount,\n coalesce(x.chart_account_id, i.chart_account_id) AS chart_account_id\n FROM inv f\n JOIN public.items i ON i.invoice_id = f.id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n WHERE i.cancelled_at IS NULL\n UNION ALL\n SELECT f.tenant_id, f.unit_id, f.competence_date, f.direction,\n i.id AS item_id, i.total, i.discount,\n coalesce(x.chart_account_id, i.chart_account_id) AS chart_account_id\n FROM inv f\n JOIN public.items i ON i.order_id = f.order_id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n WHERE f.order_id IS NOT NULL\n AND i.invoice_id IS NULL\n AND i.cancelled_at IS NULL\n AND NOT EXISTS (SELECT 1 FROM public.items own WHERE own.invoice_id = f.id)\n ),\n structure AS (\n SELECT NULL::uuid AS unit_id, NULL::date AS competence_month,\n c.id AS account_id, c.code AS account_code, c.name AS account_name,\n c.account_type, c.parent_id AS account_parent_id,\n c.code AS statement_code, c.name AS statement_name,\n false AS unclassified, 0::bigint AS line_count,\n 0::numeric AS amount, 0::numeric AS discount\n FROM public.plg_financial_chart_of_accounts c, ctx\n WHERE c.tenant_id = ctx.tenant\n AND c.is_active\n AND c.account_type IN ('revenue', 'expense')\n AND (ctx.read_null OR EXISTS (SELECT 1 FROM u_read))\n ),\n movement AS (\n SELECT l.unit_id,\n date_trunc('month', l.competence_date)::date AS competence_month,\n c.id AS account_id, c.code AS account_code, c.name AS account_name,\n CASE WHEN l.direction = 'receivable' THEN 'revenue' ELSE 'expense' END AS account_type,\n c.parent_id AS account_parent_id,\n CASE WHEN (l.direction = 'receivable' AND c.account_type = 'revenue')\n OR (l.direction = 'payable' AND c.account_type = 'expense')\n THEN c.code END AS statement_code,\n CASE WHEN (l.direction = 'receivable' AND c.account_type = 'revenue')\n OR (l.direction = 'payable' AND c.account_type = 'expense')\n THEN c.name END AS statement_name,\n (c.id IS NULL OR NOT ((l.direction = 'receivable' AND c.account_type = 'revenue')\n OR (l.direction = 'payable' AND c.account_type = 'expense'))) AS unclassified,\n count(*) AS line_count,\n sum(l.total) AS amount,\n sum(l.discount) AS discount\n FROM line l\n LEFT JOIN public.plg_financial_chart_of_accounts c\n ON c.tenant_id = l.tenant_id AND c.id = l.chart_account_id\n GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10\n )\n SELECT * FROM structure\n UNION ALL\n SELECT * FROM movement;\n$function$;\n\nREVOKE ALL ON FUNCTION public.financial_dre(date, date, uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_dre(date, date, uuid) TO authenticated, service_role;\n\nCOMMENT ON FUNCTION public.financial_dre(date, date, uuid) IS\n 'Annual/monthly income statement filtered before aggregation with tenant, unit, and financial.read fences.';\n`\n\nexport const MIGRATION_091_A_RECORRENCIA_REPETE_OS_ITENS = `-- A recurring rule is a complete invoice template. When an occurrence creates\n-- its invoice, copy every template line with its own accounting dimensions.\n\nCREATE OR REPLACE FUNCTION public.plg_financial_copy_recurring_items()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_bill public.plg_financial_recurring_bills%ROWTYPE;\n v_row jsonb;\n v_item_id uuid;\n v_quantity numeric;\n v_unit_price numeric;\n v_discount numeric;\n v_surcharge numeric;\n v_sort integer := 0;\nBEGIN\n IF coalesce(NEW.metadata ->> 'source', '') <> 'financial_recurring_run'\n OR coalesce(NEW.metadata ->> 'recurring_bill_id', '') = '' THEN\n RETURN NEW;\n END IF;\n\n SELECT * INTO v_bill\n FROM public.plg_financial_recurring_bills\n WHERE id = (NEW.metadata ->> 'recurring_bill_id')::uuid\n AND tenant_id = NEW.tenant_id;\n\n IF NOT FOUND OR jsonb_typeof(v_bill.items) <> 'array' THEN\n RETURN NEW;\n END IF;\n\n FOR v_row IN SELECT value FROM jsonb_array_elements(v_bill.items) LOOP\n v_sort := v_sort + 1;\n v_quantity := greatest(coalesce((v_row ->> 'quantity')::numeric, 1), 0.0001);\n v_unit_price := coalesce((v_row ->> 'unitPrice')::numeric, (v_row ->> 'unit_price')::numeric, 0);\n v_discount := coalesce((v_row ->> 'discount')::numeric, 0);\n v_surcharge := coalesce((v_row ->> 'surcharge')::numeric, (v_row ->> 'addition')::numeric, 0);\n\n INSERT INTO public.items\n (tenant_id, unit_id, invoice_id, product_id, name, description, quantity,\n unit_price, discount, total, sort_order, metadata, created_by)\n VALUES\n (NEW.tenant_id, NEW.unit_id, NEW.id,\n CASE WHEN coalesce(v_row ->> 'referenceId', v_row ->> 'reference_id', v_row ->> 'product_id', '')\n ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n THEN coalesce(v_row ->> 'referenceId', v_row ->> 'reference_id', v_row ->> 'product_id')::uuid END,\n nullif(v_row ->> 'description', ''), nullif(v_row ->> 'description', ''),\n v_quantity, v_unit_price, v_discount,\n round(v_quantity * v_unit_price - v_discount + v_surcharge, 2), v_sort,\n jsonb_strip_nulls(jsonb_build_object(\n 'item_kind', coalesce(v_row ->> 'itemKind', v_row ->> 'item_kind', 'other'),\n 'financial', jsonb_build_object('additional', v_surcharge)\n )), NEW.created_by)\n RETURNING id INTO v_item_id;\n\n INSERT INTO public.plg_financial_invoice_items\n (item_id, tenant_id, unit_id, chart_account_id, cost_center_id, owner_id)\n VALUES\n (v_item_id, NEW.tenant_id, NEW.unit_id,\n CASE WHEN coalesce(v_row ->> 'accountId', v_row ->> 'account_id', v_row ->> 'chart_account_id', '')\n ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n THEN coalesce(v_row ->> 'accountId', v_row ->> 'account_id', v_row ->> 'chart_account_id')::uuid END,\n CASE WHEN coalesce(v_row ->> 'costCenterId', v_row ->> 'cost_center_id', '')\n ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n THEN coalesce(v_row ->> 'costCenterId', v_row ->> 'cost_center_id')::uuid END,\n NEW.created_by);\n END LOOP;\n\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS plg_financial_recurring_invoice_items ON public.plg_financial_invoices;\nCREATE TRIGGER plg_financial_recurring_invoice_items\n AFTER INSERT ON public.plg_financial_invoices\n FOR EACH ROW EXECUTE FUNCTION public.plg_financial_copy_recurring_items();\n\nREVOKE ALL ON FUNCTION public.plg_financial_copy_recurring_items() FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_financial_copy_recurring_items() TO service_role;\n\nCOMMENT ON FUNCTION public.plg_financial_copy_recurring_items() IS\n 'Copies every item of a recurring rule into each generated invoice, preserving per-line accounting.';\n`\n\nexport const MIGRATION_092_A_CONTA_PAGA_NO_CARTAO_SAI_DO_CARTAO = `-- A bill paid by credit card is paid from the company's card.\n--\n-- financial_receive_payment settled every credit_card payment in a\n-- card_receivable_credit account — right for a sale (the acquirer owes us),\n-- wrong for a bill: paying a supplier with the corporate card puts the debt on\n-- that card (card_payable). The payables screen could never pay by card.\n-- Receivables keep the exact rule they had. Replaces the function in place;\n-- idempotent.\n\nCREATE OR REPLACE FUNCTION public.financial_receive_payment(p_installment_id uuid, p_amount numeric, p_account_id uuid DEFAULT NULL::uuid, p_method text DEFAULT NULL::text, p_cash_session_id uuid DEFAULT NULL::uuid, p_card_brand text DEFAULT NULL::text, p_card_installments integer DEFAULT NULL::integer, p_nsu text DEFAULT NULL::text, p_payment_date date DEFAULT NULL::date, p_fee_amount numeric DEFAULT NULL::numeric, p_notes text DEFAULT NULL::text, p_idempotency_key text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_actor uuid := auth.uid();\n v_inst_id uuid := p_installment_id;\n v_inst record;\n v_invoice public.plg_financial_invoices%ROWTYPE;\n v_amount numeric := round(coalesce(p_amount, 0), 2);\n v_open numeric;\n v_method text := lower(coalesce(nullif(btrim(p_method), ''), 'other'));\n v_holder uuid;\n v_holder_k text;\n v_session uuid := p_cash_session_id;\n v_counter uuid;\n v_credit numeric;\n v_mov_id uuid;\n v_alloc_id uuid;\n v_dir text;\nBEGIN\n IF v_amount <= 0 THEN\n RAISE EXCEPTION 'financial_receive_payment: amount must be > 0' USING ERRCODE = '22023';\n END IF;\n IF v_method NOT IN ('cash', 'pix', 'credit_card', 'debit_card', 'transfer', 'boleto', 'check', 'credit', 'other') THEN\n RAISE EXCEPTION 'financial_receive_payment: unknown method %', v_method USING ERRCODE = '22023';\n END IF;\n\n -- accept a legacy per-order 'bill' movement id in place of the installment\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_invoice_installments i WHERE i.id = v_inst_id) THEN\n SELECT m.financial_invoice_installment_id INTO v_inst_id FROM public.plg_financial_movements m\n WHERE m.id = p_installment_id AND m.movement_kind = 'bill' AND m.tenant_id = v_tenant;\n END IF;\n SELECT i.* INTO v_inst FROM public.plg_financial_invoice_installments i WHERE i.id = v_inst_id AND i.tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_receive_payment: installment % not found', p_installment_id USING ERRCODE = 'P0002';\n END IF;\n SELECT * INTO v_invoice FROM public.plg_financial_invoices WHERE id = v_inst.invoice_id;\n PERFORM public.plg_financial_require_permission('financial.create', v_inst.unit_id);\n\n -- idempotency\n IF p_idempotency_key IS NOT NULL THEN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT m.id INTO v_mov_id FROM public.plg_financial_movements m WHERE m.tenant_id = v_tenant AND m.idempotency_key = p_idempotency_key;\n IF v_mov_id IS NOT NULL THEN\n SELECT a.id INTO v_alloc_id FROM public.plg_financial_allocations a WHERE a.payment_movement_id = v_mov_id LIMIT 1;\n RETURN jsonb_build_object('movement_id', v_mov_id, 'allocation_id', v_alloc_id, 'installment_id', v_inst.id,\n 'invoice_id', v_inst.invoice_id, 'amount', v_amount, 'replayed', true);\n END IF;\n END IF;\n\n IF v_inst.cancelled_at IS NOT NULL OR v_invoice.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: installment is cancelled' USING ERRCODE = '55000';\n END IF;\n v_open := round(v_inst.amount - public.plg_financial_installment_paid(v_inst.id), 2);\n IF v_amount > v_open + 0.000001 THEN\n RAISE EXCEPTION 'financial_receive_payment: % exceeds the open balance % of installment %/% of %', v_amount, v_open, v_inst.number, v_invoice.number, v_invoice.number\n USING ERRCODE = '23514';\n END IF;\n\n -- the money holder\n IF v_method = 'credit' THEN\n IF v_invoice.counterparty_person_id IS NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: the invoice has no counterparty to consume credit from' USING ERRCODE = '55000';\n END IF;\n v_holder := public.plg_financial_person_account(v_tenant, v_invoice.counterparty_person_id);\n SELECT balance INTO v_credit FROM public.v_financial_account_balances WHERE account_id = v_holder;\n IF coalesce(v_credit, 0) + 0.000001 < v_amount THEN\n RAISE EXCEPTION 'financial_receive_payment: customer credit % is below %', coalesce(v_credit, 0), v_amount USING ERRCODE = '23514';\n END IF;\n ELSIF v_method = 'cash' THEN\n IF v_session IS NULL THEN\n v_session := public.plg_financial_open_session_of(v_tenant, v_actor, v_inst.unit_id);\n END IF;\n IF v_session IS NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: no open cash session (caixinha) for cash in this unit' USING ERRCODE = '55000',\n HINT = 'Open your cash session first (financial_cash_session_open).';\n END IF;\n SELECT s.account_id INTO v_holder FROM public.plg_financial_cash_register_sessions s\n WHERE s.id = v_session AND s.tenant_id = v_tenant AND s.status = 'open';\n IF v_holder IS NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: cash session % is not open', v_session USING ERRCODE = '55000';\n END IF;\n ELSE\n v_holder := p_account_id;\n IF v_holder IS NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: an account is required for method %', v_method USING ERRCODE = '22023';\n END IF;\n END IF;\n SELECT a.kind INTO v_holder_k FROM public.plg_financial_accounts a WHERE a.id = v_holder AND a.tenant_id = v_tenant AND a.is_active;\n IF v_holder_k IS NULL THEN\n RAISE EXCEPTION 'financial_receive_payment: account % not found or inactive', v_holder USING ERRCODE = 'P0002';\n END IF;\n IF v_invoice.direction = 'payable' THEN\n -- Paying a bill by credit card spends the company's own card (a liability\n -- account), never a card receivable. Debit card leaves the bank directly.\n IF v_method = 'credit_card' AND v_holder_k <> 'card_payable' THEN\n RAISE EXCEPTION 'financial_receive_payment: method credit_card must pay a bill from a card_payable account'\n USING ERRCODE = '23514';\n END IF;\n IF v_method <> 'credit_card' AND v_holder_k = 'card_payable' THEN\n RAISE EXCEPTION 'financial_receive_payment: a card_payable account only pays by credit_card' USING ERRCODE = '23514';\n END IF;\n IF v_holder_k IN ('card_receivable_credit', 'card_receivable_debit') THEN\n RAISE EXCEPTION 'financial_receive_payment: a card receivable account cannot pay a bill' USING ERRCODE = '23514';\n END IF;\n ELSE\n IF (v_method = 'credit_card' AND v_holder_k <> 'card_receivable_credit')\n OR (v_method = 'debit_card' AND v_holder_k <> 'card_receivable_debit') THEN\n RAISE EXCEPTION 'financial_receive_payment: method % must settle in a % account', v_method,\n CASE WHEN v_method = 'credit_card' THEN 'card_receivable_credit' ELSE 'card_receivable_debit' END USING ERRCODE = '23514';\n END IF;\n IF v_method NOT IN ('credit_card', 'debit_card') AND v_holder_k IN ('card_receivable_credit', 'card_receivable_debit') THEN\n RAISE EXCEPTION 'financial_receive_payment: a card receivable account only takes card methods' USING ERRCODE = '23514';\n END IF;\n END IF;\n\n IF v_invoice.direction = 'payable' THEN\n v_dir := 'debit';\n v_counter := public.plg_financial_system_account(v_tenant, 'payables');\n ELSE\n v_dir := 'credit';\n v_counter := public.plg_financial_system_account(v_tenant, 'receivables');\n END IF;\n\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, invoice_id, financial_invoice_id, financial_invoice_installment_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, method, card_brand, card_installments, nsu, cash_session_id,\n bank_account_id, fee_amount, notes, metadata, idempotency_key, created_by, debit_account_id, credit_account_id)\n VALUES\n (v_tenant, v_inst.unit_id, v_invoice.order_id, v_invoice.id, v_inst.id, v_dir, 'payment', v_amount, v_amount, 'paid',\n v_inst.due_date, coalesce(p_payment_date, current_date), v_inst.number, v_method, p_card_brand, p_card_installments, p_nsu, v_session,\n CASE WHEN v_holder_k IN ('person', 'system') THEN NULL ELSE v_holder END,\n round(coalesce(p_fee_amount, 0), 2), p_notes,\n coalesce(p_metadata, '{}'::jsonb) || jsonb_build_object('source', 'financial_receive_payment'),\n p_idempotency_key, v_actor,\n CASE WHEN v_dir = 'credit' THEN v_holder ELSE v_counter END,\n CASE WHEN v_dir = 'credit' THEN v_counter ELSE v_holder END)\n RETURNING id INTO v_mov_id;\n\n INSERT INTO public.plg_financial_allocations (tenant_id, unit_id, payment_movement_id, installment_id, amount, created_by)\n VALUES (v_tenant, v_inst.unit_id, v_mov_id, v_inst.id, v_amount, v_actor)\n RETURNING id INTO v_alloc_id;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.payment.receive', 'plg_financial_movements', v_mov_id::text,\n jsonb_build_object('installment_id', v_inst.id, 'invoice_id', v_invoice.id, 'invoice_number', v_invoice.number, 'amount', v_amount,\n 'method', v_method, 'account_id', v_holder, 'cash_session_id', v_session, 'idempotency_key', p_idempotency_key));\n\n RETURN jsonb_build_object('movement_id', v_mov_id, 'allocation_id', v_alloc_id, 'installment_id', v_inst.id,\n 'invoice_id', v_inst.invoice_id, 'amount', v_amount, 'replayed', false);\nEND $$;\n`\n\nexport const MIGRATION_093_O_PAGAMENTO_DIZ_QUAL_CONTA_ELE_QUITOU = `-- O pagamento diz qual conta ele quitou.\n--\n-- Na tela de um título a pagar, a parcela aparece **paga** e, ao abrir, diz\n-- \"Nenhum detalhe adicional registrado\". Medido em 28/09/2026 no Pertinho do\n-- Céu, título \\`17663e91\\` (NF-e 8147427, R$ 12.668,20 em 4 parcelas): as quatro\n-- contas atravessaram, os quatro pagamentos atravessaram, as quatro baixas\n-- atravessaram — e a parcela 2 mostra-se paga sem nenhum pagamento embaixo.\n--\n-- ── Por que ────────────────────────────────────────────────────────────────\n--\n-- \\`InvoiceDetailView\\` pareia pagamento com conta por \\`settles_movement_id\\`\n-- (\\`lib/settlements.ts\\`). Quando ele é nulo, cai na derivação por\n-- \\`installment_number\\` — e o próprio arquivo avisa que essa derivação erra:\n--\n-- Artorius invoice 176440 carries two payments both stamped installment 1\n-- while settling bills #1 and #2.\n--\n-- É exatamente o caso. Os dois pagamentos das parcelas 1 e 2 desse título vêm\n-- do V1 carimbados \\`installment_number = 1\\` (o segundo veio da conciliação\n-- bancária, que não renumera), então os dois caem na parcela 1 e a parcela 2\n-- fica sem nenhum. O dinheiro está certo, a atribuição é que se perde.\n--\n-- E \\`settles_movement_id\\` é nulo em TODOS os movimentos portados:\n--\n-- Pertinho do Céu 2.203 pagamentos 0 com vínculo\n-- Espaço Facial 138.262 pagamentos 0 com vínculo\n--\n-- Não é dado que faltou na origem. São dois furos:\n--\n-- 1. A \\`migration.allowlist\\` mapeia \\`parent_movement_id\\` para uma coluna\n-- \\`parent_movement_id\\` que NÃO EXISTE em \\`plg_financial_movements\\`. O de-para\n-- não erra alto: a chave simplesmente não é lida por writer nenhum, e a linha\n-- entra sem o vínculo. No V1 do Pertinho do Céu 2.978 dos 2.986 pagamentos\n-- têm \\`parent_movement_id\\` — a resposta estava lá o tempo todo.\n--\n-- 2. \\`upsert_fin_movement\\` não escreve a coluna, e \\`upsert_fin_allocation\\`\n-- tampouco — mesmo sendo a baixa quem SABE a resposta. No Espaço Facial isso\n-- é o que importa: lá o V1 preenche \\`parent_movement_id\\` em 854 dos 138.946\n-- pagamentos, mas a baixa existe para 138.104 deles.\n--\n-- ── O que muda ─────────────────────────────────────────────────────────────\n--\n-- O de-para passa a apontar para a coluna que existe; os dois writers passam a\n-- preencher o vínculo quando ele está VAZIO — nunca reescrevendo um já gravado,\n-- que é o que o append-only protege de verdade (mesma leitura da 047: um\n-- vínculo nulo não é um fato, é uma resposta que ainda não chegou). E o que já\n-- está gravado é preenchido aqui, a partir das baixas que já atravessaram.\n--\n-- O gatilho \\`plg_financial_movements_append_only\\` já admite isto: ele lista\n-- nominalmente o que um evento de dinheiro não pode mudar (valor, forma de\n-- pagamento, conta bancária, status) e \\`settles_movement_id\\` não está na lista.\n--\n-- ── O que este arquivo NÃO conserta ────────────────────────────────────────\n--\n-- \\`payment_method_type_id\\`, \\`payment_method_id\\` e \\`bank_account_id\\` continuam\n-- nulos em todo movimento portado, e é outro problema: o writer não escreve\n-- essas três colunas, e o append-only proíbe preenchê-las depois. Conserto no\n-- writer só vale para linha NOVA; o que já entrou fica. Não é o que a tela\n-- estava mostrando aqui, e misturar as duas coisas num arquivo só esconderia a\n-- segunda.\n--\n-- Idempotente.\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 1. O de-para aponta para a coluna que existe\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Só esta chave muda. A linha de \\`financial_movements\\` é \\`frozen\\` e tem 30\n-- colunas mapeadas — reescrevê-la inteira por causa de uma seria trocar o risco\n-- de errar uma pelo risco de errar as outras 29.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nUPDATE migration.allowlist\n SET columns = jsonb_set(\n columns, '{parent_movement_id}',\n '\"settles_movement_id@financial_movements:public.plg_financial_movements\"'::jsonb, true)\n WHERE source_table = 'financial_movements'\n AND columns ->> 'parent_movement_id' IS DISTINCT FROM\n 'settles_movement_id@financial_movements:public.plg_financial_movements';\n\nRESET migration.allowlist_unlock;\n\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 2. O writer do movimento grava o vínculo, e o preenche na segunda oferta\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_movement(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind_raw text := nullif(btrim(lower(coalesce(p_row ->> 'movement_kind', ''))), '');\n v_kind text := CASE v_kind_raw\n WHEN 'bill' THEN 'bill' WHEN 'payment' THEN 'payment'\n WHEN 'transfer' THEN 'transfer' WHEN 'commission' THEN 'commission'\n WHEN 'conta' THEN 'bill' WHEN 'titulo' THEN 'bill' WHEN 'título' THEN 'bill'\n WHEN 'pagamento' THEN 'payment' WHEN 'recebimento' THEN 'payment'\n WHEN 'transferencia' THEN 'transfer' WHEN 'transferência' THEN 'transfer'\n WHEN 'comissao' THEN 'commission' WHEN 'comissão' THEN 'commission'\n END;\n v_status_raw text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text := CASE v_status_raw\n WHEN 'canceled' THEN 'cancelled'\n WHEN 'cancelado' THEN 'cancelled'\n WHEN 'cancelada' THEN 'cancelled'\n WHEN 'pago' THEN 'paid'\n WHEN 'paga' THEN 'paid'\n WHEN 'pendente' THEN 'pending'\n WHEN 'vencido' THEN 'overdue'\n WHEN 'vencida' THEN 'overdue'\n WHEN 'atrasado' THEN 'overdue'\n WHEN 'parcial' THEN 'partial'\n ELSE coalesce(v_status_raw, 'pending')\n END;\n v_dir text := nullif(btrim(lower(coalesce(p_row ->> 'direction', ''))), '');\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_invoice uuid := (p_row ->> 'financial_invoice_id')::uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_due date;\n v_legs record;\n v_debit uuid := (p_row ->> 'debit_account_id')::uuid;\n v_credit uuid := (p_row ->> 'credit_account_id')::uuid;\n -- A conta que ESTE pagamento quitou. Vem de \\`financial_movements.parent_movement_id\\`\n -- do V1, já resolvido pelo ledger. Nulo é o normal e não é erro: o pagamento\n -- avulso não quita conta nenhuma, e no Espaço Facial a coluna quase não é\n -- usada — lá quem responde é a baixa, na parte 3 deste arquivo.\n v_settles uuid := (p_row ->> 'settles_movement_id')::uuid;\n v_exists boolean;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a movement needs an id');\n END IF;\n IF v_kind IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown movement kind %L (expected bill|payment|transfer|commission, or a V1 label of one)', coalesce(v_kind_raw, '<null>')));\n END IF;\n IF v_dir IS NOT NULL AND v_dir NOT IN ('debit', 'credit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', format('unknown direction %L (expected debit|credit)', v_dir));\n END IF;\n IF v_dir IS NULL AND v_invoice IS NOT NULL THEN\n SELECT CASE t.direction WHEN 'receivable' THEN 'credit' WHEN 'payable' THEN 'debit' END INTO v_dir\n FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n IF v_dir IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'the movement carries no direction and settles no invoice — which way the money went cannot be derived, and must not be defaulted');\n END IF;\n IF v_amount < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'a money event cannot carry a negative amount — direction is the direction, not the sign');\n END IF;\n\n IF v_unit IS NULL AND v_invoice IS NOT NULL THEN\n SELECT t.unit_id INTO v_unit FROM public.plg_financial_invoices t WHERE t.id = v_invoice;\n END IF;\n\n v_due := coalesce((p_row ->> 'due_date')::date, (p_row ->> 'paid_at')::date, current_date);\n\n IF v_debit IS NULL OR v_credit IS NULL THEN\n SELECT * INTO v_legs FROM public.plg_financial_legacy_legs(\n p_tenant, v_kind, v_dir, (p_row ->> 'bank_account_id')::uuid, (p_row ->> 'cash_session_id')::uuid,\n v_invoice, v_meta);\n IF v_legs.o_mirror THEN\n v_meta := v_meta || jsonb_build_object('legacy_mirror', true);\n ELSE\n v_debit := coalesce(v_debit, v_legs.o_debit);\n v_credit := coalesce(v_credit, v_legs.o_credit);\n END IF;\n END IF;\n\n -- Um movimento só quita OUTRO movimento, e só se ele já estiver aqui. O\n -- ledger pode ter resolvido um pai que caiu em quarentena — apontar para ele\n -- estouraria a FK e derrubaria o pagamento inteiro por causa do vínculo.\n -- Nulo agora, preenchido na segunda oferta.\n IF v_settles IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.plg_financial_movements m WHERE m.id = v_settles AND m.tenant_id = p_tenant) THEN\n v_settles := NULL;\n END IF;\n\n -- ── A parcela nasce ANTES do movimento ────────────────────────────────────\n -- Ver 070: \\`plg_financial_movements_installment_fk\\` não é adiável, e a\n -- sincronização do espelho só acontece na segunda chamada, no fim.\n IF v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN\n PERFORM migration._fin_installment_from_bill(\n p_tenant, p_id, v_invoice, v_unit,\n (p_row ->> 'installment_number')::integer, v_due, v_amount,\n v_status = 'cancelled', false);\n END IF;\n\n SELECT EXISTS (SELECT 1 FROM public.plg_financial_movements WHERE id = p_id) INTO v_exists;\n IF NOT v_exists THEN\n INSERT INTO public.plg_financial_movements\n (id, tenant_id, unit_id, financial_invoice_id, financial_invoice_installment_id,\n direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, notes, metadata, debit_account_id, credit_account_id,\n settles_movement_id)\n VALUES (p_id, p_tenant, v_unit, v_invoice,\n CASE WHEN v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN p_id END,\n v_dir, v_kind, v_amount,\n round(coalesce((p_row ->> 'paid_amount')::numeric, 0), 2),\n v_status, v_due, (p_row ->> 'paid_at')::date,\n (p_row ->> 'installment_number')::integer, p_row ->> 'notes', v_meta, v_debit, v_credit,\n CASE WHEN v_kind = 'payment' THEN v_settles END);\n ELSE\n UPDATE public.plg_financial_movements SET\n unit_id = coalesce(unit_id, v_unit),\n debit_account_id = coalesce(debit_account_id, v_debit),\n credit_account_id = coalesce(credit_account_id, v_credit),\n -- Mesma leitura da 047: vínculo VAZIO é resposta que não chegou, não fato.\n -- Preenchido uma vez, nunca reescrito.\n settles_movement_id = coalesce(settles_movement_id,\n CASE WHEN movement_kind = 'payment' THEN v_settles END),\n financial_invoice_installment_id = coalesce(\n financial_invoice_installment_id,\n CASE WHEN v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN p_id END)\n WHERE id = p_id;\n END IF;\n\n IF v_kind IN ('bill', 'commission') AND v_invoice IS NOT NULL THEN\n PERFORM migration._fin_installment_from_bill(\n p_tenant, p_id, v_invoice, v_unit,\n (p_row ->> 'installment_number')::integer, v_due, v_amount,\n v_status = 'cancelled', true);\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_movements', p_id, jsonb_build_object('reason', CASE WHEN v_exists THEN 'updated' ELSE 'created' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_exists THEN 'already in the ledger (append-only; legs, unit, installment, settlement link and cancellation completed)' ELSE 'created' END);\nEND $function$;\n\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 3. A baixa também responde — e no Espaço Facial é ela quem responde\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- \\`payment_allocations\\` é a tabela que SABE quanto de qual pagamento quitou qual\n-- conta, e no V2 a parcela tem o id do movimento de conta (070). Então a baixa\n-- já carrega o vínculo: \\`installment_id\\` É o movimento quitado.\n--\n-- Medido em 28/09/2026: 138.104 dos 138.262 pagamentos do Espaço Facial têm\n-- baixa, contra 854 dos 138.946 com \\`parent_movement_id\\` no V1. Sem esta parte,\n-- a parte 2 conserta o Pertinho do Céu e não conserta o Espaço Facial.\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_allocation(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_payment uuid := (p_row ->> 'payment_movement_id')::uuid;\n v_bill uuid := (p_row ->> 'installment_id')::uuid;\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_unit uuid;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_payment IS NULL OR v_bill IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma baixa precisa do pagamento e da conta que ela quita');\n END IF;\n IF v_amount <= 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', 'uma baixa de valor zero ou negativo não quita nada');\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_invoice_installments i\n WHERE i.id = v_bill AND i.tenant_id = p_tenant) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a conta que esta baixa quita ainda não virou parcela (%s)', v_bill));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_movements m\n WHERE m.id = v_payment AND m.tenant_id = p_tenant) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('o pagamento desta baixa ainda não atravessou (%s)', v_payment));\n END IF;\n\n SELECT m.unit_id INTO v_unit FROM public.plg_financial_movements m WHERE m.id = v_payment;\n\n INSERT INTO public.plg_financial_allocations\n (id, tenant_id, unit_id, payment_movement_id, installment_id, amount, metadata)\n VALUES (p_id, p_tenant, v_unit, v_payment, v_bill, v_amount,\n coalesce(p_row -> 'metadata', '{}'::jsonb))\n ON CONFLICT (payment_movement_id, installment_id) DO UPDATE\n SET amount = EXCLUDED.amount;\n\n -- O vínculo, quando ele ainda não existe. Só para o pagamento que quita UMA\n -- conta: um pagamento espalhado por duas parcelas não tem \"a\" conta dele, e\n -- escolher uma seria inventar a atribuição que a tela mostra. São 12 em\n -- 140.328 — ficam sem vínculo, e a tela os trata como não atribuídos, que é o\n -- que eles são.\n UPDATE public.plg_financial_movements m\n SET settles_movement_id = v_bill\n WHERE m.id = v_payment\n AND m.tenant_id = p_tenant\n AND m.movement_kind = 'payment'\n AND m.settles_movement_id IS NULL\n AND EXISTS (SELECT 1 FROM public.plg_financial_movements b WHERE b.id = v_bill)\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations a\n WHERE a.payment_movement_id = v_payment AND a.installment_id <> v_bill);\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_allocations', p_id, jsonb_build_object('reason', 'created')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');\nEND $function$;\n\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 4. O que já está gravado\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Os writers acima só valem para a próxima oferta, e a próxima oferta de\n-- \\`financial_movements\\` devolve \\`skipped\\` para tudo: o checksum é do dado da\n-- ORIGEM, e mudar o mapa no destino não move uma vírgula no V1. O que já\n-- atravessou se preenche aqui, pela baixa, que é a mesma resposta que a parte 3\n-- passa a dar — sem depender de re-oferta nenhuma.\n--\n-- Num cluster sem porte isto atualiza zero linhas.\n\nUPDATE public.plg_financial_movements m\n SET settles_movement_id = a.installment_id\n FROM public.plg_financial_allocations a\n WHERE a.payment_movement_id = m.id\n AND m.movement_kind = 'payment'\n AND m.settles_movement_id IS NULL\n AND EXISTS (SELECT 1 FROM public.plg_financial_movements b WHERE b.id = a.installment_id)\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations o\n WHERE o.payment_movement_id = a.payment_movement_id\n AND o.installment_id <> a.installment_id);\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\nREVOKE ALL ON FUNCTION migration.upsert_fin_movement(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_movement(uuid, uuid, jsonb, jsonb) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_fin_allocation(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_allocation(uuid, uuid, jsonb, jsonb) TO service_role;\n`\n\nexport const MIGRATION_094_O_TITULO_DA_NOTA_DE_ENTRADA_SABE_DE_QUEM_ELE_E = `-- O título da nota de entrada sabe de quem ele é.\n--\n-- Contas a Pagar → detalhe de um título vindo de DANFE mostra o valor, mostra o\n-- vencimento, e não mostra fornecedor nenhum. Medido em 28/09/2026:\n--\n-- Pertinho do Céu 3.042 títulos a pagar 547 sem contraparte\n-- 540 deles nascidos de DANFE\n-- Espaço Facial títulos de DANFE 94 sem contraparte\n--\n-- Praticamente todo título nascido de nota de entrada está sem fornecedor, nos\n-- dois clientes. Não é coincidência: nenhum deles jamais teve.\n--\n-- ── Por que ────────────────────────────────────────────────────────────────\n--\n-- \\`upsert_fin_danfe_installment\\` (084) monta o título a partir do que a parcela\n-- carrega em \\`metadata.danfe\\`, e ali o fornecedor é \\`supplier_name\\` — TEXTO, o\n-- nome curto que o operador digitou na importação. O INSERT não tem\n-- \\`counterparty_person_id\\` na lista de colunas, e o extrator nunca ofereceu o\n-- ponteiro: \\`danfe_imports.supplier_id\\` existe no V1, está preenchido em 554 das\n-- 558 notas do tenant 8, e não era lido por ninguém.\n--\n-- O nome não substitui o ponteiro, e a diferença é visível a olho nu — o V1\n-- guarda o apelido na nota e a razão social no cadastro:\n--\n-- supplier_name contacts[supplier_id].name\n-- TOP ALTO TOP ALTO PARTICIPACOES E COMERCIO LTDA\n-- BETOFRUIT Hortifrutigranjeiros Betofruti Ltda ME\n-- PRIMEGAS PRIMEGAS DISTRIBUIDORA DE GAS LTDA\n--\n-- Casar por nome acertaria 137 de 554. O ponteiro acerta 554, e é o que o V1 usa\n-- para saber a quem ele deve.\n--\n-- ── O que muda ─────────────────────────────────────────────────────────────\n--\n-- O de-para passa a pedir \\`note_supplier_id\\` (o extrator o deriva da própria\n-- nota, como já faz com os outros oito campos dela) e a resolvê-lo pelo ledger\n-- de \\`contacts\\`, exatamente como \\`recurring_bills.counterparty_contact_id\\` já\n-- fazia. O writer grava a contraparte, e a preenche quando ela está vazia.\n--\n-- A nota SEM \\`supplier_id\\` na origem continua sem contraparte — 4 das 558. Nota\n-- sem fornecedor declarado é um dado ausente, e inventá-lo pelo nome seria pior\n-- que deixá-lo à vista.\n--\n-- ── O que este arquivo NÃO conserta ────────────────────────────────────────\n--\n-- O título de DANFE também não tem ITENS, e isso é outra coisa: as linhas da\n-- nota vão para \\`plg_inventory_stock_movements\\` (plugin-inventory/010), que é\n-- onde elas pertencem — é mercadoria entrando, não serviço vendido. Um título a\n-- pagar de nota de entrada mostrando itens exigiria decidir se a linha fiscal é\n-- também linha financeira, e essa decisão não existe ainda. O que existe é o\n-- vínculo: \\`plg_inventory_operations.idempotency_key\\` = a chave de acesso, e o\n-- título carrega \\`danfe:<mesma chave>\\`. A tela pode seguir esse fio; este\n-- arquivo não o inventa.\n--\n-- Idempotente.\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 1. Um título fechado pode GANHAR a contraparte que nunca teve\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- \\`trg_plg_financial_invoices_immutable\\` recusa qualquer mudança em\n-- \\`counterparty_person_id\\` depois de emitido, e recusa com razão: trocar a quem\n-- se deve é trocar o fato. Só que NULL não é um fato — é uma resposta que não\n-- chegou, e o porte é exatamente a situação em que ela chega depois.\n--\n-- É a mesma leitura que \\`plg_financial_movements_append_only\\` já faz com as\n-- pernas contábeis, na 047: \"a leg may be FILLED exactly once and never\n-- rewritten\". Aqui vale a mesma frase, com a mesma trava — preenchido uma vez,\n-- nunca reescrito.\n\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_immutable()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nBEGIN\n -- Not closed yet: a draft is a draft.\n IF OLD.issue_date IS NULL OR OLD.cancelled_at IS NOT NULL THEN RETURN NEW; END IF;\n\n -- The named exception: cancelling, and the bookkeeping that goes with it.\n IF NEW.cancelled_at IS NOT NULL AND OLD.cancelled_at IS NULL THEN RETURN NEW; END IF;\n\n IF NEW.total IS DISTINCT FROM OLD.total\n OR NEW.direction IS DISTINCT FROM OLD.direction\n -- Uma contraparte AUSENTE pode ser preenchida exatamente uma vez. Trocar\n -- uma que existe continua proibido, que é o que a imutabilidade protege.\n OR (OLD.counterparty_person_id IS NOT NULL\n AND NEW.counterparty_person_id IS DISTINCT FROM OLD.counterparty_person_id)\n OR NEW.order_id IS DISTINCT FROM OLD.order_id\n OR NEW.number IS DISTINCT FROM OLD.number\n OR NEW.competence_date IS DISTINCT FROM OLD.competence_date\n OR NEW.issue_date IS DISTINCT FROM OLD.issue_date\n OR NEW.currency IS DISTINCT FROM OLD.currency THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % is issued and cannot be altered', OLD.number\n USING ERRCODE = '23514',\n HINT = 'A closed invoice is immutable (PRD 04). Correct it with a new invoice, or cancel this one through financial_cancel_invoice.';\n END IF;\n RETURN NEW;\nEND $function$;\n\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 2. O de-para pede o ponteiro do fornecedor\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Uma chave a mais na linha da 084. \\`@contacts:public.people\\` é a mesma forma\n-- que \\`recurring_bills\\` usa — o fornecedor do V1 vive em \\`contacts\\` e vira\n-- \\`public.people\\` no V2.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nUPDATE migration.allowlist\n SET columns = jsonb_set(\n columns, '{note_supplier_id}',\n '\"counterparty_person_id@contacts:public.people\"'::jsonb, true)\n WHERE source_table = 'danfe_payments'\n AND columns ->> 'note_supplier_id' IS DISTINCT FROM\n 'counterparty_person_id@contacts:public.people';\n\nRESET migration.allowlist_unlock;\n\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 3. O writer grava a contraparte\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_danfe_installment(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_danfe jsonb := coalesce(v_meta -> 'danfe', '{}'::jsonb);\n v_note uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_invoice uuid;\n v_issue date;\n v_due date;\n v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);\n v_number integer := coalesce((p_row ->> 'number')::integer, 1);\n v_total numeric;\n v_desc text;\n -- O fornecedor, já resolvido pelo ledger de \\`contacts\\`. Nulo quando a nota do\n -- V1 não declara \\`supplier_id\\` (4 das 558 no tenant 8) — e nulo fica.\n v_party uuid := (p_row ->> 'counterparty_person_id')::uuid;\n v_existing uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma parcela precisa de id');\n END IF;\n\n BEGIN v_note := (v_danfe ->> 'import_uuid')::uuid; EXCEPTION WHEN OTHERS THEN v_note := NULL; END;\n IF v_note IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'parcela de nota de entrada sem a nota — o título dela não tem de que nascer');\n END IF;\n\n BEGIN v_issue := (v_danfe ->> 'issue_date')::timestamptz::date; EXCEPTION WHEN OTHERS THEN v_issue := NULL; END;\n BEGIN v_due := (p_row ->> 'due_date')::date; EXCEPTION WHEN OTHERS THEN v_due := NULL; END;\n v_due := coalesce(v_due, v_issue, current_date);\n\n IF v_amount < 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('parcela de valor negativo (%s) — o destino exige amount >= 0', v_amount));\n END IF;\n\n -- Uma contraparte que não é deste tenant é pior que nenhuma: ela ligaria o\n -- título a alguém de outro cliente. Recusa em silêncio e o título nasce sem.\n IF v_party IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.people p WHERE p.id = v_party AND p.tenant_id = p_tenant) THEN\n v_party := NULL;\n END IF;\n\n v_total := round(coalesce((v_danfe ->> 'payments_total')::numeric, v_amount), 2);\n v_desc := btrim('NF-e ' || coalesce(v_danfe ->> 'nfe_number', 's/n')\n || coalesce('/' || (v_danfe ->> 'nfe_series'), '')\n || coalesce(' — ' || (v_danfe ->> 'supplier_name'), ''));\n v_invoice := md5('danfe-invoice:' || v_note::text)::uuid;\n\n INSERT INTO public.plg_financial_invoices\n (id, tenant_id, unit_id, direction, number, competence_date, issue_date, total,\n description, idempotency_key, metadata, counterparty_person_id)\n VALUES (v_invoice, p_tenant, v_unit, 'payable', NULL,\n coalesce(v_issue, v_due), coalesce(v_issue, v_due), v_total,\n nullif(v_desc, ''),\n 'danfe:' || coalesce(v_danfe ->> 'access_key', v_note::text),\n jsonb_build_object('danfe', v_danfe - 'payment'), v_party)\n ON CONFLICT (id) DO NOTHING;\n\n -- A segunda parcela da mesma nota encontra o título que a primeira criou. Se\n -- aquela veio sem fornecedor e esta tem, o título ganha o que lhe faltava —\n -- uma vez, pela mesma regra do gatilho na parte 1.\n IF v_party IS NOT NULL THEN\n UPDATE public.plg_financial_invoices\n SET counterparty_person_id = v_party\n WHERE id = v_invoice AND tenant_id = p_tenant AND counterparty_person_id IS NULL;\n END IF;\n\n SELECT tenant_id INTO v_existing FROM public.plg_financial_invoice_installments WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('a parcela %s já existe em outro tenant', p_id));\n END IF;\n v_created := v_existing IS NULL;\n\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, metadata, created_at)\n VALUES (p_id, p_tenant, v_unit, v_invoice, v_number, v_due, v_amount, v_meta,\n coalesce((p_row ->> 'created_at')::timestamptz, now()))\n ON CONFLICT (id) DO UPDATE SET\n unit_id = coalesce(EXCLUDED.unit_id, public.plg_financial_invoice_installments.unit_id),\n invoice_id = EXCLUDED.invoice_id,\n number = EXCLUDED.number,\n due_date = EXCLUDED.due_date,\n amount = EXCLUDED.amount,\n metadata = migration._merge_meta(public.plg_financial_invoice_installments.metadata, EXCLUDED.metadata),\n updated_at = now();\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.plg_financial_invoice_installments', p_id,\n jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END,\n 'invoice_id', v_invoice));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 4. Os títulos que já estão gravados\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Sem depender de re-oferta e sem ler o V1: a nota já atravessou como operação\n-- de inventário, e ela carrega \\`metadata.danfe.supplier.v1_ref\\` — o mesmo\n-- \\`supplier_id\\` que o de-para passa a pedir. O fio entre as duas é a chave de\n-- acesso, que é \\`idempotency_key\\` nas duas pontas (084).\n--\n-- Medido em 28/09/2026: 634 títulos de DANFE sem contraparte nos dois tenants,\n-- 634 com a operação do lado do inventário, e 630 com o fornecedor já no ledger.\n-- Os 4 restantes são as notas que o V1 importou sem declarar fornecedor.\n--\n-- Num cluster sem porte isto atualiza zero linhas.\n\n-- A GUARDA QUE A 084 JÁ TINHA ESCRITO.\n--\n-- «este writer é do financeiro, e o financeiro roda antes do inventário — ler\n-- \\`plg_inventory_operations\\` daqui seria depender de uma tabela que ainda não\n-- existe quando este arquivo é aplicado.» — 084, oito arquivos atrás\n--\n-- É literalmente o que este backfill faz, e é por isso que a cadeia de zero\n-- morria aqui: \\`relation \"public.plg_inventory_operations\" does not exist\\`.\n-- Num cluster REAL a tabela está lá, porque o inventário já foi instalado —\n-- então o defeito só aparece no banco de ensaio, que é onde ele deve aparecer.\n--\n-- A guarda é a mesma da 015, que resolve o mesmo problema neste mesmo plugin.\n-- Ela não muda nada onde a tabela existe, e onde ela não existe também não há\n-- o que preencher: o próprio comentário acima diz «num cluster sem porte isto\n-- atualiza zero linhas».\nDO $backfill$\nBEGIN\n IF to_regclass('public.plg_inventory_operations') IS NULL THEN\n RETURN;\n END IF;\n\n UPDATE public.plg_financial_invoices i\n SET counterparty_person_id = l.target_id\n FROM public.plg_inventory_operations op\n JOIN migration.ledger l\n ON l.source_table = 'contacts'\n AND l.source_numeric_id = (op.metadata -> 'danfe' -> 'supplier' ->> 'v1_ref')::bigint\n AND l.target_id IS NOT NULL\n WHERE op.tenant_id = i.tenant_id\n AND i.idempotency_key = 'danfe:' || op.idempotency_key\n AND i.counterparty_person_id IS NULL\n AND i.cancelled_at IS NULL\n AND i.metadata ? 'danfe'\n AND EXISTS (SELECT 1 FROM public.people p\n WHERE p.id = l.target_id AND p.tenant_id = i.tenant_id);\nEND\n$backfill$;\n\n\n-- ── quem pode executar ────────────────────────────────────────────────────\nREVOKE ALL ON FUNCTION migration.upsert_fin_danfe_installment(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_danfe_installment(uuid, uuid, jsonb, jsonb) TO service_role;\n`\n\nexport const MIGRATION_095_O_CAIXA_DO_V1_E_REGISTRO_NAO_REFERENCIA = `-- O caixa do V1 é registro, não referência.\n--\n-- 32 movimentos financeiros do Pertinho do Céu estão em quarentena de \\`fk\\` por\n-- isto, e nenhum deles vai sair de lá sozinho:\n--\n-- unresolved reference(s): [{\"ref\": \"95\", \"column\": \"cash_session_id\",\n-- \"ref_table\": \"cash_register_sessions\"}]\n--\n-- \\`cash_register_sessions\\` está em \\`migration.excluded_tables\\`, por decisão\n-- escrita: a sessão de caixa do V2 é ATRIBUÍDA (\\`user_id\\` NOT NULL, uma aberta\n-- por usuário e unidade), e a do V1 não tem a quem atribuir. A tabela nunca vai\n-- existir no destino para este porte — então a referência nunca resolve, e o\n-- movimento que a cita cai para sempre.\n--\n-- ── A linha já sabia disso ─────────────────────────────────────────────────\n--\n-- A própria \\`notes\\` da allowlist de \\`financial_movements\\` diz, desde a 043:\n--\n-- \\`cash_session_id\\` é REGISTRO (v1_cash_session_ref), não referência.\n--\n-- e o mapa da mesma linha diz o contrário:\n--\n-- \"cash_session_id\": \"cash_session_id@cash_register_sessions\"\n--\n-- A nota está certa e o mapa está errado. Quem lê é o mapa.\n--\n-- ── E o writer nunca usou a resposta ───────────────────────────────────────\n--\n-- \\`upsert_fin_movement\\` não grava \\`cash_session_id\\`; ele só o entrega a\n-- \\`plg_financial_legacy_legs\\`, que o usa para achar a conta que segura o\n-- dinheiro — em \\`plg_financial_cash_register_sessions\\`, que para este porte está\n-- vazia. A função já trata a ausência: sem sessão e sem banco, a perna cai em\n-- \\`legacy_holder\\`. Ou seja, mesmo os movimentos que ATRAVESSARAM já postam em\n-- \\`legacy_holder\\`; o que a referência faz hoje é só derrubar os 32 que a citam.\n--\n-- Trocar a referência por registro não perde informação nenhuma — o id do caixa\n-- do V1 continua legível em \\`metadata.v1_cash_session_ref\\`, que é onde\n-- \\`invoices.cash_session_id\\` já o guarda — e devolve os 32 movimentos.\n--\n-- ── O que este arquivo NÃO conserta ────────────────────────────────────────\n--\n-- \\`bank_account_id\\` tem o mesmo mapa de registro (\\`jsonb:v1_bank_account_ref\\`) e\n-- o caso dele é o OPOSTO: \\`bank_accounts\\` ATRAVESSA, para\n-- \\`public.plg_financial_accounts\\`, e o ledger tem os pares (13 no tenant 8, 42 no\n-- tenant 1). Hoje todo pagamento portado posta em \\`legacy_holder\\` em vez da conta\n-- que de fato segurou o dinheiro. Corrigir aquilo muda a perna contábil de todo\n-- pagamento NOVO — e não corrige os que já entraram, porque o append-only só\n-- deixa preencher perna NULA e a deles já está escrita. É decisão de quem é dono\n-- do razão, não conserto de transporte, e por isso não vai junto.\n--\n-- Idempotente.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nUPDATE migration.allowlist\n SET columns = jsonb_set(\n columns, '{cash_session_id}', '\"jsonb:v1_cash_session_ref\"'::jsonb, true)\n WHERE source_table = 'financial_movements'\n AND columns ->> 'cash_session_id' IS DISTINCT FROM 'jsonb:v1_cash_session_ref';\n\nRESET migration.allowlist_unlock;\n\n-- A quarentena de \\`fk\\` é reaberta a cada onda (\\`_reopen_stale\\` no driver), então\n-- os 32 são reoferecidos na próxima rodada do espelho e entram. Não há o que\n-- desfazer aqui: nenhum deles chegou a ser gravado.\n`\n\nexport const MIGRATION_096_O_CAIXA_DO_V1_JA_TEM_DONO_E_PODE_ENTRAR = `-- O caixa do V1 já tem dono, e pode entrar.\n--\n-- \\`cash_register_sessions\\` está fora do porte por uma linha em\n-- \\`migration.excluded_tables\\` escrita em 03/09/2026:\n--\n-- Deferred: a V2 cash session is ATTRIBUTED — user_id is NOT NULL and only\n-- one may be open per user and unit... 091 carries V1's opener only as an\n-- opaque \\`v1_opened_by\\` reference with no mapping to a V2 auth user, so\n-- every migrated session would have to be attributed to somebody who did\n-- not open it.\n--\n-- A objeção estava certa e já foi respondida. Quem a respondeu foi o próprio\n-- writer que veio depois: \\`migration.upsert_fin_cash_session\\` existe, a linha de\n-- de-para existe com as 29 colunas mapeadas, e a \\`notes\\` dela diz a solução em\n-- voz alta —\n--\n-- \\`user_id\\` recebe o uuid de quem ABRIU no V1 — a coluna é NOT NULL e não\n-- tem FK, então ela exige identidade, não um login do V2. Assim ninguém do\n-- V2 herda a gaveta de ninguém, que era a objeção que mantinha esta tabela\n-- fora.\n--\n-- O que ficou foi a linha de exclusão, que ninguém apagou. E ela vence: o motor\n-- recusa a tabela com \\`kind='allowlist'\\` antes de olhar para o writer. Medido em\n-- 28/09/2026 no Pertinho do Céu: 13 sessões em quarentena por isso.\n--\n-- ── Por que ela pode entrar agora ──────────────────────────────────────────\n--\n-- Conferido na origem, tenant 8, 28/09/2026:\n--\n-- 22 sessões, 6 operadores distintos, TODAS com \\`opened_by_user_id\\`\n-- preenchido e \\`opened_by_name\\` legível\n-- 19 fechadas, 3 abertas — as 3 de operadores DIFERENTES, na mesma unidade\n-- 22 de 22 com \\`unit_id\\` e \\`account_central_id\\`\n-- 1 única conta (\\`bank_accounts\\` 23), já no ledger\n--\n-- As 3 abertas são o teste que importa: \\`plg_financial_cash_sessions_one_open_uq\\`\n-- é UNIQUE em \\`(tenant_id, user_id, unit_id) WHERE status='open'\\`, e três\n-- operadores distintos na mesma unidade não colidem. Se um dia colidirem, a\n-- sessão cai em quarentena \\`state\\` com o 23505 escrito — que é o comportamento\n-- certo, porque duas gavetas abertas pela mesma pessoa no mesmo lugar é um\n-- estado que o V2 recusa de propósito.\n--\n-- ── O que entra junto ──────────────────────────────────────────────────────\n--\n-- 32 movimentos financeiros do Pertinho do Céu estão em quarentena de \\`fk\\`\n-- citando \\`cash_session_id\\`. A 091 os resolve pelo outro lado (o caixa vira\n-- REGISTRO no movimento, não referência) e este arquivo os resolve pela raiz.\n-- Os dois são úteis: a 091 vale para o movimento cujo caixa NÃO atravessa —\n-- sessão de outro tenant, ou de uma janela fora da onda.\n--\n-- \\`cash_session_lines\\`, \\`cash_session_adjustments\\` e \\`cash_session_payment_links\\`\n-- (637 linhas no tenant 8) continuam sem destino no V2. Não é esquecimento: a\n-- conferência de caixa do V2 tem forma própria (\\`v_financial_cash_session_review\\`\n-- e \\`plg_financial_cash_session_reviewers\\`), e o resumo da conferência do V1 já\n-- chega em \\`metadata.review\\` pela linha de de-para que existe. O detalhe linha a\n-- linha é desenho de produto, não transporte.\n--\n-- Idempotente.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nDELETE FROM migration.excluded_tables WHERE source_table = 'cash_register_sessions';\n\nRESET migration.allowlist_unlock;\n\n-- A quarentena de \\`allowlist\\` não é reaberta pelo driver (só a de \\`fk\\` é): ela\n-- descreve uma decisão, não uma corrida. Reabrir aqui é o que faz a próxima\n-- rodada reoferecer as sessões em vez de esperar a marca d'água mexer — e o\n-- checksum do ledger devolve \\`skipped\\` para o que já estiver certo.\nUPDATE migration.ledger l\n SET status = 'pending', reason = 'readmitida: a exclusão do caixa saiu (092)', updated_at = now()\n WHERE l.source_table = 'cash_register_sessions'\n AND l.status = 'quarantined'\n AND EXISTS (SELECT 1 FROM migration.quarantine q\n WHERE q.ledger_id = l.id AND q.kind = 'allowlist' AND q.resolved_at IS NULL);\n\nUPDATE migration.quarantine q\n SET resolved_at = now(), resolution = 'a tabela voltou ao de-para (092)'\n FROM migration.ledger l\n WHERE q.ledger_id = l.id AND l.source_table = 'cash_register_sessions'\n AND q.kind = 'allowlist' AND q.resolved_at IS NULL;\n`\n\nexport const MIGRATION_097_O_CAIXA_CONTA_LINHA_POR_LINHA = `-- O caixa conta LINHA POR LINHA, e o V2 só guardava o total.\n--\n-- A 096 readmitiu \\`cash_register_sessions\\`: as 22 sessões do Pertinho do Céu\n-- atravessam e \\`plg_financial_cash_register_sessions\\` guarda abertura,\n-- fechamento, esperado, contado e diferença. Isso responde \"fechou ou não\n-- fechou\". Não responde a pergunta que vem em seguida, que é a única que o\n-- operador realmente faz: **onde** não fechou.\n--\n-- No V1 essa resposta está em três tabelas que não têm destino nenhum:\n--\n-- cash_session_lines 59 a conferência por forma de pagamento\n-- cash_session_payment_links 568 qual recebimento entrou em qual caixa\n-- cash_session_adjustments 14 o ajuste feito à mão, com motivo\n--\n-- ── A linha de conferência tem TRÊS valores, e nenhum é redundante ────────\n--\n-- system_amount o que o sistema apurou\n-- operator_amount o que o operador contou na gaveta\n-- review_amount o que a revisão da retaguarda concluiu depois\n--\n-- Guardar só a diferença jogaria fora a história: um caixa em que o operador\n-- errou a contagem e a revisão corrigiu é um caso; um em que o sistema errou é\n-- outro; e os dois fecham com a mesma diferença final. \\`por_fora\\` é a quarta\n-- informação — dinheiro que entrou sem passar pelo sistema — e é justamente a\n-- linha que ninguém quer perder.\n--\n-- ── Por que o vínculo do recebimento é uma tabela, e não uma coluna ───────\n--\n-- \\`cash_session_payment_links\\` tem \\`previous_cash_session_id\\`: o recebimento\n-- MUDA de caixa. Acontece quando alguém fecha o turno com um pagamento lançado\n-- no caixa errado e a retaguarda remaneja. Uma coluna \\`cash_session_id\\` no\n-- pagamento guardaria só o estado final e apagaria o remanejamento — que é\n-- exatamente o que uma auditoria de caixa vai procurar.\n--\n-- O recebimento aqui é \\`plg_tables_order_payments\\`, a tabela que a\n-- plugin-tables/011 criou: 989 no Pertinho do Céu, dos quais 568 têm vínculo\n-- de caixa.\n--\n-- Idempotente.\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 1. A conferência, por forma de pagamento\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_cash_session_lines (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n session_id uuid NOT NULL REFERENCES public.plg_financial_cash_register_sessions(id) ON DELETE CASCADE,\n -- 'cash', 'card', 'pix'… o V1 não fecha a lista e o V2 também não: a forma de\n -- pagamento é vocabulário do cliente, e um CHECK aqui derrubaria o caixa\n -- inteiro no dia em que a casa criar uma nova.\n kind text NOT NULL,\n payment_method_type_id uuid REFERENCES public.plg_financial_payment_method_types(id) ON DELETE SET NULL,\n card_brand text,\n settlement_kind text,\n system_amount numeric(14,2),\n system_count integer,\n operator_amount numeric(14,2),\n operator_note text,\n -- Dinheiro que entrou SEM passar pelo sistema. É a linha que explica a\n -- diferença quando ela não é erro de contagem.\n por_fora boolean NOT NULL DEFAULT false,\n review_amount numeric(14,2),\n review_status text,\n review_note text,\n reviewed_by_name text,\n reviewed_at timestamptz,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 2. Qual recebimento entrou em qual caixa — e de onde ele veio\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_cash_session_payments (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n -- SEM \\`REFERENCES\\`, e não por preguiça: seria um CICLO.\n --\n -- O \\`plugin-tables\\` já aponta para o financial — \\`plg_tables_order_payments\n -- .movement_id\\` (011) e \\`plg_tables_service_charge_snapshots\n -- .cash_session_id\\` (019) —, então o financial roda ANTES do tables na\n -- corrente. Uma FK daqui para lá referencia tabela que ainda não existe, e é\n -- exatamente onde o \\`db-from-zero\\` parou:\n --\n -- 097_o_caixa_conta_linha_por_linha.sql … FAILED\n -- ERROR: relation \"public.plg_tables_order_payments\" does not exist\n --\n -- Duas plugins não podem apontar uma para a outra numa cadeia linear. Some o\n -- CASCADE, e o preço está anotado: apagar o pagamento da mesa deixa esta\n -- linha órfã. A saída que o repositório já nomeou nas outras isenções de\n -- \\`cross-prefix-fk\\` é o par polimórfico (tipo + id) com views tolerantes à\n -- ausência do plugin dono — e é ela que fecha isto de verdade.\n order_payment_id uuid NOT NULL,\n session_id uuid NOT NULL REFERENCES public.plg_financial_cash_register_sessions(id) ON DELETE CASCADE,\n -- O caixa de ONDE ele saiu, quando foi remanejado. NULL na esmagadora\n -- maioria: só o remanejamento preenche.\n previous_session_id uuid REFERENCES public.plg_financial_cash_register_sessions(id) ON DELETE SET NULL,\n source text,\n reason text,\n created_by_name text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n -- Um recebimento está num caixa por vez. O remanejamento REESCREVE a linha e\n -- guarda de onde veio; não empilha uma segunda.\n UNIQUE (order_payment_id)\n);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 3. O ajuste feito à mão\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_cash_session_adjustments (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n session_id uuid NOT NULL REFERENCES public.plg_financial_cash_register_sessions(id) ON DELETE CASCADE,\n action text NOT NULL,\n payment_method_type_id uuid REFERENCES public.plg_financial_payment_method_types(id) ON DELETE SET NULL,\n card_brand text,\n -- O ajuste que virou dinheiro no razão. NULL quando foi só reclassificação.\n movement_id uuid REFERENCES public.plg_financial_movements(id) ON DELETE SET NULL,\n amount numeric(14,2),\n reason text,\n created_by_name text,\n payload jsonb,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 4. Unidade, índices, updated_at e a cerca\n-- ═══════════════════════════════════════════════════════════════════════════\n\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_financial_cash_session_lines','plg_financial_cash_session_payments',\n 'plg_financial_cash_session_adjustments'] LOOP\n BEGIN\n EXECUTE format('ALTER TABLE public.%I ADD CONSTRAINT %I FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL', t, t || '_unit_fkey');\n EXCEPTION WHEN duplicate_object THEN NULL; END;\n END LOOP;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_cash_session_lines_sessao_idx\n ON public.plg_financial_cash_session_lines (session_id);\nCREATE INDEX IF NOT EXISTS plg_financial_cash_session_payments_sessao_idx\n ON public.plg_financial_cash_session_payments (session_id);\nCREATE INDEX IF NOT EXISTS plg_financial_cash_session_adjustments_sessao_idx\n ON public.plg_financial_cash_session_adjustments (session_id, created_at DESC);\n\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_financial_cash_session_lines','plg_financial_cash_session_payments'] LOOP\n BEGIN\n EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()', t || '_updated_at', t);\n EXCEPTION WHEN duplicate_object THEN NULL; END;\n END LOOP;\nEND $$;\n\n-- AUTHZ PELO TEMPLATE DA CASA, não à mão.\n--\n-- Aqui havia um laço que criava quatro políticas por tabela, assim:\n--\n-- CREATE POLICY %I ON public.%I FOR SELECT USING (tenant_id = app.current_tenant_id())\n--\n-- Três coisas erradas nisso, e as três o banco de testes cobra:\n--\n-- 1. **Sem \\`TO authenticated\\`, a política é \\`FOR PUBLIC\\`** — e o pgTAP tem uma\n-- linha só para isso: *«no policy is FOR PUBLIC (every policy is TO\n-- authenticated / a named role)»*.\n-- 2. **Sem permissão**: a política olhava só o inquilino. Quem entra na casa\n-- lê o caixa inteiro, sem passar por \\`app.has_permission\\`.\n-- 3. **Fora do registro**: \\`app.scaffold_registry\\` é o censo de quem declarou\n-- o próprio acesso, e o teste cobra que NENHUMA \\`plg_financial_*\\` fique de\n-- fora em modo \\`enforce\\`. Uma tabela financeira sem template é uma tabela\n-- cujo acesso ninguém declarou.\n--\n-- As três são partes da SESSÃO de caixa, então levam o recurso dela —\n-- \\`financial.cash_session\\`, o mesmo de \\`plg_financial_cash_register_sessions\\`\n-- no baseline. Quem pode ver a sessão pode ver as linhas dela.\n--\n-- \\`unit_optional\\` e não \\`unit\\`: a linha carrega \\`unit_id\\` quando sabe, e uma\n-- linha sem unidade não pode sumir da conferência por causa de um campo vazio.\nSELECT app.scaffold_table('public.plg_financial_cash_session_lines'::regclass,\n 'financial.cash_session', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_cash_session_lines'::regclass, 'unit_optional', true);\n\nSELECT app.scaffold_table('public.plg_financial_cash_session_payments'::regclass,\n 'financial.cash_session', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_cash_session_payments'::regclass, 'unit_optional', true);\n\nSELECT app.scaffold_table('public.plg_financial_cash_session_adjustments'::regclass,\n 'financial.cash_session', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_cash_session_adjustments'::regclass, 'unit_optional', true);\n`\n\nexport const MIGRATION_098_A_CONTA_QUE_NINGUEM_PAGOU_AINDA_MUDA_DE_DATA_E_DE_PARCELAS = `-- ============================================================================\n-- 098 — a conta que ninguém pagou ainda muda de data e de parcelas\n--\n-- Uma conta a pagar nasce ISSUED (issue_date DEFAULT current_date), e o gatilho\n-- de imutabilidade tratava isso como \"lançada no livro\": a competência não\n-- mudava mais, e o formulário de edição mostrava a data como TEXTO, sem\n-- calendário. O cronograma seguia a mesma sorte — os atalhos 1x/2x/3x sumiam na\n-- edição, e salvar só reescrevia o vencimento de quem já existia.\n--\n-- Mas um título que ninguém pagou não é história: é um RASCUNHO com número.\n-- Errar a data ou o número de parcelas no cadastro e ter de cancelar e refazer\n-- a conta — queimando um número de sequência e enchendo a auditoria de\n-- cancelamentos — é caro para uma correção de digitação.\n--\n-- A regra vira: enquanto NADA foi recebido, a competência e o cronograma são\n-- editáveis. Depois do primeiro centavo, tudo volta a ser imutável, e a\n-- correção é cancelar e reemitir, como sempre foi.\n--\n-- O período fechado continua mandando: o gatilho de 006 já dispara em\n-- UPDATE OF competence_date e recusa competência em mês fechado. Este arquivo\n-- não o afrouxa.\n-- ============================================================================\n\n-- ── 1. a competência de um título sem recebimento pode ser corrigida ────────\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_immutable() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_paid numeric;\nBEGIN\n -- Not closed yet: a draft is a draft.\n IF OLD.issue_date IS NULL OR OLD.cancelled_at IS NOT NULL THEN RETURN NEW; END IF;\n\n -- The named exception: cancelling, and the bookkeeping that goes with it.\n IF NEW.cancelled_at IS NOT NULL AND OLD.cancelled_at IS NULL THEN RETURN NEW; END IF;\n\n IF NEW.total IS DISTINCT FROM OLD.total\n OR NEW.direction IS DISTINCT FROM OLD.direction\n OR NEW.counterparty_person_id IS DISTINCT FROM OLD.counterparty_person_id\n OR NEW.order_id IS DISTINCT FROM OLD.order_id\n OR NEW.number IS DISTINCT FROM OLD.number\n OR NEW.issue_date IS DISTINCT FROM OLD.issue_date\n OR NEW.currency IS DISTINCT FROM OLD.currency THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % is issued and cannot be altered', OLD.number\n USING ERRCODE = '23514',\n HINT = 'A closed invoice is immutable (PRD 04). Correct it with a new invoice, or cancel this one through financial_cancel_invoice.';\n END IF;\n\n -- A competência saiu da lista acima e ganhou a sua própria pergunta: o preço\n -- dela é uma varredura das parcelas, e não vale pagá-lo em toda gravação de\n -- observação.\n IF NEW.competence_date IS DISTINCT FROM OLD.competence_date THEN\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = OLD.id;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % has % already received and its competence date cannot move', OLD.number, v_paid\n USING ERRCODE = '23514',\n HINT = 'Reverse the payments first, or correct it with a new invoice.';\n END IF;\n END IF;\n\n RETURN NEW;\nEND $$;\n\nCOMMENT ON FUNCTION public.trg_plg_financial_invoices_immutable() IS\n 'Título emitido é imutável, menos a competência enquanto nada foi recebido (098).';\n\n-- ── 2. trocar o cronograma inteiro, de uma vez ──────────────────────────────\n-- Por que RPC e não três chamadas do cliente: o cronograma tem de somar o\n-- total do título, e (invoice_id, number) é UNIQUE e não deferrable. Apagar e\n-- reinserir numa transação é a única forma de ir de 3x para 2x sem passar por\n-- um estado inválido — e de garantir que uma falha no meio não deixe o título\n-- com meia parcela.\nCREATE OR REPLACE FUNCTION public.financial_reschedule_invoice(\n p_invoice_id uuid,\n p_installments jsonb,\n p_idempotency_key text DEFAULT NULL::text\n) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_invoice public.plg_financial_invoices%ROWTYPE;\n v_paid numeric;\n v_mirror int;\n v_sum numeric := 0;\n v_i jsonb;\n v_n int := 0;\n v_out jsonb := '[]'::jsonb;\n v_inst_id uuid;\n v_due date;\n v_amount numeric;\n v_meta jsonb;\n v_old jsonb;\nBEGIN\n SELECT * INTO v_invoice FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % not found', p_invoice_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_invoice.unit_id);\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n\n IF v_invoice.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % is cancelled', v_invoice.number USING ERRCODE = '55000';\n END IF;\n\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = p_invoice_id;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % has % already received; its schedule is history now', v_invoice.number, v_paid\n USING ERRCODE = '55000',\n HINT = 'Move the due date of an unpaid installment instead, or cancel and reissue.';\n END IF;\n\n -- O espelho legado (\\`bill\\`) é append-only: nem apaga nem muda de valor. Um\n -- título vindo do V1 carrega esses movimentos, e reescrever o cronograma\n -- deixaria o espelho apontando para parcela que não existe mais.\n SELECT count(*) INTO v_mirror FROM public.plg_financial_movements m\n WHERE m.financial_invoice_id = p_invoice_id AND m.movement_kind = 'bill' AND m.status <> 'cancelled';\n IF v_mirror > 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % carries legacy bill movements and cannot be rescheduled', v_invoice.number\n USING ERRCODE = '55000',\n HINT = 'Cancel and reissue this invoice.';\n END IF;\n\n IF jsonb_typeof(p_installments) <> 'array' OR jsonb_array_length(p_installments) = 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: installments must be a non-empty array' USING ERRCODE = '22023';\n END IF;\n FOR v_i IN SELECT * FROM jsonb_array_elements(p_installments) LOOP\n v_sum := v_sum + round(coalesce((v_i ->> 'amount')::numeric, 0), 2);\n END LOOP;\n IF v_sum <> round(v_invoice.total, 2) THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: installments sum to % but the invoice total is %', v_sum, v_invoice.total\n USING ERRCODE = '23514';\n END IF;\n\n -- O que estava anexado à parcela de número N volta para a parcela de número\n -- N. Sem isso, trocar 2x por 3x apagaria em silêncio o que o cadastro tinha\n -- pendurado ali.\n SELECT coalesce(jsonb_object_agg(i.number::text, i.metadata), '{}'::jsonb) INTO v_old\n FROM public.plg_financial_invoice_installments i WHERE i.invoice_id = p_invoice_id;\n\n DELETE FROM public.plg_financial_invoice_installments WHERE invoice_id = p_invoice_id;\n\n FOR v_i IN SELECT * FROM jsonb_array_elements(p_installments) LOOP\n v_n := v_n + 1;\n v_due := coalesce((v_i ->> 'due_date')::date, v_invoice.competence_date, current_date);\n v_amount := round(coalesce((v_i ->> 'amount')::numeric, 0), 2);\n v_meta := CASE WHEN jsonb_typeof(v_i -> 'metadata') = 'object' THEN v_i -> 'metadata'\n ELSE coalesce(v_old -> v_n::text, '{}'::jsonb) END;\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount, metadata)\n VALUES (v_tenant, v_invoice.unit_id, p_invoice_id, v_n, v_due, v_amount, v_meta)\n RETURNING id INTO v_inst_id;\n v_out := v_out || jsonb_build_object('id', v_inst_id, 'number', v_n, 'due_date', v_due, 'amount', v_amount);\n END LOOP;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.reschedule', 'plg_financial_invoices', p_invoice_id::text,\n jsonb_build_object('number', v_invoice.number, 'installments', v_n, 'idempotency_key', p_idempotency_key));\n\n RETURN jsonb_build_object('invoice_id', p_invoice_id, 'installments', v_out);\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_reschedule_invoice(uuid, jsonb, text) IS\n 'Troca o cronograma inteiro de um título sem recebimento; soma tem de bater com o total (098).';\n\nREVOKE ALL ON FUNCTION public.financial_reschedule_invoice(uuid, jsonb, text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.financial_reschedule_invoice(uuid, jsonb, text) TO authenticated;\nGRANT ALL ON FUNCTION public.financial_reschedule_invoice(uuid, jsonb, text) TO service_role;\n`\n\nexport const MIGRATION_099_OS_ITENS_DE_UMA_CONTA_SEM_RECEBIMENTO_AINDA_SE_CORRIGEM = `-- ============================================================================\n-- 099 — os itens de uma conta sem recebimento ainda se corrigem\n--\n-- A 098 abriu a data e o cronograma enquanto o título não recebeu nada. O que\n-- ficou fechado foram os ITENS: \\`items_invoice_line_immutable\\` recusa qualquer\n-- UPDATE ou DELETE de linha de fatura emitida, e o total do título é imutável\n-- no gatilho da 000. Resultado: digitar 140 no lugar de 14 no cadastro só se\n-- conserta cancelando a conta e lançando outra — queimando um número de\n-- sequência e enchendo a auditoria de cancelamentos por causa de um dígito.\n--\n-- A regra é a mesma da 098, e é a regra certa: um título que ninguém pagou é\n-- um RASCUNHO COM NÚMERO. Depois do primeiro centavo tudo volta a ser história\n-- — a linha virou razão, e razão não se reescreve.\n--\n-- O período fechado continua mandando: o gatilho de 006 dispara em\n-- UPDATE OF competence_date, total e recusa competência em mês fechado.\n-- ============================================================================\n\n-- ── 1. a linha de uma fatura sem recebimento ────────────────────────────────\nCREATE OR REPLACE FUNCTION public.trg_invoice_line_immutable() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_issued date; v_cancelled timestamptz; v_invoice uuid; v_paid numeric;\nBEGIN\n v_invoice := coalesce(NEW.invoice_id, OLD.invoice_id);\n IF v_invoice IS NULL THEN RETURN coalesce(NEW, OLD); END IF;\n SELECT f.issue_date, f.cancelled_at INTO v_issued, v_cancelled\n FROM public.plg_financial_invoices f WHERE f.id = v_invoice;\n IF v_issued IS NOT NULL AND v_cancelled IS NULL THEN\n -- O que decide não é mais \"foi emitida\", é \"já entrou dinheiro\".\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = v_invoice;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'items: the lines of invoice % cannot be altered — % already received', v_invoice, v_paid\n USING ERRCODE = '23514',\n HINT = 'A paid invoice is immutable (PRD 04). Reverse the payments, or raise a new invoice.';\n END IF;\n END IF;\n RETURN coalesce(NEW, OLD);\nEND $$;\n\nCOMMENT ON FUNCTION public.trg_invoice_line_immutable() IS\n 'Linha de fatura é imutável depois do primeiro recebimento; antes dele, ainda é rascunho (099).';\n\n-- ── 2. e o total que essas linhas somam ─────────────────────────────────────\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_immutable() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_paid numeric;\nBEGIN\n IF OLD.issue_date IS NULL OR OLD.cancelled_at IS NOT NULL THEN RETURN NEW; END IF;\n IF NEW.cancelled_at IS NOT NULL AND OLD.cancelled_at IS NULL THEN RETURN NEW; END IF;\n\n IF NEW.direction IS DISTINCT FROM OLD.direction\n OR NEW.counterparty_person_id IS DISTINCT FROM OLD.counterparty_person_id\n OR NEW.order_id IS DISTINCT FROM OLD.order_id\n OR NEW.number IS DISTINCT FROM OLD.number\n OR NEW.issue_date IS DISTINCT FROM OLD.issue_date\n OR NEW.currency IS DISTINCT FROM OLD.currency THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % is issued and cannot be altered', OLD.number\n USING ERRCODE = '23514',\n HINT = 'A closed invoice is immutable (PRD 04). Correct it with a new invoice, or cancel this one through financial_cancel_invoice.';\n END IF;\n\n -- Competência (098) e total (099) têm a mesma licença e o mesmo limite, e a\n -- pergunta só se faz quando um dos dois mudou: ela varre as parcelas, e não\n -- vale pagá-la em toda gravação de observação.\n IF NEW.competence_date IS DISTINCT FROM OLD.competence_date\n OR NEW.total IS DISTINCT FROM OLD.total THEN\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = OLD.id;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % has % already received and cannot be rewritten', OLD.number, v_paid\n USING ERRCODE = '23514',\n HINT = 'Reverse the payments first, or correct it with a new invoice.';\n END IF;\n END IF;\n\n RETURN NEW;\nEND $$;\n\nCOMMENT ON FUNCTION public.trg_plg_financial_invoices_immutable() IS\n 'Título emitido é imutável; competência e total ainda se corrigem enquanto nada foi recebido (098, 099).';\n`\n\nexport const MIGRATION_100_A_CONTA_BANCARIA_DIZ_DE_QUEM_ELA_E = `-- ---------------------------------------------------------------------------\n-- 100_a_conta_bancaria_diz_de_quem_ela_e.sql — quatro colunas que faltavam\n-- para uma conta bancária ser uma conta bancária.\n--\n-- \\`plg_financial_accounts\\` já sabia o nome do banco, a agência e o número da\n-- conta. Faltava o que a tela de cadastro pergunta logo depois, e o que o boleto\n-- e o arquivo CNAB exigem antes de sair:\n--\n-- bank_code o NÚMERO do banco — 077, 104, 290. Não é decoração: é o\n-- que identifica a instituição em qualquer integração\n-- bancária, e é a chave que o CNAB e o PIX usam. \"Inter\" é\n-- como a pessoa chama; \"077\" é como o sistema bancário\n-- chama, e os dois são necessários.\n--\n-- holder_name o TITULAR. A conta de uma rede raramente está no nome da\n-- rede: está no nome do sócio, da holding, da empresa de\n-- serviços. Quem confere um extrato precisa saber de quem é\n-- a conta antes de bater o valor.\n--\n-- holder_document CPF ou CNPJ do titular. É o par do nome — e sem ele uma\n-- transferência sai para o titular errado com o nome certo.\n--\n-- card_brand_id a BANDEIRA, para a conta que é de cartão. Uma conta\n-- \\`card_payable\\` é \"o cartão Amex do Silvio\", e sem a\n-- bandeira ela é só \"um cartão\": a fatura não se concilia\n-- com a adquirente, e a taxa de \\`plg_financial_card_fee_configs\\`\n-- — que JÁ é por bandeira — não tem como encontrar a conta.\n--\n-- Por que aqui e não numa tabela à parte: \\`plg_financial_accounts\\` já carrega\n-- \\`bank_name\\`, \\`agency_number\\` e \\`account_number\\`. Pôr o código do banco numa\n-- segunda tabela separaria três campos do mesmo formulário em dois lugares, e a\n-- primeira consulta que alguém escrevesse teria de juntá-los de novo.\n--\n-- Todas NULL: uma conta de pessoa, de caixa físico ou de sessão não tem banco\n-- nem titular, e exigir qualquer uma delas transformaria as quatro num campo\n-- obrigatório para quem não tem o que preencher.\n--\n-- Idempotente e replay-safe.\n-- ---------------------------------------------------------------------------\n\nALTER TABLE public.plg_financial_accounts\n ADD COLUMN IF NOT EXISTS bank_code text,\n ADD COLUMN IF NOT EXISTS holder_name text,\n ADD COLUMN IF NOT EXISTS holder_document text,\n ADD COLUMN IF NOT EXISTS card_brand_id uuid;\n\n-- A bandeira é uma REFERÊNCIA ao catálogo, não um texto solto. \"Master\",\n-- \"MASTERCARD\" e \"mastercard\" digitados em três contas diferentes são a mesma\n-- bandeira, e só o catálogo faz com que a taxa as encontre.\n--\n-- ON DELETE SET NULL e não CASCADE: apagar uma bandeira do catálogo não pode\n-- levar junto a conta bancária da casa.\nDO $$\nBEGIN\n IF to_regclass('public.plg_financial_card_brands') IS NULL THEN RETURN; END IF;\n IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_accounts_card_brand_fk') THEN RETURN; END IF;\n ALTER TABLE public.plg_financial_accounts\n ADD CONSTRAINT plg_financial_accounts_card_brand_fk\n FOREIGN KEY (card_brand_id) REFERENCES public.plg_financial_card_brands(id) ON DELETE SET NULL;\nEND $$;\n\n-- O código do banco é numérico e curto (o COMPE brasileiro tem 3 dígitos; a\n-- ISPB, 8). O CHECK recusa o nome do banco digitado no campo do código, que é o\n-- erro que este arquivo existe para tornar impossível — e é exatamente o erro\n-- que o V1 cometeu: lá \\`bank_name\\` guarda \"077\" em algumas linhas.\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_accounts_bank_code_ck') THEN RETURN; END IF;\n ALTER TABLE public.plg_financial_accounts\n ADD CONSTRAINT plg_financial_accounts_bank_code_ck\n CHECK (bank_code IS NULL OR bank_code ~ '^[0-9]{3,8}$');\nEND $$;\n\nCREATE INDEX IF NOT EXISTS plg_financial_accounts_card_brand_idx\n ON public.plg_financial_accounts (tenant_id, card_brand_id)\n WHERE card_brand_id IS NOT NULL;\n\nCOMMENT ON COLUMN public.plg_financial_accounts.bank_code IS\n 'Número da instituição (COMPE, 3 dígitos, ou ISPB). É o identificador que CNAB, boleto e PIX usam; \\`bank_name\\` é o nome que a pessoa lê.';\nCOMMENT ON COLUMN public.plg_financial_accounts.holder_name IS\n 'Titular da conta. Raramente é o nome do tenant: é o sócio, a holding ou a empresa de serviços em cujo nome a conta está aberta.';\nCOMMENT ON COLUMN public.plg_financial_accounts.holder_document IS\n 'CPF ou CNPJ do titular, como digitado. Par obrigatório de holder_name para qualquer transferência.';\nCOMMENT ON COLUMN public.plg_financial_accounts.card_brand_id IS\n 'Bandeira, para conta de cartão (kind card_payable / card_receivable_*). É o que liga a conta às taxas de plg_financial_card_fee_configs, que já são por bandeira.';\n`\n\nexport const MIGRATION_101_O_CARTAO_SE_RECONHECE_PELOS_QUATRO_ULTIMOS = `-- ---------------------------------------------------------------------------\n-- 101_o_cartao_se_reconhece_pelos_quatro_ultimos.sql\n--\n-- \\`plg_financial_accounts.last_four\\` — os quatro últimos dígitos do cartão.\n--\n-- ── por que isto é um campo, e não um detalhe ──────────────────────────\n--\n-- Uma casa tem «Cartão Silvio Mario», «Cartão Silvio Master 0307» e «Cartão\n-- Silvio Amex 0016» — três nomes que só se distinguem porque ALGUÉM digitou o\n-- final do número dentro do nome. É o mesmo sintoma que a unidade tinha nas\n-- contas homônimas: o operador põe no campo errado o dado que a tela não pede.\n--\n-- Com coluna, o final volta a ser o que ele é — a identificação do cartão na\n-- fatura e no extrato da adquirente, que é por onde se concilia — e o NOME\n-- volta a ser o nome («Cartão do Silvio»). Quem já digitou no nome não perde\n-- nada: a coluna nasce vazia e o nome continua lá.\n--\n-- ── quatro dígitos, e só ───────────────────────────────────────────────\n--\n-- O CHECK recusa qualquer coisa que não sejam exatamente quatro dígitos. Não é\n-- rigor decorativo: um campo livre aqui recebe «1234-5678-9012-3456» mais cedo\n-- ou mais tarde, e guardar o número inteiro de um cartão é um problema de outra\n-- natureza — dado de portador que ninguém aqui quer ter, nem para conciliar.\n-- Quatro dígitos não identificam um cartão para cobrar nele; identificam para\n-- RECONHECER, que é tudo o que a tela precisa.\n-- ---------------------------------------------------------------------------\n\nALTER TABLE public.plg_financial_accounts\n ADD COLUMN IF NOT EXISTS last_four text;\n\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conname = 'plg_financial_accounts_last_four_check'\n ) THEN\n ALTER TABLE public.plg_financial_accounts\n ADD CONSTRAINT plg_financial_accounts_last_four_check\n CHECK (last_four IS NULL OR last_four ~ '^[0-9]{4}$');\n END IF;\nEND $$;\n\nCOMMENT ON COLUMN public.plg_financial_accounts.last_four IS\n 'Os quatro últimos dígitos do cartão — o que a fatura e o extrato da adquirente mostram, e por onde se reconhece qual cartão é. NUNCA o número completo: o CHECK recusa qualquer coisa que não sejam exatamente quatro dígitos.';\n\nCOMMENT ON COLUMN public.plg_financial_accounts.color IS\n 'A cor do cartão, como ele é na carteira. Serve para reconhecê-lo de relance numa lista onde três linhas começam com a mesma palavra.';\n`\n\nexport const MIGRATION_102_A_BANDEIRA_DO_CARTAO_E_DO_PAIS_NAO_DO_LOJISTA = `-- ---------------------------------------------------------------------------\n-- 102 — a bandeira do cartão é do PAÍS, não do lojista.\n--\n-- Mesma história da 002, outra tabela. \\`plg_financial_card_brands\\` nasce vazia\n-- em todo tenant: o Feira Demo tem ZERO bandeiras e o campo «Bandeira» do\n-- cadastro de cartão abre um seletor sem nenhuma opção. O lojista não tem como\n-- adivinhar que precisa cadastrar «Visa» antes de cadastrar o cartão Visa.\n--\n-- E o custo não é só de cadastro: as taxas de \\`plg_financial_card_fee_configs\\`\n-- JÁ são por bandeira. Sem catálogo, a conta de recebíveis não encontra a taxa,\n-- e a conciliação do cartão não fecha.\n--\n-- POR QUE POR TENANT E NÃO GLOBAL — a mesma razão que a 002 escreveu:\n-- \\`tenant_id\\` é NOT NULL, e uma linha global não cabe sem mexer no schema e na\n-- RLS de uma tabela que já tem 57 linhas em produção. Semear por tenant dá o\n-- mesmo resultado visível (a lista vem pronta) sem tocar em isolamento, e\n-- \\`is_active\\` continua sendo o que É do lojista: aceitar ou não cada uma.\n--\n-- QUAIS: as que passam num balcão brasileiro. Não é a lista completa das\n-- bandeiras do mundo — um seletor com quarenta opções é a mesma poluição que a\n-- lista vazia, do outro lado. Vale-refeição entra porque no Brasil ele passa na\n-- mesma maquininha e concilia pelo mesmo caminho.\n-- ---------------------------------------------------------------------------\n\nCREATE OR REPLACE FUNCTION public.financial_seed_card_brands(p_tenant uuid)\nRETURNS void\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = public, pg_catalog\nAS $$\nBEGIN\n -- Só quem ainda não tem nenhuma. Um tenant que já montou o catálogo dele não\n -- recebe as nossas por cima — a 002 aprendeu isso do lado das formas.\n IF EXISTS (SELECT 1 FROM public.plg_financial_card_brands WHERE tenant_id = p_tenant) THEN\n RETURN;\n END IF;\n\n INSERT INTO public.plg_financial_card_brands (tenant_id, name, is_active)\n SELECT p_tenant, nome, ativo\n FROM (VALUES\n ('Visa', true),\n ('Mastercard', true),\n ('Elo', true),\n ('American Express', true),\n ('Hipercard', true),\n ('Diners Club', false),\n ('Discover', false),\n -- Vale-refeição e vale-alimentação passam na mesma maquininha e conciliam\n -- pelo mesmo caminho; para o razão, são bandeira como as outras.\n ('Alelo', true),\n ('Sodexo', true),\n ('Ticket', true),\n ('VR', true),\n ('Pluxee', true)\n ) AS t(nome, ativo);\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_seed_card_brands(uuid) IS\n 'Semeia o catálogo de bandeiras de cartão de um tenant. Idempotente: não faz nada se o tenant já tem alguma. Ver a 102 e a 002 — bandeira e forma de pagamento são do país, não do lojista.';\n\nGRANT EXECUTE ON FUNCTION public.financial_seed_card_brands(uuid) TO authenticated;\n\n-- Todo tenant que existe hoje e está sem catálogo.\nDO $$\nDECLARE t record;\nBEGIN\n FOR t IN SELECT id FROM public.tenants LOOP\n PERFORM public.financial_seed_card_brands(t.id);\n END LOOP;\nEND $$;\n\n-- E os que vierem. Mesmo gatilho que a 002 pendurou para as formas.\nCREATE OR REPLACE FUNCTION public.trg_seed_card_brands()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = public, pg_catalog\nAS $$\nBEGIN\n PERFORM public.financial_seed_card_brands(NEW.id);\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS seed_card_brands ON public.tenants;\nCREATE TRIGGER seed_card_brands\n AFTER INSERT ON public.tenants\n FOR EACH ROW EXECUTE FUNCTION public.trg_seed_card_brands();\n`\n\nexport const MIGRATION_103_O_RAIL_DA_FICHA_E_IGUAL_EM_TODO_CADASTRO = `-- ---------------------------------------------------------------------------\n-- 103 — o rail da ficha é igual em todo cadastro.\n--\n-- Irmã da 150 do núcleo, para os cadastros do financeiro. Mesmo argumento: o\n-- rail direito responde sempre às mesmas quatro perguntas — de que unidade é,\n-- se vale, o que alguém anotou, e como a casa recorta este registro —, e um\n-- lugar fixo só é fixo se estiver em todos.\n--\n-- Aqui a falta doía mais do que no núcleo: uma conta bancária é exatamente o\n-- tipo de registro sobre o qual alguém precisa anotar («conta antiga, só para\n-- o aluguel»), e não tinha onde.\n-- ---------------------------------------------------------------------------\n\nALTER TABLE public.plg_financial_accounts\n ADD COLUMN IF NOT EXISTS notes text,\n ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];\n\nALTER TABLE public.plg_financial_chart_of_accounts\n ADD COLUMN IF NOT EXISTS notes text,\n ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];\n\nALTER TABLE public.plg_financial_cost_centers\n ADD COLUMN IF NOT EXISTS notes text,\n ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];\n\nALTER TABLE public.plg_financial_payment_method_types\n ADD COLUMN IF NOT EXISTS notes text,\n ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];\n\nALTER TABLE public.plg_financial_card_brands\n ADD COLUMN IF NOT EXISTS notes text,\n ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];\n\nCREATE INDEX IF NOT EXISTS plg_financial_accounts_tags_gin\n ON public.plg_financial_accounts USING gin (tags);\n`\n\nexport const MIGRATION_104_SERVICO_E_O_QUE_A_CASA_VENDE = `-- Serviço é o que a casa VENDE, nunca o que ela compra.\n--\n-- O catálogo de serviços é o que a casa FORNECE: dele saem o preço de tabela, a\n-- duração, a comissão e a execução. Uma conta a PAGAR que aponta para uma linha\n-- desse catálogo lança a despesa contra a própria linha de receita — e é assim\n-- que \"quanto entrou de Botox\" passa a somar o que saiu.\n--\n-- O V1 é explícito sobre isso e resolve na porta: o seletor \"O que deseja\n-- adicionar?\" de \\`PayablesForm\\` esconde Serviço, Pacote e Gift Card com\n-- \\`!isPayable\\`. Numa conta a pagar sobram Produto — que a casa compra de\n-- verdade — e \"Outra Despesa\". O V2 oferecia os mesmos tipos nos dois sentidos,\n-- e a porta já foi fechada no aplicativo (\\`availableInvoiceItemTypes\\`).\n--\n-- Falta o que já entrou por ela. São poucas linhas e elas não somem: viram\n-- \"Outra Despesa\", que é o que o V1 teria gravado. O nome digitado fica —\n-- «Produção Musical – Studio Session» continua dizendo o que foi pago; o que\n-- sai é a AMARRA ao catálogo, que é a parte errada.\n--\n-- \\`service_id\\` também é limpo: deixá-lo apontando faria o item continuar\n-- contando como venda daquele serviço em qualquer relatório que o siga, com o\n-- tipo dizendo outra coisa. Uma linha não pode ter duas respostas.\n--\n-- ── Por que o gatilho de imutabilidade sai do caminho ──────────────────────\n--\n-- \\`items_invoice_line_immutable\\` recusa qualquer alteração na linha de uma\n-- conta que já recebeu dinheiro, e ele está certo: o valor de uma conta paga é\n-- histórico, não rascunho. Só que ele recusa a linha INTEIRA, e o que muda aqui\n-- não é dinheiro — quantidade, preço e total ficam byte a byte como estão. O\n-- que muda é a CLASSIFICAÇÃO, que é justamente o que está errado e o que\n-- nenhum extrato reproduz.\n--\n-- Por isso ele é desligado para este UPDATE e religado em seguida, inclusive se\n-- o UPDATE falhar. Uma correção de classificação com o dinheiro intocado é o\n-- caso para o qual esta porta existe.\n--\n-- Idempotente: roda de novo sem achar nada.\n\nALTER TABLE public.items DISABLE TRIGGER items_invoice_line_immutable;\n\n-- \\`items.service_id\\` SÓ SE A COLUNA EXISTIR.\n--\n-- O espelho \\`public.services\\` foi aposentado pela 191 do spine — um serviço é\n-- um produto de \\`kind='service'\\` — e com ele saiu a coluna que apontava para\n-- lá. Um pool antigo ainda a tem e precisa da limpeza; um banco levantado do\n-- zero não, e citá-la derruba a cadeia em \\`column i.service_id does not exist\\`.\n-- Foi o que o db-from-zero pegou.\n--\n-- O \\`itemKind\\` é corrigido nos dois casos: ele é o que a tela lê.\nDO $servico$\nDECLARE v_tem_coluna boolean := EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'items' AND column_name = 'service_id'\n);\nBEGIN\n EXECUTE format($sql$\n UPDATE public.items i\n SET metadata = jsonb_set(\n coalesce(i.metadata, '{}'::jsonb) - 'referenceId',\n '{itemKind}', '\"other\"'::jsonb, true),\n %s\n updated_at = now()\n FROM public.plg_financial_invoices fi\n WHERE fi.id = i.invoice_id\n AND fi.direction = 'payable'\n AND (i.metadata ->> 'itemKind' = 'service' %s)\n $sql$,\n CASE WHEN v_tem_coluna THEN 'service_id = NULL,' ELSE '' END,\n CASE WHEN v_tem_coluna THEN 'OR i.service_id IS NOT NULL' ELSE '' END);\nEND $servico$;\n\nALTER TABLE public.items ENABLE TRIGGER items_invoice_line_immutable;\n`\n\nexport const MIGRATION_105_A_OCORRENCIA_PROVISIONADA_VIRA_CONTA_QUANDO_ALGUEM_MANDA = `-- A ocorrência provisionada vira conta quando ALGUÉM manda.\n--\n-- ── O que o V1 faz ─────────────────────────────────────────────────────────\n--\n-- A recorrência do V1 tem dois tempos, e é isso que a torna confiável:\n--\n-- 1. PROVISIONAR — \\`recurring_bill_occurrences\\` ganha uma linha por\n-- vencimento futuro, com \\`status = 'provisioned'\\`. Ela não é conta: não\n-- tem fatura, não entra no razão, não cobra ninguém. É uma PREVISÃO.\n-- 2. CONSOLIDAR — alguém clica «Consolidar» na linha e a RPC\n-- \\`consolidate_recurring_occurrence\\` GERA a conta real. A ocorrência\n-- passa a \\`consolidated\\` e aponta para o título gerado.\n--\n-- A lista de Contas a Pagar mostra as provisionadas como linhas virtuais ao\n-- lado das contas de verdade — o operador vê o mês inteiro, o previsto e o\n-- lançado, e decide o que vira dívida.\n--\n-- ── O que o V2 tinha ───────────────────────────────────────────────────────\n--\n-- As duas tabelas, e 1.169 ocorrências provisionadas migradas do V1 — todas\n-- invisíveis. \\`financial_recurring_run\\` pula o primeiro tempo: ela cria a\n-- fatura direto da régua, o que é auto-consolidar sem o passo de alguém\n-- mandar. Faltava exatamente a porta do segundo tempo.\n--\n-- ── O que esta migration acrescenta ────────────────────────────────────────\n--\n-- \\`financial_consolidate_occurrence(uuid)\\` — o equivalente do V1, para UMA\n-- ocorrência. Reusa \\`financial_create_invoice\\` (nenhum caminho paralelo até o\n-- dinheiro: extrato, saldo e KPI veem o mesmo evento) e copia as linhas da\n-- régua do mesmo jeito que \\`financial_recurring_run\\` copia.\n--\n-- Três decisões que valem explicação:\n--\n-- \\`overridden\\` A ocorrência manda no valor e na data, não a régua. O V1\n-- deixa editar o vencimento provisionado, e consolidar com o\n-- valor da régua desfaria a edição em silêncio.\n--\n-- idempotência A chave é a mesma de \\`financial_recurring_run\\`\n-- (\\`recurring:<bill>:<AAAA-MM>\\`), então consolidar à mão o que\n-- a rotina já criou devolve a fatura existente em vez de\n-- duplicar a dívida. Consolidar é uma ação de um clique numa\n-- lista; clique duplo não pode custar uma conta a mais.\n--\n-- permissão \\`financial.create\\`, a mesma de lançar conta à mão. Consolidar\n-- CRIA dívida — não é um detalhe de exibição.\n--\n-- Idempotente: substitui a função.\n\nCREATE OR REPLACE FUNCTION public.financial_consolidate_occurrence(p_occurrence_id uuid)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_occ record;\n v_bill record;\n v_key text;\n v_res jsonb;\n v_invoice uuid;\n v_line jsonb;\n v_i int := 0;\n v_item uuid;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_consolidate_occurrence: sem tenant na sessao' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.create', NULL) THEN\n RAISE EXCEPTION 'financial_consolidate_occurrence: exige financial.create' USING ERRCODE = '42501';\n END IF;\n\n SELECT * INTO v_occ\n FROM public.plg_financial_recurring_bill_occurrences\n WHERE id = p_occurrence_id AND tenant_id = v_tenant;\n\n IF v_occ IS NULL THEN\n RAISE EXCEPTION 'financial_consolidate_occurrence: ocorrencia % nao encontrada', p_occurrence_id\n USING ERRCODE = 'P0002';\n END IF;\n\n -- Já consolidada devolve o que existe. Duas pessoas na mesma lista clicam o\n -- mesmo botão, e a segunda não pode criar a segunda dívida.\n IF v_occ.status = 'consolidated' THEN\n RETURN jsonb_build_object('occurrence_id', v_occ.id, 'invoice_id', v_occ.invoice_id, 'created', false);\n END IF;\n IF v_occ.status = 'cancelled' THEN\n RAISE EXCEPTION 'financial_consolidate_occurrence: ocorrencia cancelada' USING ERRCODE = '23514';\n END IF;\n\n SELECT * INTO v_bill\n FROM public.plg_financial_recurring_bills\n WHERE id = v_occ.recurring_bill_id AND tenant_id = v_tenant;\n\n IF v_bill IS NULL THEN\n RAISE EXCEPTION 'financial_consolidate_occurrence: regua da ocorrencia % sumiu', p_occurrence_id\n USING ERRCODE = 'P0002';\n END IF;\n\n v_key := 'recurring:' || v_bill.id::text || ':'\n || to_char(coalesce(v_occ.competence_date, v_occ.due_date), 'YYYY-MM');\n\n SELECT f.id INTO v_invoice\n FROM public.plg_financial_invoices f\n WHERE f.tenant_id = v_tenant\n AND f.metadata ->> 'recurring_key' = v_key\n AND f.cancelled_at IS NULL\n LIMIT 1;\n\n IF v_invoice IS NULL THEN\n v_res := public.financial_create_invoice(\n v_bill.direction,\n v_occ.amount,\n v_bill.counterparty_person_id,\n NULL,\n coalesce(v_occ.unit_id, v_bill.unit_id),\n jsonb_build_array(jsonb_build_object('number', 1, 'due_date', v_occ.due_date, 'amount', v_occ.amount)),\n coalesce(v_occ.competence_date, v_occ.due_date),\n v_bill.description,\n coalesce(v_occ.notes, v_bill.notes),\n jsonb_build_object('source', 'financial_consolidate_occurrence',\n 'recurring_bill_id', v_bill.id,\n 'recurring_occurrence_id', v_occ.id,\n 'recurring_key', v_key),\n v_key\n );\n v_invoice := (v_res ->> 'invoice_id')::uuid;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(v_bill.items, '[]'::jsonb))\n LOOP\n INSERT INTO public.items (\n tenant_id, invoice_id, name, quantity, unit_price, discount, total, sort_order, metadata\n ) VALUES (\n v_tenant, v_invoice,\n v_line ->> 'description',\n coalesce((v_line ->> 'quantity')::numeric, 1),\n coalesce((v_line ->> 'unit_price')::numeric, 0),\n coalesce((v_line ->> 'discount')::numeric, 0),\n round(coalesce((v_line ->> 'quantity')::numeric, 1) * coalesce((v_line ->> 'unit_price')::numeric, 0)\n - coalesce((v_line ->> 'discount')::numeric, 0)\n + coalesce((v_line ->> 'surcharge')::numeric, 0), 2),\n v_i,\n jsonb_build_object('itemKind', v_line ->> 'item_kind', 'referenceId', v_line ->> 'reference_id')\n )\n RETURNING id INTO v_item;\n\n IF (v_line ->> 'account_id') IS NOT NULL OR (v_line ->> 'cost_center_id') IS NOT NULL THEN\n INSERT INTO public.plg_financial_invoice_items (\n item_id, parent_kind, tenant_id, chart_account_id, cost_center_id\n ) VALUES (\n v_item, 'invoice', v_tenant,\n nullif(v_line ->> 'account_id', '')::uuid,\n nullif(v_line ->> 'cost_center_id', '')::uuid\n )\n ON CONFLICT (item_id) DO UPDATE\n SET chart_account_id = excluded.chart_account_id,\n cost_center_id = excluded.cost_center_id;\n END IF;\n\n v_i := v_i + 1;\n END LOOP;\n END IF;\n\n UPDATE public.plg_financial_recurring_bill_occurrences\n SET status = 'consolidated', invoice_id = v_invoice, updated_at = now()\n WHERE id = v_occ.id;\n\n RETURN jsonb_build_object('occurrence_id', v_occ.id, 'invoice_id', v_invoice, 'created', true);\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_consolidate_occurrence(uuid) IS\n 'Transforma UMA ocorrência provisionada na conta real (V1: consolidate_recurring_occurrence). Idempotente pela mesma chave de financial_recurring_run.';\n\nREVOKE ALL ON FUNCTION public.financial_consolidate_occurrence(uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_consolidate_occurrence(uuid) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_consolidate_occurrence(uuid) TO service_role;\n`\n\nexport const MIGRATION_106_A_RECORRENCIA_NOVA_TAMBEM_PROVISIONA = `-- A recorrência cadastrada HOJE também provisiona.\n--\n-- As 1.169 ocorrências provisionadas que o V2 tem vieram todas da migração do\n-- V1. Nada no V2 as CRIA: \\`financial_recurring_run\\` pula o primeiro tempo e\n-- gera a fatura direto da régua. O efeito é que uma recorrência cadastrada\n-- agora não aparece como previsto em lugar nenhum — ela some até alguém rodar\n-- a rotina, e aí nasce como dívida sem ninguém ter conferido.\n--\n-- Esta função é o primeiro tempo do V1: escreve os vencimentos FUTUROS como\n-- \\`provisioned\\`, sem tocar em fatura nenhuma. Quem os transforma em conta é\n-- \\`financial_consolidate_occurrence\\`, um a um, quando alguém manda.\n--\n-- ── Horizonte ──────────────────────────────────────────────────────────────\n--\n-- Doze meses por padrão. O suficiente para o ano fechar na tela de quem\n-- planeja, e curto o bastante para que mudar o valor da régua não deixe uma\n-- fila de previsões erradas até 2030. Quem quiser mais passa \\`p_through\\`.\n--\n-- ── Idempotência ───────────────────────────────────────────────────────────\n--\n-- \\`(recurring_bill_id, seq)\\` já é único, e o \\`seq\\` é derivado do MÊS — não de\n-- um contador. Rodar de novo sobre a mesma régua não duplica nem renumera, e\n-- uma ocorrência já consolidada continua onde está: o \\`ON CONFLICT DO NOTHING\\`\n-- é o que garante que reprovisionar nunca desfaz trabalho feito.\n--\n-- Idempotente: substitui a função.\n\nCREATE OR REPLACE FUNCTION public.financial_recurring_provision(\n p_bill_id uuid DEFAULT NULL,\n p_through date DEFAULT NULL\n)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_through date := coalesce(p_through, (current_date + interval '12 months')::date);\n v_bill record;\n v_month date;\n v_due date;\n v_seq int;\n v_made int := 0;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_recurring_provision: sem tenant na sessao' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.create', NULL) THEN\n RAISE EXCEPTION 'financial_recurring_provision: exige financial.create' USING ERRCODE = '42501';\n END IF;\n\n FOR v_bill IN\n SELECT * FROM public.plg_financial_recurring_bills\n WHERE tenant_id = v_tenant AND is_active\n AND (p_bill_id IS NULL OR id = p_bill_id)\n LOOP\n v_month := date_trunc('month', greatest(v_bill.starts_on, current_date))::date;\n\n WHILE v_month <= v_through LOOP\n v_due := least(\n (v_month + ((v_bill.due_day - 1) || ' days')::interval)::date,\n (v_month + interval '1 month - 1 day')::date);\n\n EXIT WHEN v_bill.ends_on IS NOT NULL AND v_due > v_bill.ends_on;\n\n -- O \\`seq\\` sai do MÊS e não de uma contagem: é o que faz reprovisionar\n -- cair sempre na mesma linha, em vez de criar uma segunda com outro\n -- número para o mesmo vencimento.\n v_seq := (extract(year from v_month)::int * 12) + extract(month from v_month)::int;\n\n INSERT INTO public.plg_financial_recurring_bill_occurrences (\n tenant_id, unit_id, recurring_bill_id, seq, competence_date, due_date, amount, status\n ) VALUES (\n v_tenant, v_bill.unit_id, v_bill.id, v_seq, v_month, v_due, v_bill.amount, 'provisioned'\n )\n ON CONFLICT (recurring_bill_id, seq) DO NOTHING;\n\n IF FOUND THEN v_made := v_made + 1; END IF;\n\n v_month := (v_month + CASE v_bill.frequency\n WHEN 'weekly' THEN interval '7 days'\n WHEN 'quarterly' THEN interval '3 months'\n WHEN 'yearly' THEN interval '1 year'\n ELSE interval '1 month'\n END)::date;\n END LOOP;\n END LOOP;\n\n RETURN jsonb_build_object('through', v_through, 'provisioned', v_made);\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_recurring_provision(uuid, date) IS\n 'Escreve os vencimentos futuros de uma régua (ou de todas) como ocorrências provisionadas. É o primeiro tempo da recorrência do V1; o segundo é financial_consolidate_occurrence.';\n\nREVOKE ALL ON FUNCTION public.financial_recurring_provision(uuid, date) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_recurring_provision(uuid, date) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_recurring_provision(uuid, date) TO service_role;\n`\n\nexport const MIGRATION_107_A_PREVISAO_COMECA_ONDE_A_REGUA_COMECA = `-- A previsão começa onde a RÉGUA começa, não onde o relógio está.\n--\n-- A 106 provisionava a partir de hoje: \\`greatest(starts_on, current_date)\\`.\n-- O efeito é que uma recorrência cadastrada com início em julho — a conta de\n-- luz que a casa paga desde julho — nasce sem julho, agosto e setembro. E são\n-- justamente esses que a pessoa procura: os três vencimentos que já passaram e\n-- ninguém lançou. Ela filtra por «últimos 90 dias», não vê nada, e conclui que\n-- a recorrência não funciona.\n--\n-- Previsão é sobre o CALENDÁRIO da régua, não sobre o instante em que alguém\n-- salvou. Uma previsão vencida continua sendo uma previsão: ela é exatamente a\n-- dívida que ainda não foi reconhecida, e é para isso que a lista serve.\n--\n-- ── O que NÃO pode virar previsão duas vezes ───────────────────────────────\n--\n-- \\`financial_recurring_run\\` pode ter criado a fatura daquele mês antes desta\n-- porta existir. Provisionar o passado sem olhar para isso poria, lado a lado,\n-- a conta lançada e uma previsão dela — a mesma dívida contada duas vezes na\n-- soma do rodapé. A chave \\`recurring:<régua>:<AAAA-MM>\\` é a mesma nas duas\n-- funções, então basta perguntar por ela.\n--\n-- Idempotente: substitui a função.\n\nCREATE OR REPLACE FUNCTION public.financial_recurring_provision(\n p_bill_id uuid DEFAULT NULL,\n p_through date DEFAULT NULL\n)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_through date := coalesce(p_through, (current_date + interval '12 months')::date);\n v_bill record;\n v_month date;\n v_due date;\n v_seq int;\n v_key text;\n v_made int := 0;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_recurring_provision: sem tenant na sessao' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.create', NULL) THEN\n RAISE EXCEPTION 'financial_recurring_provision: exige financial.create' USING ERRCODE = '42501';\n END IF;\n\n FOR v_bill IN\n SELECT * FROM public.plg_financial_recurring_bills\n WHERE tenant_id = v_tenant AND is_active\n AND (p_bill_id IS NULL OR id = p_bill_id)\n LOOP\n -- DESDE O COMEÇO DA RÉGUA. O que já venceu é o que mais importa ver.\n v_month := date_trunc('month', v_bill.starts_on)::date;\n\n WHILE v_month <= v_through LOOP\n v_due := least(\n (v_month + ((v_bill.due_day - 1) || ' days')::interval)::date,\n (v_month + interval '1 month - 1 day')::date);\n\n EXIT WHEN v_bill.ends_on IS NOT NULL AND v_due > v_bill.ends_on;\n\n v_seq := (extract(year from v_month)::int * 12) + extract(month from v_month)::int;\n v_key := 'recurring:' || v_bill.id::text || ':' || to_char(v_month, 'YYYY-MM');\n\n -- O mês que já virou conta não vira previsão.\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_financial_invoices f\n WHERE f.tenant_id = v_tenant\n AND f.metadata ->> 'recurring_key' = v_key\n AND f.cancelled_at IS NULL\n ) THEN\n INSERT INTO public.plg_financial_recurring_bill_occurrences (\n tenant_id, unit_id, recurring_bill_id, seq, competence_date, due_date, amount, status\n ) VALUES (\n v_tenant, v_bill.unit_id, v_bill.id, v_seq, v_month, v_due, v_bill.amount, 'provisioned'\n )\n ON CONFLICT (recurring_bill_id, seq) DO NOTHING;\n\n IF FOUND THEN v_made := v_made + 1; END IF;\n END IF;\n\n v_month := (v_month + CASE v_bill.frequency\n WHEN 'weekly' THEN interval '7 days'\n WHEN 'quarterly' THEN interval '3 months'\n WHEN 'yearly' THEN interval '1 year'\n ELSE interval '1 month'\n END)::date;\n END LOOP;\n END LOOP;\n\n RETURN jsonb_build_object('through', v_through, 'provisioned', v_made);\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_recurring_provision(uuid, date) IS\n 'Escreve os vencimentos da régua — do início dela até o horizonte — como ocorrências provisionadas. O mês que já virou conta (mesma recurring_key) fica de fora.';\n\n-- ── O que já estava cadastrado ─────────────────────────────────────────────\n--\n-- As réguas de antes desta porta nasceram sem previsão nenhuma. O backfill vai\n-- em SQL puro e não pela função: a função pede \\`financial.create\\` do usuário da\n-- sessão, e numa migration não há usuário — forjar a permissão para contornar\n-- a própria guarda seria pior que não ter guarda.\n--\n-- Só MENSAL: é o que 100% das réguas usam hoje, e escrever o calendário das\n-- outras frequências em SQL de conjunto custaria mais do que vale — elas se\n-- provisionam pela função no próximo salvar.\n\nINSERT INTO public.plg_financial_recurring_bill_occurrences (\n tenant_id, unit_id, recurring_bill_id, seq, competence_date, due_date, amount, status\n)\nSELECT b.tenant_id, b.unit_id, b.id,\n (extract(year from m)::int * 12) + extract(month from m)::int,\n m::date,\n least((m + ((b.due_day - 1) || ' days')::interval)::date,\n (m + interval '1 month - 1 day')::date),\n b.amount,\n 'provisioned'\n FROM public.plg_financial_recurring_bills b\n CROSS JOIN LATERAL generate_series(\n date_trunc('month', b.starts_on),\n date_trunc('month', (current_date + interval '12 months')),\n interval '1 month') AS m\n WHERE b.is_active\n AND b.frequency = 'monthly'\n AND (b.ends_on IS NULL OR m::date <= b.ends_on)\n AND NOT EXISTS (\n SELECT 1 FROM public.plg_financial_invoices f\n WHERE f.tenant_id = b.tenant_id\n AND f.metadata ->> 'recurring_key' = 'recurring:' || b.id::text || ':' || to_char(m, 'YYYY-MM')\n AND f.cancelled_at IS NULL\n )\nON CONFLICT (recurring_bill_id, seq) DO NOTHING;\n`\n\nexport const MIGRATION_108_MUDOU_A_REGUA_MUDA_A_PREVISAO = `-- Mudou a régua, muda a previsão.\n--\n-- A recorrência do aluguel foi de R$ 2.400 para R$ 800 e a lista continuou\n-- mostrando 2.400 nas previsões: \\`financial_recurring_provision\\` só INSERE, e\n-- \\`ON CONFLICT DO NOTHING\\` deixa intacto o mês que já existia. A régua passou a\n-- dizer uma coisa e as previsões dela, outra.\n--\n-- Uma previsão é a régua projetada no calendário. Se a régua muda, o que ainda\n-- não virou conta tem de mudar junto — do contrário a soma do rodapé responde\n-- com um valor que não existe mais em lugar nenhum.\n--\n-- ── O que NÃO se mexe ──────────────────────────────────────────────────────\n--\n-- consolidada Já é conta. O valor dela é histórico, e reescrevê-lo seria\n-- mudar uma dívida reconhecida por um efeito colateral de\n-- editar a régua.\n-- \\`overridden\\` Alguém editou AQUELE vencimento à mão — o V1 deixa, e é o\n-- caso do mês em que a luz veio diferente. A régua não desfaz\n-- uma decisão que uma pessoa tomou sobre um mês específico.\n--\n-- A data acompanha pelo mesmo motivo: mudar o dia de vencimento da régua sem\n-- mover as previsões deixaria a lista prometendo o dia 10 para uma régua que\n-- agora diz 20.\n--\n-- Idempotente: substitui a função.\n\nCREATE OR REPLACE FUNCTION public.financial_recurring_provision(\n p_bill_id uuid DEFAULT NULL,\n p_through date DEFAULT NULL\n)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_through date := coalesce(p_through, (current_date + interval '12 months')::date);\n v_bill record;\n v_month date;\n v_due date;\n v_seq int;\n v_key text;\n v_made int := 0;\n v_sync int := 0;\n v_n int;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_recurring_provision: sem tenant na sessao' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.create', NULL) THEN\n RAISE EXCEPTION 'financial_recurring_provision: exige financial.create' USING ERRCODE = '42501';\n END IF;\n\n FOR v_bill IN\n SELECT * FROM public.plg_financial_recurring_bills\n WHERE tenant_id = v_tenant AND is_active\n AND (p_bill_id IS NULL OR id = p_bill_id)\n LOOP\n -- 1. O que já está previsto passa a dizer o que a régua diz agora.\n UPDATE public.plg_financial_recurring_bill_occurrences o\n SET amount = v_bill.amount,\n unit_id = v_bill.unit_id,\n due_date = least(\n (date_trunc('month', o.competence_date)\n + ((v_bill.due_day - 1) || ' days')::interval)::date,\n (date_trunc('month', o.competence_date)\n + interval '1 month - 1 day')::date),\n updated_at = now()\n WHERE o.recurring_bill_id = v_bill.id\n AND o.tenant_id = v_tenant\n AND o.status = 'provisioned'\n AND NOT coalesce(o.overridden, false)\n AND (o.amount IS DISTINCT FROM v_bill.amount\n OR o.unit_id IS DISTINCT FROM v_bill.unit_id\n OR o.due_date IS DISTINCT FROM least(\n (date_trunc('month', o.competence_date)\n + ((v_bill.due_day - 1) || ' days')::interval)::date,\n (date_trunc('month', o.competence_date)\n + interval '1 month - 1 day')::date));\n GET DIAGNOSTICS v_n = ROW_COUNT;\n v_sync := v_sync + v_n;\n\n -- 2. E os meses que ainda não existem entram.\n v_month := date_trunc('month', v_bill.starts_on)::date;\n\n WHILE v_month <= v_through LOOP\n v_due := least(\n (v_month + ((v_bill.due_day - 1) || ' days')::interval)::date,\n (v_month + interval '1 month - 1 day')::date);\n\n EXIT WHEN v_bill.ends_on IS NOT NULL AND v_due > v_bill.ends_on;\n\n v_seq := (extract(year from v_month)::int * 12) + extract(month from v_month)::int;\n v_key := 'recurring:' || v_bill.id::text || ':' || to_char(v_month, 'YYYY-MM');\n\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_financial_invoices f\n WHERE f.tenant_id = v_tenant\n AND f.metadata ->> 'recurring_key' = v_key\n AND f.cancelled_at IS NULL\n ) THEN\n INSERT INTO public.plg_financial_recurring_bill_occurrences (\n tenant_id, unit_id, recurring_bill_id, seq, competence_date, due_date, amount, status\n ) VALUES (\n v_tenant, v_bill.unit_id, v_bill.id, v_seq, v_month, v_due, v_bill.amount, 'provisioned'\n )\n ON CONFLICT (recurring_bill_id, seq) DO NOTHING;\n\n IF FOUND THEN v_made := v_made + 1; END IF;\n END IF;\n\n v_month := (v_month + CASE v_bill.frequency\n WHEN 'weekly' THEN interval '7 days'\n WHEN 'quarterly' THEN interval '3 months'\n WHEN 'yearly' THEN interval '1 year'\n ELSE interval '1 month'\n END)::date;\n END LOOP;\n END LOOP;\n\n RETURN jsonb_build_object('through', v_through, 'provisioned', v_made, 'synced', v_sync);\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_recurring_provision(uuid, date) IS\n 'Sincroniza as previsões com a régua (valor, unidade e vencimento) e escreve os meses que faltam. Não toca no que virou conta nem no vencimento editado à mão (overridden).';\n\n-- As previsões que ficaram para trás de uma régua já editada.\nUPDATE public.plg_financial_recurring_bill_occurrences o\n SET amount = b.amount,\n due_date = least(\n (date_trunc('month', o.competence_date) + ((b.due_day - 1) || ' days')::interval)::date,\n (date_trunc('month', o.competence_date) + interval '1 month - 1 day')::date),\n updated_at = now()\n FROM public.plg_financial_recurring_bills b\n WHERE b.id = o.recurring_bill_id\n AND o.status = 'provisioned'\n AND NOT coalesce(o.overridden, false)\n AND o.amount IS DISTINCT FROM b.amount;\n`\n\nexport const MIGRATION_109_A_REGUA_GRAVA_CAMELCASE_E_O_SQL_LIA_SNAKE = `-- A régua grava \\`unitPrice\\`; o SQL lia \\`unit_price\\`.\n--\n-- A conta de luz consolidada nasceu com total R$ 120,00 e a linha dela zerada.\n-- A causa é literal: \\`plg_financial_recurring_bills.items\\` é escrito pela TELA,\n-- em JSON de JavaScript — \\`unitPrice\\`, \\`itemKind\\`, \\`accountId\\` —, e as duas\n-- funções que copiam esses itens leem \\`unit_price\\`, \\`item_kind\\`, \\`account_id\\`.\n-- Nenhuma casa; \\`->>\\` devolve NULL; o \\`coalesce\\` cai em zero. Sem erro, sem\n-- log: a fatura fica com o total certo (que vem da régua, não das linhas) e com\n-- uma linha que diz zero.\n--\n-- \\`financial_recurring_run\\` tem o mesmo defeito desde que foi escrita — eu o\n-- copiei para \\`financial_consolidate_occurrence\\` ao espelhá-la. As duas são\n-- corrigidas aqui.\n--\n-- ── Por que ler as DUAS grafias ────────────────────────────────────────────\n--\n-- Padronizar o JSON numa só seria mais limpo e quebraria toda régua já\n-- cadastrada no instante em que rodasse. Ler as duas custa um \\`coalesce\\` por\n-- campo e não deixa ninguém para trás; quem vier depois escreve na grafia que\n-- quiser e as duas funcionam.\n--\n-- Idempotente: substitui as funções e recalcula só o que está zerado.\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 1. As linhas de uma régua, lidas nas duas grafias\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE OR REPLACE FUNCTION public.plg_financial_line_field(p_line jsonb, p_snake text, p_camel text)\nRETURNS text\nLANGUAGE sql\nIMMUTABLE\nSET search_path TO ''\nAS $$\n SELECT coalesce(nullif(p_line ->> p_snake, ''), nullif(p_line ->> p_camel, ''));\n$$;\n\nCOMMENT ON FUNCTION public.plg_financial_line_field(jsonb, text, text) IS\n 'Um campo da linha de uma régua de recorrência, na grafia do SQL ou na do JavaScript. A tela grava camelCase; as funções liam snake_case.';\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 2. Copiar as linhas passa a ser UMA função, usada pelas duas RPCs\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Estava duplicada — e foi por isso que o defeito viajou de uma para a outra.\n\nCREATE OR REPLACE FUNCTION public.plg_financial_copy_bill_items(\n p_tenant uuid, p_invoice uuid, p_items jsonb\n)\nRETURNS integer\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_line jsonb;\n v_item uuid;\n v_i int := 0;\n v_qty numeric;\n v_unit numeric;\n v_disc numeric;\n v_surc numeric;\n v_acc text;\n v_cc text;\nBEGIN\n FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_items, '[]'::jsonb))\n LOOP\n v_qty := coalesce(public.plg_financial_line_field(v_line, 'quantity', 'quantity')::numeric, 1);\n v_unit := coalesce(public.plg_financial_line_field(v_line, 'unit_price', 'unitPrice')::numeric, 0);\n v_disc := coalesce(public.plg_financial_line_field(v_line, 'discount', 'discount')::numeric, 0);\n v_surc := coalesce(public.plg_financial_line_field(v_line, 'surcharge', 'surcharge')::numeric, 0);\n v_acc := public.plg_financial_line_field(v_line, 'account_id', 'accountId');\n v_cc := public.plg_financial_line_field(v_line, 'cost_center_id', 'costCenterId');\n\n INSERT INTO public.items (\n tenant_id, invoice_id, name, quantity, unit_price, discount, total, sort_order, metadata\n ) VALUES (\n p_tenant, p_invoice,\n public.plg_financial_line_field(v_line, 'description', 'description'),\n v_qty, v_unit, v_disc,\n round(v_qty * v_unit - v_disc + v_surc, 2),\n v_i,\n jsonb_build_object(\n 'itemKind', coalesce(public.plg_financial_line_field(v_line, 'item_kind', 'itemKind'), 'other'),\n 'referenceId', public.plg_financial_line_field(v_line, 'reference_id', 'referenceId'))\n )\n RETURNING id INTO v_item;\n\n IF v_acc IS NOT NULL OR v_cc IS NOT NULL THEN\n INSERT INTO public.plg_financial_invoice_items (\n item_id, parent_kind, tenant_id, chart_account_id, cost_center_id\n ) VALUES (v_item, 'invoice', p_tenant, v_acc::uuid, v_cc::uuid)\n ON CONFLICT (item_id) DO UPDATE\n SET chart_account_id = excluded.chart_account_id,\n cost_center_id = excluded.cost_center_id;\n END IF;\n\n v_i := v_i + 1;\n END LOOP;\n\n RETURN v_i;\nEND $$;\n\nCOMMENT ON FUNCTION public.plg_financial_copy_bill_items(uuid, uuid, jsonb) IS\n 'Copia as linhas de uma régua de recorrência para a fatura gerada. Uma só cópia, usada pelo consolidar e pela rotina — duplicá-la foi como o defeito de grafia viajou entre as duas.';\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 3. As linhas zeradas que já foram criadas\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Só as de fatura gerada por recorrência, só as que estão em zero, e só\n-- enquanto nada foi recebido — o gatilho de imutabilidade continua valendo, e\n-- aqui ele não precisa sair do caminho: uma fatura de recorrência recém-gerada\n-- não tem pagamento.\n\nUPDATE public.items i\n SET unit_price = l.unit_price,\n quantity = l.quantity,\n total = round(l.quantity * l.unit_price, 2),\n name = coalesce(nullif(i.name, ''), l.nome),\n metadata = jsonb_set(coalesce(i.metadata, '{}'::jsonb), '{itemKind}', to_jsonb(l.kind), true),\n updated_at = now()\n FROM (\n SELECT f.id AS invoice_id,\n coalesce(public.plg_financial_line_field(li.value, 'quantity', 'quantity')::numeric, 1) AS quantity,\n coalesce(public.plg_financial_line_field(li.value, 'unit_price', 'unitPrice')::numeric, 0) AS unit_price,\n public.plg_financial_line_field(li.value, 'description', 'description') AS nome,\n coalesce(public.plg_financial_line_field(li.value, 'item_kind', 'itemKind'), 'other') AS kind,\n li.ordinality - 1 AS pos\n FROM public.plg_financial_invoices f\n JOIN public.plg_financial_recurring_bills b\n ON b.id = (f.metadata ->> 'recurring_bill_id')::uuid\n CROSS JOIN LATERAL jsonb_array_elements(coalesce(b.items, '[]'::jsonb)) WITH ORDINALITY AS li(value, ordinality)\n WHERE f.metadata ? 'recurring_bill_id'\n AND f.cancelled_at IS NULL\n ) AS l\n WHERE l.invoice_id = i.invoice_id\n AND l.pos = i.sort_order\n AND coalesce(i.unit_price, 0) = 0\n AND l.unit_price > 0\n AND NOT EXISTS (\n SELECT 1 FROM public.v_financial_installment_balances v\n WHERE v.invoice_id = i.invoice_id AND coalesce(v.paid, 0) > 0\n );\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 4. As duas RPCs passam a copiar pela função única\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE OR REPLACE FUNCTION public.financial_consolidate_occurrence(p_occurrence_id uuid)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_occ record;\n v_bill record;\n v_key text;\n v_res jsonb;\n v_invoice uuid;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_consolidate_occurrence: sem tenant na sessao' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.create', NULL) THEN\n RAISE EXCEPTION 'financial_consolidate_occurrence: exige financial.create' USING ERRCODE = '42501';\n END IF;\n\n SELECT * INTO v_occ FROM public.plg_financial_recurring_bill_occurrences\n WHERE id = p_occurrence_id AND tenant_id = v_tenant;\n IF v_occ IS NULL THEN\n RAISE EXCEPTION 'financial_consolidate_occurrence: ocorrencia % nao encontrada', p_occurrence_id\n USING ERRCODE = 'P0002';\n END IF;\n IF v_occ.status = 'consolidated' THEN\n RETURN jsonb_build_object('occurrence_id', v_occ.id, 'invoice_id', v_occ.invoice_id, 'created', false);\n END IF;\n IF v_occ.status = 'cancelled' THEN\n RAISE EXCEPTION 'financial_consolidate_occurrence: ocorrencia cancelada' USING ERRCODE = '23514';\n END IF;\n\n SELECT * INTO v_bill FROM public.plg_financial_recurring_bills\n WHERE id = v_occ.recurring_bill_id AND tenant_id = v_tenant;\n IF v_bill IS NULL THEN\n RAISE EXCEPTION 'financial_consolidate_occurrence: regua da ocorrencia % sumiu', p_occurrence_id\n USING ERRCODE = 'P0002';\n END IF;\n\n v_key := 'recurring:' || v_bill.id::text || ':'\n || to_char(coalesce(v_occ.competence_date, v_occ.due_date), 'YYYY-MM');\n\n SELECT f.id INTO v_invoice FROM public.plg_financial_invoices f\n WHERE f.tenant_id = v_tenant AND f.metadata ->> 'recurring_key' = v_key\n AND f.cancelled_at IS NULL LIMIT 1;\n\n IF v_invoice IS NULL THEN\n v_res := public.financial_create_invoice(\n v_bill.direction, v_occ.amount, v_bill.counterparty_person_id, NULL,\n coalesce(v_occ.unit_id, v_bill.unit_id),\n jsonb_build_array(jsonb_build_object('number', 1, 'due_date', v_occ.due_date, 'amount', v_occ.amount)),\n coalesce(v_occ.competence_date, v_occ.due_date),\n v_bill.description, coalesce(v_occ.notes, v_bill.notes),\n jsonb_build_object('source', 'financial_consolidate_occurrence',\n 'recurring_bill_id', v_bill.id,\n 'recurring_occurrence_id', v_occ.id,\n 'recurring_key', v_key),\n v_key);\n v_invoice := (v_res ->> 'invoice_id')::uuid;\n PERFORM public.plg_financial_copy_bill_items(v_tenant, v_invoice, v_bill.items);\n END IF;\n\n UPDATE public.plg_financial_recurring_bill_occurrences\n SET status = 'consolidated', invoice_id = v_invoice, updated_at = now()\n WHERE id = v_occ.id;\n\n RETURN jsonb_build_object('occurrence_id', v_occ.id, 'invoice_id', v_invoice, 'created', true);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.financial_recurring_run(p_through date DEFAULT NULL::date)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_through date := coalesce(p_through, (date_trunc('month', current_date) + interval '1 month - 1 day')::date);\n v_bill record; v_month date; v_due date; v_key text;\n v_made int := 0; v_total numeric := 0; v_res jsonb; v_invoice uuid;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_recurring_run: sem tenant na sessao' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('financial.create', NULL) THEN\n RAISE EXCEPTION 'financial_recurring_run: exige financial.create' USING ERRCODE = '42501';\n END IF;\n\n FOR v_bill IN\n SELECT * FROM public.plg_financial_recurring_bills\n WHERE tenant_id = v_tenant AND is_active AND starts_on <= v_through\n AND (ends_on IS NULL OR ends_on >= starts_on)\n LOOP\n v_month := date_trunc('month', greatest(v_bill.starts_on, date_trunc('month', v_through)::date))::date;\n WHILE v_month <= v_through LOOP\n v_due := least((v_month + ((v_bill.due_day - 1) || ' days')::interval)::date,\n (v_month + interval '1 month - 1 day')::date);\n EXIT WHEN v_bill.ends_on IS NOT NULL AND v_due > v_bill.ends_on;\n v_key := 'recurring:' || v_bill.id::text || ':' || to_char(v_month, 'YYYY-MM');\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_invoices f\n WHERE f.tenant_id = v_tenant\n AND f.metadata ->> 'recurring_key' = v_key\n AND f.cancelled_at IS NULL) THEN\n v_res := public.financial_create_invoice(\n v_bill.direction, v_bill.amount, v_bill.counterparty_person_id, NULL, v_bill.unit_id,\n jsonb_build_array(jsonb_build_object('number', 1, 'due_date', v_due, 'amount', v_bill.amount)),\n v_month, v_bill.description, v_bill.notes,\n jsonb_build_object('source', 'financial_recurring_run',\n 'recurring_bill_id', v_bill.id, 'recurring_key', v_key),\n v_key);\n v_invoice := (v_res ->> 'invoice_id')::uuid;\n PERFORM public.plg_financial_copy_bill_items(v_tenant, v_invoice, v_bill.items);\n v_made := v_made + 1;\n v_total := v_total + v_bill.amount;\n END IF;\n v_month := (v_month + CASE v_bill.frequency\n WHEN 'weekly' THEN interval '7 days'\n WHEN 'quarterly' THEN interval '3 months'\n WHEN 'yearly' THEN interval '1 year'\n ELSE interval '1 month' END)::date;\n END LOOP;\n END LOOP;\n\n RETURN jsonb_build_object('through', v_through, 'created', v_made, 'total', round(v_total, 2));\nEND $$;\n`\n\nexport const MIGRATION_110_TROCAR_DE_FORNECEDOR_ENQUANTO_NINGUEM_PAGOU = `-- Trocar de fornecedor enquanto ninguém pagou.\n--\n-- A trava é do BANCO: \\`trg_plg_financial_invoices_immutable\\` recusa qualquer\n-- mudança em \\`counterparty_person_id\\` a partir do instante em que a conta é\n-- emitida — mesmo que ela não tenha recebido um centavo.\n--\n-- Isso já foi revisto duas vezes para os vizinhos dela. A 098 abriu a\n-- competência e a 099 abriu o total, as duas com o MESMO limite: vale enquanto\n-- \\`sum(paid) = 0\\`. A contraparte ficou de fora — não por decisão, ao que tudo\n-- indica, mas porque ninguém tinha esbarrado nela.\n--\n-- E ela é o campo mais errado de todos na hora de lançar: quem digita seis\n-- contas seguidas escolhe o fornecedor da linha de cima na quarta. Hoje a\n-- correção é cancelar a conta e lançar outra — um documento cancelado no\n-- histórico para consertar um clique.\n--\n-- ── O que continua imutável, e por quê ─────────────────────────────────────\n--\n-- \\`number\\` É a identidade do documento. Trocá-lo é fazer o histórico\n-- falar de outra conta.\n-- \\`direction\\` Pagar e receber são lados opostos do razão; virar o sinal\n-- não é corrigir, é inverter o fato.\n-- \\`issue_date\\` A emissão é quando o documento passou a existir.\n-- \\`currency\\` O valor gravado só significa alguma coisa com ela.\n-- \\`order_id\\` O vínculo com a venda que a originou.\n--\n-- A contraparte entra no grupo que a 098 e a 099 já definiram: enquanto nada\n-- foi recebido, é rascunho; depois do primeiro centavo, é história.\n--\n-- Idempotente: substitui a função do gatilho.\n\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_immutable()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\nDECLARE v_paid numeric;\nBEGIN\n IF OLD.issue_date IS NULL OR OLD.cancelled_at IS NOT NULL THEN RETURN NEW; END IF;\n IF NEW.cancelled_at IS NOT NULL AND OLD.cancelled_at IS NULL THEN RETURN NEW; END IF;\n\n -- O que é identidade do documento não muda nunca.\n IF NEW.direction IS DISTINCT FROM OLD.direction\n OR NEW.order_id IS DISTINCT FROM OLD.order_id\n OR NEW.number IS DISTINCT FROM OLD.number\n OR NEW.issue_date IS DISTINCT FROM OLD.issue_date\n OR NEW.currency IS DISTINCT FROM OLD.currency THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % is issued and cannot be altered', OLD.number\n USING ERRCODE = '23514',\n HINT = 'A closed invoice is immutable (PRD 04). Correct it with a new invoice, or cancel this one through financial_cancel_invoice.';\n END IF;\n\n -- Competência (098), total (099) e CONTRAPARTE (110) têm a mesma licença e o\n -- mesmo limite. A pergunta só se faz quando um dos três mudou: ela varre as\n -- parcelas, e não vale pagá-la em toda gravação de observação.\n IF NEW.competence_date IS DISTINCT FROM OLD.competence_date\n OR NEW.total IS DISTINCT FROM OLD.total\n OR NEW.counterparty_person_id IS DISTINCT FROM OLD.counterparty_person_id THEN\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = OLD.id;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % has % already received and cannot be rewritten', OLD.number, v_paid\n USING ERRCODE = '23514',\n HINT = 'Reverse the payments first, or correct it with a new invoice.';\n END IF;\n END IF;\n\n RETURN NEW;\nEND $function$;\n\nCOMMENT ON FUNCTION public.trg_plg_financial_invoices_immutable() IS\n 'Uma conta emitida é imutável na IDENTIDADE (número, direção, emissão, moeda, pedido) sempre; e na competência, no total e na contraparte só depois do primeiro centavo recebido.';\n`\n\nexport const MIGRATION_111_REPARCELAR_SEM_APAGAR_A_PARCELA = `-- Reparcelar sem APAGAR a parcela.\n--\n-- Trocar o valor de uma conta cujo pagamento foi estornado falhava com:\n--\n-- plg_financial_movements is append-only: amount, kind, accounts and LINKS\n-- of movement … cannot change — post a reversal instead\n--\n-- São duas regras certas se atropelando:\n--\n-- \\`financial_reschedule_invoice\\` APAGA as parcelas e as reescreve — é como\n-- ele transforma 2x em 3x sem passar por um estado que \\`(invoice_id, number)\\`\n-- recusa.\n--\n-- \\`plg_financial_movements.financial_invoice_installment_id\\` é FK com ON\n-- DELETE SET NULL, e o movimento é append-only justamente nos LINKS.\n--\n-- O pagamento estornado continua existindo — é isso que um estorno significa:\n-- o par fica no livro, líquido em zero. Ele aponta para a parcela. Apagar a\n-- parcela manda o banco anular esse ponteiro, e o gatilho recusa — com razão,\n-- porque um lançamento que muda de dono depois de escrito deixa de ser\n-- histórico.\n--\n-- ── A correção ─────────────────────────────────────────────────────────────\n--\n-- A parcela de número N continua sendo a parcela de número N: ela é ATUALIZADA\n-- no lugar, e o id não muda. Só some a que sobrou quando o cronograma encolheu\n-- — e essa, se tiver movimento pendurado, o banco recusa, que é o certo.\n--\n-- De quebra, o que estava anexado à parcela deixa de precisar do resgate por\n-- número: não há mais o que resgatar, porque a linha nunca deixou de existir.\n--\n-- Idempotente: substitui a função.\n\nCREATE OR REPLACE FUNCTION public.financial_reschedule_invoice(\n p_invoice_id uuid,\n p_installments jsonb,\n p_idempotency_key text DEFAULT NULL::text\n)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_invoice public.plg_financial_invoices%ROWTYPE;\n v_paid numeric;\n v_mirror int;\n v_sum numeric := 0;\n v_i jsonb;\n v_n int := 0;\n v_out jsonb := '[]'::jsonb;\n v_inst_id uuid;\n v_due date;\n v_amount numeric;\n v_meta jsonb;\n v_total int;\nBEGIN\n SELECT * INTO v_invoice FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % not found', p_invoice_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_invoice.unit_id);\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n\n IF v_invoice.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % is cancelled', v_invoice.number USING ERRCODE = '55000';\n END IF;\n\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = p_invoice_id;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % has % already received; its schedule is history now', v_invoice.number, v_paid\n USING ERRCODE = '55000',\n HINT = 'Move the due date of an unpaid installment instead, or cancel and reissue.';\n END IF;\n\n -- O espelho legado (\\`bill\\`) é append-only: nem apaga nem muda de valor.\n SELECT count(*) INTO v_mirror FROM public.plg_financial_movements m\n WHERE m.financial_invoice_id = p_invoice_id AND m.movement_kind = 'bill' AND m.status <> 'cancelled';\n IF v_mirror > 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % carries legacy bill movements and cannot be rescheduled', v_invoice.number\n USING ERRCODE = '55000',\n HINT = 'Cancel and reissue this invoice.';\n END IF;\n\n IF jsonb_typeof(p_installments) <> 'array' OR jsonb_array_length(p_installments) = 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: installments must be a non-empty array' USING ERRCODE = '22023';\n END IF;\n FOR v_i IN SELECT * FROM jsonb_array_elements(p_installments) LOOP\n v_sum := v_sum + round(coalesce((v_i ->> 'amount')::numeric, 0), 2);\n END LOOP;\n IF v_sum <> round(v_invoice.total, 2) THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: installments sum to % but the invoice total is %', v_sum, v_invoice.total\n USING ERRCODE = '23514';\n END IF;\n\n v_total := jsonb_array_length(p_installments);\n\n -- 1. A sobra sai PRIMEIRO. Se ela tiver movimento, o banco recusa aqui — e é\n -- a recusa certa: encolher o cronograma apagaria a linha que um\n -- lançamento aponta.\n DELETE FROM public.plg_financial_invoice_installments\n WHERE invoice_id = p_invoice_id AND number > v_total;\n\n -- 2. E a parcela de número N continua sendo a de número N.\n FOR v_i IN SELECT * FROM jsonb_array_elements(p_installments) LOOP\n v_n := v_n + 1;\n v_due := coalesce((v_i ->> 'due_date')::date, v_invoice.competence_date, current_date);\n v_amount := round(coalesce((v_i ->> 'amount')::numeric, 0), 2);\n v_meta := CASE WHEN jsonb_typeof(v_i -> 'metadata') = 'object' THEN v_i -> 'metadata' ELSE NULL END;\n\n UPDATE public.plg_financial_invoice_installments\n SET due_date = v_due,\n amount = v_amount,\n metadata = coalesce(v_meta, metadata),\n updated_at = now()\n WHERE invoice_id = p_invoice_id AND number = v_n\n RETURNING id INTO v_inst_id;\n\n IF v_inst_id IS NULL THEN\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount, metadata)\n VALUES (v_tenant, v_invoice.unit_id, p_invoice_id, v_n, v_due, v_amount, coalesce(v_meta, '{}'::jsonb))\n RETURNING id INTO v_inst_id;\n END IF;\n\n v_out := v_out || jsonb_build_object('id', v_inst_id, 'number', v_n, 'due_date', v_due, 'amount', v_amount);\n END LOOP;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.reschedule', 'plg_financial_invoices', p_invoice_id::text,\n jsonb_build_object('number', v_invoice.number, 'installments', v_n, 'idempotency_key', p_idempotency_key));\n\n RETURN jsonb_build_object('invoice_id', p_invoice_id, 'installments', v_out);\nEND $function$;\n\nCOMMENT ON FUNCTION public.financial_reschedule_invoice(uuid, jsonb, text) IS\n 'Reescreve o cronograma ATUALIZANDO a parcela de cada número, sem apagá-la: um pagamento estornado continua apontando para ela, e o movimento é append-only nos links.';\n`\n\nexport const MIGRATION_112_A_LINHA_DO_EXTRATO_NAO_E_UM_LANCAMENTO_AINDA = `-- A linha do extrato não é um lançamento ainda.\n--\n-- ── O defeito, primeiro, porque é ele que decide o desenho ─────────────────\n--\n-- \\`importBankTransactions\\` (data/supabase.ts) insere a linha importada como\n-- MOVIMENTO, assim:\n--\n-- movement_kind: 'payment', amount, status: 'paid',\n-- external_id, external_source -- e nenhuma perna\n--\n-- e o baseline não deixa:\n--\n-- CONSTRAINT plg_financial_movements_legs_check CHECK (\n-- movement_kind IN ('bill','commission')\n-- OR metadata ? 'legacy_mirror'\n-- OR (debit_account_id IS NOT NULL AND credit_account_id IS NOT NULL\n-- AND debit_account_id <> credit_account_id))\n--\n-- Todo INSERT daquele caminho é recusado. Os testes do plugin passam porque\n-- exercitam o provider MOCK, que não tem constraint nenhuma — verde que não é\n-- verdade em lugar nenhum. É por isso que a Conciliação do V2 nunca listou uma\n-- transação: não havia como gravar uma.\n--\n-- ── E a constraint está CERTA ──────────────────────────────────────────────\n--\n-- Uma linha do extrato recém-chegada não tem duas pernas porque ainda não se\n-- sabe contra o quê ela lança. \"RSHOP POSTO STA C-001016, R$ 100,00, débito\"\n-- é evidência de que o dinheiro saiu do Itaú; a outra ponta — combustível?\n-- adiantamento? transferência para a conta da outra loja? — é justamente o que\n-- a conciliação existe para descobrir. Forçá-la a nascer com uma conta-ponte\n-- inventada é gravar um palpite com cara de fato, e é o palpite que depois\n-- ninguém consegue distinguir do lançamento de verdade.\n--\n-- Então a evidência ganha onde morar, e o movimento continua sendo o evento\n-- econômico. A view da 031 já trabalha exatamente nessa distinção:\n--\n-- «Economic cash events. Native movements always count; imported statement\n-- lines count only after standalone acceptance, never when matched.»\n--\n-- ── O que o V1 guarda, e por que quase tudo atravessa ─────────────────────\n--\n-- \\`ob_statement_transactions\\` tem 25 colunas e a tela do V1 mostra ou busca em\n-- quase todas. \\`payer_cpf_cnpj\\` é o que casa um PIX com o cliente; \\`category\\` e\n-- \\`transaction_type\\` são a classificação que o provedor já fez; \\`code\\` e\n-- \\`document\\` são o que o operador digita no campo de busca quando tem o\n-- comprovante na mão; \\`balance\\` é o saldo do banco NAQUELA linha, que é como se\n-- confere se o extrato bate. \\`raw_payload\\` fica porque o provedor muda de\n-- contrato sem avisar, e quem tem o cru consegue reprocessar.\n--\n-- \\`provider_deleted\\` não é enfeite: o Open Finance REMOVE lançamento (um PIX\n-- devolvido, um estorno do adquirente). A linha não pode sumir da tabela, ou o\n-- que já foi conciliado contra ela vira órfão sem explicação — ela é marcada, e\n-- a tela deixa de mostrá-la.\n--\n-- Idempotente.\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 1. A evidência\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_bank_statement_lines (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n -- A conta do extrato. \\`kind = 'bank'\\` na prática, mas sem CHECK: o V1 também\n -- traz fatura de cartão por este caminho (\\`statement_type = 'CREDIT_CARD'\\`).\n bank_account_id uuid NOT NULL REFERENCES public.plg_financial_accounts(id) ON DELETE CASCADE,\n\n -- Identidade do provedor. É o par que torna a importação idempotente, e é o\n -- mesmo contrato que \\`plg_financial_movements (external_source, external_id)\\`\n -- já usa — reimportar o mesmo período não duplica, que é o comportamento\n -- que o sync do V1 dá e sem o qual «Sincronizar agora» vira uma armadilha.\n external_source text NOT NULL,\n external_id text NOT NULL,\n\n posted_on date NOT NULL,\n -- Quando o provedor diz que a transação foi efetivada, se disser. Difere de\n -- \\`posted_on\\` em cartão e em TED de fim de semana.\n effective_at timestamptz,\n direction text NOT NULL CHECK (direction IN ('credit', 'debit')),\n -- Sempre POSITIVO. O sinal está em \\`direction\\`, e guardar o valor com sinal\n -- faria toda soma da tela depender de lembrar disso.\n amount numeric(14,2) NOT NULL CHECK (amount >= 0),\n -- O saldo do banco NESTA linha. É com ele que se confere se o extrato bate.\n balance_after numeric(14,2),\n\n description text,\n -- O nome que o provedor dá à contraparte ('itaubba', o nome do lojista).\n counterparty_name text,\n counterparty_document text,\n document text,\n code text,\n category text,\n transaction_type text,\n\n -- O provedor tirou a linha do extrato. Ela NÃO é apagada: o que já foi\n -- conciliado contra ela precisa continuar explicável.\n provider_deleted boolean NOT NULL DEFAULT false,\n -- O cru, porque o contrato do provedor muda sem avisar.\n raw_payload jsonb,\n\n -- A conciliação. \\`matched_movement_id\\` nulo com \\`reconciled_at\\` preenchido é\n -- a linha ACEITA COMO AVULSA — e é aí que ela vira o evento econômico.\n reconciled_at timestamptz,\n matched_movement_id uuid REFERENCES public.plg_financial_movements(id) ON DELETE SET NULL,\n\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n\n UNIQUE (tenant_id, external_source, external_id)\n);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 2. Unidade, índices, updated_at\n-- ═══════════════════════════════════════════════════════════════════════════\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_bank_statement_lines\n ADD CONSTRAINT plg_financial_bank_statement_lines_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\n-- A consulta da tela, na ordem em que ela filtra: conta, período, e a data\n-- descendente que é como o extrato se lê.\nCREATE INDEX IF NOT EXISTS plg_financial_bank_statement_lines_conta_periodo_idx\n ON public.plg_financial_bank_statement_lines (bank_account_id, posted_on DESC)\n WHERE provider_deleted = false;\n\n-- «Status: Pendente» é o filtro mais usado da tela, e é o que sobra depois de\n-- um mês de conciliação — o índice parcial cobre exatamente essa fila.\nCREATE INDEX IF NOT EXISTS plg_financial_bank_statement_lines_pendentes_idx\n ON public.plg_financial_bank_statement_lines (tenant_id, posted_on DESC)\n WHERE reconciled_at IS NULL AND provider_deleted = false;\n\nCREATE INDEX IF NOT EXISTS plg_financial_bank_statement_lines_movimento_idx\n ON public.plg_financial_bank_statement_lines (matched_movement_id)\n WHERE matched_movement_id IS NOT NULL;\n\nDO $$\nBEGIN\n CREATE TRIGGER plg_financial_bank_statement_lines_updated_at\n BEFORE UPDATE ON public.plg_financial_bank_statement_lines\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 3. A cerca, pelo template da casa\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Recurso próprio (\\`financial.bank_statement\\`) e não o do movimento: quem\n-- concilia não é necessariamente quem lança. No V1 a tela é do financeiro e do\n-- dono, e a recepção não a vê — um recurso separado é o que permite dizer isso\n-- sem abrir o razão inteiro junto.\n--\n-- \\`unit_optional\\`: a linha carrega a unidade da conta quando a conta tem uma, e\n-- uma linha sem unidade não pode sumir da conciliação por causa de campo vazio.\nSELECT app.scaffold_table('public.plg_financial_bank_statement_lines'::regclass,\n 'financial.bank_statement', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_bank_statement_lines'::regclass, 'unit_optional', true);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 4. O extrato como evento econômico\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- A 031 decidiu quando uma linha importada CONTA. Agora que a evidência saiu de\n-- \\`plg_financial_movements\\`, aquela regra vale aqui: a linha conta quando foi\n-- aceita como avulsa (\\`reconciled_at\\` preenchido, \\`matched_movement_id\\` nulo).\n-- Casada, quem conta é o movimento — a linha é só a prova de que ele aconteceu.\nCREATE OR REPLACE VIEW public.v_financial_bank_statement_unreconciled\nWITH (security_invoker = true) AS\nSELECT line.*\n FROM public.plg_financial_bank_statement_lines line\n WHERE line.reconciled_at IS NULL\n AND line.provider_deleted = false;\n\nCOMMENT ON VIEW public.v_financial_bank_statement_unreconciled IS\n 'Linhas do extrato que ainda não casaram com nada. A fila da Conciliação Bancária.';\n\nGRANT SELECT ON public.v_financial_bank_statement_unreconciled TO authenticated, service_role;\n`\n\nexport const MIGRATION_113_SO_A_CONTA_INTEGRADA_APARECE_NA_CONCILIACAO = `-- Só a conta INTEGRADA aparece na Conciliação.\n--\n-- ── O filtro que faltava ───────────────────────────────────────────────────\n--\n-- O V1 (\\`BankReconciliation.tsx:263\\`) monta o seletor assim:\n--\n-- .eq('active', true)\n-- .eq('openfinance_configured', true) ← este\n-- account_type IN ('conta_bancaria','cartao_credito_pagamento')\n-- + cartão exige openfinance_card_last4 com 4 dígitos\n--\n-- É por isso que lá aparece UM Itaú: é o único com Open Banking ligado. O V2\n-- listava toda conta bancária, cartão e caixa do plano — inclusive as que nunca\n-- vão trazer uma linha. Quem escolhe uma delas vê a lista vazia e conclui que a\n-- tela está quebrada; o defeito é o convite, não a lista.\n--\n-- ── Por que uma tabela, e não uma coluna ──────────────────────────────────\n--\n-- \\`openfinance_configured\\` é um booleano porque o V1 tem UM provedor. Aqui a\n-- conta pode ser servida por mais de um (o banco por Open Finance, o cartão\n-- pelo adquirente), e o vínculo carrega coisas que não são da conta nem do\n-- provedor sozinhos: como o provedor CHAMA aquela conta (\\`external_account_ref\\`\n-- — o hash da conta, os quatro últimos do cartão) e se a origem ainda está\n-- viva. Numa coluna, ligar o segundo provedor obriga a escolher qual dos dois\n-- fica.\n--\n-- O \\`connection_id\\` aponta para \\`connections\\` SEM \\`REFERENCES\\`: aquela tabela é\n-- de packages/db e o vínculo é opcional (uma origem pode ser semeada antes de\n-- existir conexão, e é assim que o porte prova a tela). A ausência da FK está\n-- anotada aqui porque ela é uma escolha, não um esquecimento.\n--\n-- Idempotente.\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_account_statement_sources (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n account_id uuid NOT NULL REFERENCES public.plg_financial_accounts(id) ON DELETE CASCADE,\n\n -- Quem traz o extrato desta conta: 'plugbank', 'inter', 'stone'…\n -- Sem CHECK: a lista de provedores é do mercado, não do schema, e uma\n -- constraint aqui derruba a conciliação no dia em que entrar o próximo.\n provider text NOT NULL,\n -- A conexão do conector, quando existe. Sem FK: ver o cabeçalho.\n connection_id uuid,\n -- Como o PROVEDOR chama esta conta. É o que casa a linha que ele devolve com\n -- a conta daqui — no V1 são \\`openfinance_account_hash\\` e\n -- \\`openfinance_card_last4\\`.\n external_account_ref text,\n\n statement_kind text NOT NULL DEFAULT 'bank'\n CHECK (statement_kind IN ('bank', 'credit_card')),\n card_last4 text,\n -- A regra do V1, escrita: extrato de cartão sem os quatro últimos não casa\n -- com nada, então ele não pode nem ser cadastrado.\n CONSTRAINT plg_financial_account_statement_sources_cartao_check\n CHECK (statement_kind <> 'credit_card' OR card_last4 ~ '^[0-9]{4}$'),\n\n -- Desligar a origem NÃO apaga a linha: o extrato que já entrou por ela\n -- continua precisando dizer de onde veio.\n is_active boolean NOT NULL DEFAULT true,\n last_synced_at timestamptz,\n last_sync_status text,\n last_sync_error text,\n\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_account_statement_sources\n ADD CONSTRAINT plg_financial_account_statement_sources_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\n-- Uma conta não se liga duas vezes ao MESMO provedor pela mesma referência\n-- externa. Índice e não constraint: \\`external_account_ref\\` é nulo enquanto a\n-- conexão não disse o nome dela, e \\`UNIQUE\\` deixaria passar N nulos.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_financial_account_statement_sources_unica_idx\n ON public.plg_financial_account_statement_sources\n (tenant_id, account_id, provider, coalesce(external_account_ref, ''));\n\n-- A pergunta que a tela faz: quais contas deste inquilino têm origem viva?\nCREATE INDEX IF NOT EXISTS plg_financial_account_statement_sources_ativas_idx\n ON public.plg_financial_account_statement_sources (tenant_id, account_id)\n WHERE is_active = true;\n\nDO $$\nBEGIN\n CREATE TRIGGER plg_financial_account_statement_sources_updated_at\n BEFORE UPDATE ON public.plg_financial_account_statement_sources\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\n-- Mesma cerca do extrato: quem pode ver a conciliação pode ver de onde ela vem.\nSELECT app.scaffold_table('public.plg_financial_account_statement_sources'::regclass,\n 'financial.bank_statement', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_account_statement_sources'::regclass, 'unit_optional', true);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- O seletor da Conciliação, como uma pergunta só\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Equivale ao \\`openfinance_configured = true\\` do V1, e resolve de quebra o que\n-- fazia a lista nascer vazia: \\`getBankAccounts\\` varria o plano INTEIRO ordenado\n-- por nome, e num inquilino com milhares de contas de pessoa o teto da resposta\n-- se enchia antes de chegar em «Itaú».\n-- DROP, then CREATE: the view selects \\`account.*\\`, and OR REPLACE refuses to\n-- shift columns when the table gains one (124 added \\`currency\\`), so a replay\n-- of this file failed on any pool that had moved on.\nDROP VIEW IF EXISTS public.v_financial_statement_accounts;\nCREATE VIEW public.v_financial_statement_accounts\nWITH (security_invoker = true) AS\nSELECT account.*,\n source.provider,\n source.statement_kind,\n source.card_last4,\n source.last_synced_at\n FROM public.plg_financial_accounts account\n JOIN public.plg_financial_account_statement_sources source\n ON source.account_id = account.id\n AND source.is_active = true\n WHERE account.is_active = true;\n\nCOMMENT ON VIEW public.v_financial_statement_accounts IS\n 'As contas que TÊM extrato: as que carregam uma origem ativa. É o seletor da Conciliação Bancária, e o equivalente ao openfinance_configured do V1.';\n\nGRANT SELECT ON public.v_financial_statement_accounts TO authenticated, service_role;\n`\n\nexport const MIGRATION_114_A_PARCELA_DIZ_POR_QUAL_CONTA_ELA_FOI_PAGA = `-- ============================================================================\n-- 114 — a parcela diz POR QUAL CONTA ela foi paga.\n--\n-- A lista do V1 recorta por \"Conta de pagamento\" / \"Conta de recebimento\", e\n-- aqui nao havia por onde: o vinculo parcela -> conta bancaria mora duas tabelas\n-- adiante (alocacao -> movimento), e perguntá-lo de fora custaria uma lista de\n-- ids do tamanho do historico da conta antes da primeira linha aparecer.\n--\n-- O LATERAL que soma as baixas ja visita exatamente essas duas tabelas, pelo\n-- indice (installment_id). A conta de pagamento sai da MESMA visita, como um\n-- agregado a mais — o plano da consulta nao muda, e o recorte vira um \\`cs\\`\n-- sobre um array em vez de um \\`in\\` sobre dez mil ids.\n--\n-- ARRAY e nao coluna: uma parcela quitada em partes pode ter saido de duas\n-- contas, e escolher uma delas para caber numa coluna e escolher esconder a\n-- outra de quem filtra por ela.\n--\n-- Idempotente.\n-- ============================================================================\n\nDROP VIEW IF EXISTS public.v_financial_invoice_lines_due;\n\nCREATE VIEW public.v_financial_invoice_lines_due AS\nSELECT\n i.id AS installment_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n t.direction,\n t.number,\n t.competence_date,\n t.issue_date,\n t.description,\n t.notes,\n t.metadata,\n t.order_id,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n i.number AS installment_number,\n it.total AS installment_count,\n i.due_date,\n i.amount,\n COALESCE(pa.paid, 0::numeric)::numeric(14,2) AS paid,\n -- De quais contas bancarias saiu (ou entrou) o dinheiro desta parcela.\n -- Vazio enquanto ninguem pagou — e vazio nunca satisfaz um \\`cs\\`, que e\n -- exatamente o que o filtro quer dizer.\n COALESCE(pa.payment_account_ids, ARRAY[]::uuid[]) AS payment_account_ids,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(i.amount - COALESCE(pa.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n -- Mesmas palavras de \\`v_financial_installment_balances\\`. Se as duas\n -- divergirem, a lista e o detalhe passam a dizer coisas diferentes sobre a\n -- mesma parcela.\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'\n WHEN COALESCE(pa.paid, 0::numeric) >= i.amount THEN 'paid'\n WHEN COALESCE(pa.paid, 0::numeric) > 0::numeric THEN 'partial'\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'\n ELSE 'pending'\n END AS status,\n i.cancelled_at,\n t.cancel_reason,\n t.created_by,\n i.created_at,\n i.updated_at\n FROM public.plg_financial_invoice_installments i\n JOIN public.plg_financial_invoices t ON t.id = i.invoice_id\n -- Correlacionado: so as baixas DESTA parcela, pelo indice (installment_id).\n LEFT JOIN LATERAL (\n SELECT sum(a.amount) AS paid,\n array_agg(DISTINCT m.bank_account_id)\n FILTER (WHERE m.bank_account_id IS NOT NULL) AS payment_account_ids\n FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = i.id\n AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL\n ) pa ON true\n -- \"3 de 4\": o denominador e do titulo, e sem ele a coluna de parcela mente.\n LEFT JOIN LATERAL (\n SELECT count(*) AS total\n FROM public.plg_financial_invoice_installments x\n WHERE x.invoice_id = i.invoice_id AND x.cancelled_at IS NULL\n ) it ON true\n LEFT JOIN public.people p ON p.id = t.counterparty_person_id;\n\nCOMMENT ON VIEW public.v_financial_invoice_lines_due IS\n 'Uma linha por PARCELA — a granularidade da lista de contas a pagar do V1, e '\n 'a unica em que \"o que vence no dia 11\" tem resposta. Filtra pelo indice '\n '(tenant_id, due_date) das parcelas: 0,15 s contra 7,5 s da forma por titulo. '\n 'payment_account_ids responde ao filtro \"conta de pagamento\" do V1 sem uma '\n 'segunda consulta.';\n\nALTER VIEW public.v_financial_invoice_lines_due SET (security_invoker = true);\nGRANT SELECT ON public.v_financial_invoice_lines_due TO authenticated, service_role;\n`\n\nexport const MIGRATION_115_A_REMESSA_APONTA_PARA_A_CONTA_QUE_EXISTE = `-- ---------------------------------------------------------------------------\n-- 115 — a remessa aponta para a conta que EXISTE.\n--\n-- \\`plg_financial_cnab_batches.bank_account_id\\` carrega DUAS chaves estrangeiras\n-- para a mesma coluna:\n--\n-- plg_financial_cnab_batches_account_fkey → plg_financial_bank_accounts\n-- plg_financial_cnab_batches_ledger_account_fkey → plg_financial_accounts\n--\n-- A segunda é a certa. A primeira aponta para a tabela LEGADA — aquela que a\n-- 021 aposentou e que hoje é só o encaminhamento por gatilho. Neste cluster ela\n-- tem 5 linhas contra 63 contas bancárias no razão, e o efeito é exato: gerar\n-- uma remessa por qualquer conta criada depois do porte morre com\n--\n-- violates foreign key constraint \"plg_financial_cnab_batches_account_fkey\"\n-- Key is not present in table \"plg_financial_bank_accounts\"\n--\n-- Uma coluna com duas FKs exige que o id esteja nas DUAS tabelas, e é por isso\n-- que a tela nunca conseguiu gravar: ela escreve onde \\`getBankAccounts\\` lê, que\n-- é o razão, e o legado nunca soube daquela conta.\n--\n-- A 080 trouxe a tabela do V1 com a FK que o V1 tinha. O que faltou foi\n-- aposentar a chave junto com a tabela a que ela se referia.\n--\n-- Derrubar a FK legada não perde nada: a do razão continua guardando a coluna, e\n-- é ela que diz a verdade. As 60 remessas já migradas apontam para contas que\n-- estão nas duas tabelas e seguem válidas.\n--\n-- Idempotente.\n-- ---------------------------------------------------------------------------\n\nALTER TABLE public.plg_financial_cnab_batches\n DROP CONSTRAINT IF EXISTS plg_financial_cnab_batches_account_fkey;\n\n-- A do razão pode não existir num pool que recebeu a 080 e não a correção; é\n-- ela que fica, então se faltar, nasce aqui.\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conrelid = 'public.plg_financial_cnab_batches'::regclass\n AND conname = 'plg_financial_cnab_batches_ledger_account_fkey'\n ) THEN\n ALTER TABLE public.plg_financial_cnab_batches\n ADD CONSTRAINT plg_financial_cnab_batches_ledger_account_fkey\n FOREIGN KEY (bank_account_id)\n REFERENCES public.plg_financial_accounts(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\nCOMMENT ON COLUMN public.plg_financial_cnab_batches.bank_account_id IS\n 'A conta do RAZÃO que o arquivo debita — plg_financial_accounts, a mesma que getBankAccounts lê. Não é a tabela legada plg_financial_bank_accounts: uma conta criada depois do porte só existe no razão.';\n\n-- ── a parcela que não tem movimento gêmeo ──────────────────────────────────\n--\n-- \\`plg_financial_cnab_items.movement_id\\` aponta para \\`plg_financial_movements\\`.\n-- A tela manda a PARCELA, e o porte deixou parcela e movimento com o mesmo id\n-- na maioria das linhas — mas não em todas: aqui são 171.978 parcelas contra\n-- 143.175 com movimento de mesmo id. As outras 28.803 fariam a remessa morrer\n-- na hora de arquivar um arquivo que JÁ foi baixado e já foi para o banco.\n--\n-- O item passa a aceitar a parcela em coluna própria. \\`movement_id\\` continua\n-- sendo o elo com o razão onde ele existe; \\`installment_id\\` é o que a tela\n-- sempre teve na mão, e é por ele que «esta conta já foi numa remessa» tem\n-- resposta.\nALTER TABLE public.plg_financial_cnab_items\n ADD COLUMN IF NOT EXISTS installment_id uuid;\n\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conrelid = 'public.plg_financial_cnab_items'::regclass\n AND conname = 'plg_financial_cnab_items_installment_fkey'\n ) THEN\n ALTER TABLE public.plg_financial_cnab_items\n ADD CONSTRAINT plg_financial_cnab_items_installment_fkey\n FOREIGN KEY (installment_id)\n REFERENCES public.plg_financial_invoice_installments(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\n-- As linhas já migradas: onde o movimento tem parcela de mesmo id, o elo novo\n-- se preenche sozinho. O que não casar fica nulo, que é a verdade.\nUPDATE public.plg_financial_cnab_items ci\n SET installment_id = ci.movement_id\n WHERE ci.installment_id IS NULL\n AND ci.movement_id IS NOT NULL\n AND EXISTS (\n SELECT 1 FROM public.plg_financial_invoice_installments i WHERE i.id = ci.movement_id\n );\n\nCREATE INDEX IF NOT EXISTS plg_financial_cnab_items_installment_idx\n ON public.plg_financial_cnab_items (tenant_id, installment_id)\n WHERE installment_id IS NOT NULL;\n\nCOMMENT ON COLUMN public.plg_financial_cnab_items.installment_id IS\n 'A PARCELA que esta linha da remessa paga — é o que a tela de contas tem na mão. movement_id continua apontando para o razão onde o movimento gêmeo existe.';\n\n-- ── uma conta padrão por favorecido, sem derrubar a remessa ────────────────\n--\n-- \\`payee_accounts_one_default_uidx\\` é único em (tenant_id, owner_type, owner_id)\n-- WHERE is_default, e está certo: duas contas padrão fazem a tela mostrar uma e\n-- a remessa pagar a outra.\n--\n-- O que estava errado era quem escrevia: a remessa inseria sempre com\n-- \\`is_default = true\\`, então a SEGUNDA tentativa de gerar o mesmo arquivo —\n-- depois de a primeira morrer na FK acima — batia em\n--\n-- duplicate key value violates unique constraint\n--\n-- O conserto é do lado de quem escreve (\\`saveCnabPayees\\` procura antes de\n-- inserir), e esta função é a rede: um \\`insert\\` que já tem padrão vira o padrão\n-- da pessoa sem precisar que o chamador saiba disso.\nCREATE OR REPLACE FUNCTION public.trg_payee_accounts_one_default()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY INVOKER\nSET search_path = public, pg_catalog\nAS $$\nBEGIN\n IF NEW.is_default THEN\n UPDATE public.payee_accounts\n SET is_default = false\n WHERE tenant_id = NEW.tenant_id\n AND owner_type = NEW.owner_type\n AND owner_id = NEW.owner_id\n AND is_default\n AND id <> NEW.id;\n END IF;\n RETURN NEW;\nEND\n$$;\n\nDROP TRIGGER IF EXISTS payee_accounts_one_default ON public.payee_accounts;\nCREATE TRIGGER payee_accounts_one_default\n BEFORE INSERT OR UPDATE OF is_default ON public.payee_accounts\n FOR EACH ROW EXECUTE FUNCTION public.trg_payee_accounts_one_default();\n`\n\nexport const MIGRATION_116_UM_ESTORNO_NAO_SE_ESTORNA = `-- ---------------------------------------------------------------------------\n-- 116 — um estorno não se estorna.\n--\n-- \\`financial_reverse_movement\\` aceitava qualquer \\`payment\\`, inclusive a linha\n-- que ela mesma lança como contrapartida. A tela mirava «o último pagamento da\n-- parcela», que depois do primeiro estorno É o estorno: cada clique estornava o\n-- estorno anterior, o dinheiro ia e voltava, e o pagamento vivo continuava lá —\n-- a conta seguia «paga» para sempre. Agora o banco recusa com uma mensagem que\n-- a tela mostra.\n--\n-- Idempotente: só repõe a função.\n-- ---------------------------------------------------------------------------\n\nCREATE OR REPLACE FUNCTION public.financial_reverse_movement(p_movement_id uuid, p_reason text, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_m public.plg_financial_movements%ROWTYPE;\n v_rev uuid;\nBEGIN\n IF nullif(btrim(coalesce(p_reason, '')), '') IS NULL THEN\n RAISE EXCEPTION 'financial_reverse_movement: a reason is required' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_m FROM public.plg_financial_movements WHERE id = p_movement_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_reverse_movement: movement % not found', p_movement_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_m.unit_id);\n IF v_m.movement_kind NOT IN ('payment', 'transfer') OR v_m.debit_account_id IS NULL THEN\n RAISE EXCEPTION 'financial_reverse_movement: % is not a money event', p_movement_id USING ERRCODE = '55000';\n END IF;\n IF v_m.reverses_id IS NOT NULL THEN\n RAISE EXCEPTION 'Este lançamento já é um estorno e não pode ser estornado.' USING ERRCODE = '55000';\n END IF;\n IF v_m.reversed_by_id IS NOT NULL THEN\n RETURN jsonb_build_object('reversal_id', v_m.reversed_by_id, 'movement_id', v_m.id, 'replayed', true);\n END IF;\n IF p_idempotency_key IS NOT NULL THEN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT m.id INTO v_rev FROM public.plg_financial_movements m WHERE m.tenant_id = v_tenant AND m.idempotency_key = p_idempotency_key;\n IF v_rev IS NOT NULL THEN\n RETURN jsonb_build_object('reversal_id', v_rev, 'movement_id', v_m.id, 'replayed', true);\n END IF;\n END IF;\n\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, invoice_id, financial_invoice_id, financial_invoice_installment_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, method, cash_session_id, bank_account_id, reason, notes, metadata,\n idempotency_key, created_by, reverses_id, debit_account_id, credit_account_id)\n VALUES\n (v_m.tenant_id, v_m.unit_id, v_m.invoice_id, v_m.financial_invoice_id, v_m.financial_invoice_installment_id,\n CASE WHEN v_m.direction = 'credit' THEN 'debit' ELSE 'credit' END, v_m.movement_kind, v_m.amount, v_m.amount, 'paid',\n current_date, current_date, v_m.installment_number, v_m.method, v_m.cash_session_id, v_m.bank_account_id, btrim(p_reason),\n 'Estorno de ' || v_m.id::text,\n jsonb_build_object('source', 'financial_reverse_movement', 'reversal_of', v_m.id, 'reason', btrim(p_reason)),\n p_idempotency_key, auth.uid(), v_m.id, v_m.credit_account_id, v_m.debit_account_id)\n RETURNING id INTO v_rev;\n\n UPDATE public.plg_financial_movements SET reversed_by_id = v_rev, reason = coalesce(reason, btrim(p_reason)), updated_at = now()\n WHERE id = v_m.id;\n IF v_m.financial_invoice_installment_id IS NOT NULL THEN PERFORM public.plg_financial_sync_mirror(v_m.financial_invoice_installment_id); END IF;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.movement.reverse', 'plg_financial_movements', v_m.id::text,\n jsonb_build_object('reversal_id', v_rev, 'amount', v_m.amount, 'reason', btrim(p_reason), 'idempotency_key', p_idempotency_key));\n RETURN jsonb_build_object('reversal_id', v_rev, 'movement_id', v_m.id, 'replayed', false);\nEND $$;\n\n`\n\nexport const MIGRATION_117_COPYING_LINES_IS_AN_INTERNAL_STEP = `-- ---------------------------------------------------------------------------\n-- 117 — copying lines onto an invoice is an internal step, not a door.\n--\n-- Both copiers are SECURITY DEFINER with no caller check and were executable by\n-- PUBLIC (so anon too): anyone holding an invoice id could write public.items\n-- into it. Every real caller is another SECURITY DEFINER function owned by\n-- postgres (fn_invoice_from_order_internal, order_raise_receivable,\n-- financial_recurring_run, financial_consolidate_occurrence), which keeps its\n-- right to execute as owner. So the fix is the grant, not the body.\n--\n-- Idempotent: grants only.\n-- ---------------------------------------------------------------------------\n\nREVOKE ALL ON FUNCTION public.plg_financial_copy_bill_items(uuid, uuid, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_financial_copy_bill_items(uuid, uuid, jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_copy_order_lines_to_invoice(uuid, uuid) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_financial_copy_order_lines_to_invoice(uuid, uuid) TO service_role;\n`\n\nexport const MIGRATION_118_GIFT_CARD_FOUND_BY_EXACT_CODE = `-- 118_gift_card_found_by_exact_code.sql\n--\n-- Selling a gift card on a receivable takes the code typed at the counter and\n-- must match exactly one card. financial_gift_cards (027) matches by substring,\n-- so a partial code answered with real codes of other cards. This lookup\n-- returns the one card whose code equals the input (spaces, dots, dashes and\n-- case ignored), or nothing.\n--\n-- The shop table is optional and applied after this chain, so it is resolved\n-- at run time, as in 027.\n\nCREATE OR REPLACE FUNCTION public.financial_gift_card_by_code(p_code text)\nRETURNS TABLE (\n id uuid,\n code text,\n face_value numeric,\n balance numeric,\n balance_known boolean,\n state text,\n currency text,\n issued_at timestamptz,\n expires_at timestamptz,\n activated_at timestamptz,\n note text,\n customer_email text\n)\n LANGUAGE plpgsql\n STABLE\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_code text := upper(regexp_replace(coalesce(p_code, ''), '[\\\\s.\\\\-]', '', 'g'));\n v_activated text;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_gift_card_by_code: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n -- Whoever can raise a receivable can sell a card on it.\n IF NOT app.has_permission('financial.create', NULL) THEN\n RAISE EXCEPTION 'financial_gift_card_by_code: exige financial.create' USING ERRCODE = '42501';\n END IF;\n IF v_code = '' OR to_regclass('public.plg_shop_gift_cards') IS NULL THEN\n RETURN;\n END IF;\n\n IF EXISTS (SELECT 1 FROM information_schema.columns c\n WHERE c.table_schema = 'public' AND c.table_name = 'plg_shop_gift_cards'\n AND c.column_name = 'activated_at') THEN\n v_activated := 'g.activated_at';\n ELSE\n v_activated :=\n 'CASE WHEN g.status = ''disabled'' AND g.balance = 0 THEN NULL'\n || ' WHEN g.updated_at > g.created_at THEN g.updated_at'\n || ' ELSE g.created_at END';\n END IF;\n\n -- Same state vocabulary as financial_gift_cards (027).\n RETURN QUERY EXECUTE\n 'SELECT r.id, r.code, r.initial_balance,'\n || 'CASE WHEN r.never THEN NULL ELSE r.balance END,'\n || '(NOT r.never),'\n || 'CASE WHEN r.never THEN ''issued'''\n || ' WHEN r.balance = 0 OR r.status = ''redeemed'' THEN ''redeemed'''\n || ' WHEN r.expires_at IS NOT NULL AND r.expires_at < now() THEN ''expired'''\n || ' WHEN r.status = ''disabled'' THEN ''revoked'''\n || ' ELSE ''active'' END,'\n || 'r.currency, r.created_at, r.expires_at, r.activated_at, r.note, r.customer_email '\n || 'FROM (SELECT g.id, g.code, g.initial_balance, g.balance, g.currency, g.status,'\n || 'g.created_at, g.expires_at, g.note, g.customer_email,'\n || '(g.status = ''disabled'' AND g.balance = 0) AS never,'\n || v_activated || ' AS activated_at '\n || 'FROM public.plg_shop_gift_cards g '\n || 'WHERE g.tenant_id = $1 AND upper(g.code) = $2) r '\n || 'LIMIT 1'\n USING v_tenant, v_code;\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_gift_card_by_code(text) IS\n 'The one gift card whose code equals the input (spaces/case ignored), for selling it on a receivable. Never matches part of a code (118).';\n\nREVOKE ALL ON FUNCTION public.financial_gift_card_by_code(text) FROM PUBLIC;\nREVOKE ALL ON FUNCTION public.financial_gift_card_by_code(text) FROM anon;\nGRANT EXECUTE ON FUNCTION public.financial_gift_card_by_code(text) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_gift_card_by_code(text) TO service_role;\n`\n\nexport const MIGRATION_119_AN_UNPAID_LEGACY_BILL_CAN_BE_RESCHEDULED = `-- An unpaid bill migrated from V1 could not be rescheduled.\n--\n-- Editing the lines of such a bill saved the new lines and total, then\n-- \\`financial_reschedule_invoice\\` refused with \"carries legacy bill movements\":\n-- the installment kept the old amount and every later save failed the same way.\n--\n-- The \\`bill\\` mirror in plg_financial_movements has had no reader since 044 —\n-- the installment is the obligation. Its amount is append-only, its status is\n-- not, so an unpaid mirror is cancelled (stamped in metadata) and the schedule\n-- goes through. A mirror that records a payment still blocks.\n--\n-- Idempotent: replaces the function.\n\nCREATE OR REPLACE FUNCTION public.financial_reschedule_invoice(\n p_invoice_id uuid,\n p_installments jsonb,\n p_idempotency_key text DEFAULT NULL::text\n)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_invoice public.plg_financial_invoices%ROWTYPE;\n v_paid numeric;\n v_mirror int;\n v_sum numeric := 0;\n v_i jsonb;\n v_n int := 0;\n v_out jsonb := '[]'::jsonb;\n v_inst_id uuid;\n v_due date;\n v_amount numeric;\n v_meta jsonb;\n v_total int;\nBEGIN\n SELECT * INTO v_invoice FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % not found', p_invoice_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_invoice.unit_id);\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n\n IF v_invoice.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % is cancelled', v_invoice.number USING ERRCODE = '55000';\n END IF;\n\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = p_invoice_id;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % has % already received; its schedule is history now', v_invoice.number, v_paid\n USING ERRCODE = '55000',\n HINT = 'Move the due date of an unpaid installment instead, or cancel and reissue.';\n END IF;\n\n -- The legacy \\`bill\\` mirror can't change amount, but nothing reads it any more\n -- (the installment is the obligation since 044) and its status may move.\n -- Retire it unless it records a payment the installments don't know about.\n SELECT count(*) INTO v_mirror FROM public.plg_financial_movements m\n WHERE m.financial_invoice_id = p_invoice_id AND m.movement_kind = 'bill'\n AND m.status <> 'cancelled' AND coalesce(m.paid_amount, 0) > 0;\n IF v_mirror > 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % carries a paid legacy bill movement and cannot be rescheduled', v_invoice.number\n USING ERRCODE = '55000',\n HINT = 'Cancel and reissue this invoice.';\n END IF;\n UPDATE public.plg_financial_movements m\n SET status = 'cancelled', updated_at = now(),\n metadata = coalesce(m.metadata, '{}'::jsonb)\n || jsonb_build_object('retired_by', 'financial_reschedule_invoice', 'retired_at', now())\n WHERE m.financial_invoice_id = p_invoice_id AND m.movement_kind = 'bill' AND m.status <> 'cancelled';\n\n IF jsonb_typeof(p_installments) <> 'array' OR jsonb_array_length(p_installments) = 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: installments must be a non-empty array' USING ERRCODE = '22023';\n END IF;\n FOR v_i IN SELECT * FROM jsonb_array_elements(p_installments) LOOP\n v_sum := v_sum + round(coalesce((v_i ->> 'amount')::numeric, 0), 2);\n END LOOP;\n IF v_sum <> round(v_invoice.total, 2) THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: installments sum to % but the invoice total is %', v_sum, v_invoice.total\n USING ERRCODE = '23514';\n END IF;\n\n v_total := jsonb_array_length(p_installments);\n\n -- 1. A sobra sai PRIMEIRO. Se ela tiver movimento, o banco recusa aqui — e é\n -- a recusa certa: encolher o cronograma apagaria a linha que um\n -- lançamento aponta.\n DELETE FROM public.plg_financial_invoice_installments\n WHERE invoice_id = p_invoice_id AND number > v_total;\n\n -- 2. E a parcela de número N continua sendo a de número N.\n FOR v_i IN SELECT * FROM jsonb_array_elements(p_installments) LOOP\n v_n := v_n + 1;\n v_due := coalesce((v_i ->> 'due_date')::date, v_invoice.competence_date, current_date);\n v_amount := round(coalesce((v_i ->> 'amount')::numeric, 0), 2);\n v_meta := CASE WHEN jsonb_typeof(v_i -> 'metadata') = 'object' THEN v_i -> 'metadata' ELSE NULL END;\n\n UPDATE public.plg_financial_invoice_installments\n SET due_date = v_due,\n amount = v_amount,\n metadata = coalesce(v_meta, metadata),\n updated_at = now()\n WHERE invoice_id = p_invoice_id AND number = v_n\n RETURNING id INTO v_inst_id;\n\n IF v_inst_id IS NULL THEN\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount, metadata)\n VALUES (v_tenant, v_invoice.unit_id, p_invoice_id, v_n, v_due, v_amount, coalesce(v_meta, '{}'::jsonb))\n RETURNING id INTO v_inst_id;\n END IF;\n\n v_out := v_out || jsonb_build_object('id', v_inst_id, 'number', v_n, 'due_date', v_due, 'amount', v_amount);\n END LOOP;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.reschedule', 'plg_financial_invoices', p_invoice_id::text,\n jsonb_build_object('number', v_invoice.number, 'installments', v_n, 'idempotency_key', p_idempotency_key));\n\n RETURN jsonb_build_object('invoice_id', p_invoice_id, 'installments', v_out);\nEND $function$;\n\nCOMMENT ON FUNCTION public.financial_reschedule_invoice(uuid, jsonb, text) IS\n 'Rewrites the schedule by UPDATING each numbered installment in place, and retires an unpaid legacy bill mirror instead of refusing (119).';\n`\n\nexport const MIGRATION_120_CANCELAR_E_POR_ITEM_COM_MOTIVO_E_DESTINO = `-- Cancelar uma conta é escolher O QUE se cancela, POR QUÊ e o que acontece\n-- com a mercadoria — e o que já foi pago vira crédito, não some.\n--\n-- Contrato: V1, \\`financeiro/contas-a-pagar-cancelamento/FLOW.md\\` (09-10-2026).\n-- Até aqui o V2 cancelava só a conta inteira, com motivo em texto livre, e\n-- RECUSAVA conta com pagamento. O V1 faz quatro coisas que este arquivo traz:\n--\n-- 1. cancela por ITEM — corta a parte não paga das parcelas, da última para\n-- trás; só depois libera o pago, do pagamento mais recente para trás;\n-- 2. o motivo é uma LISTA da casa (Outros, Solicitado pelo cliente,\n-- Solicitado pelo profissional) + um detalhe;\n-- 3. o que já foi pago vira CRÉDITO da contraparte (fornecedor ou cliente);\n-- 4. mercadoria que entrou no estoque por aquela nota pede destino:\n-- devolver ao fornecedor / baixar como perda / manter.\n--\n-- E um defeito que estava esperando por isto: pagar uma conta a pagar com\n-- «crédito» conferia o saldo com o sinal do CLIENTE. Crédito de fornecedor é o\n-- fornecedor nos devendo — débito na conta da pessoa — e só conta o que veio\n-- de um cancelamento (ver §8).\n--\n-- Idempotente.\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 1. Os motivos\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_cancellation_reasons (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n name text NOT NULL CHECK (btrim(name) <> ''),\n is_active boolean NOT NULL DEFAULT true,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nCREATE UNIQUE INDEX IF NOT EXISTS plg_financial_cancellation_reasons_name_uq\n ON public.plg_financial_cancellation_reasons (tenant_id, lower(btrim(name)));\n\nDO $$\nBEGIN\n CREATE TRIGGER plg_financial_cancellation_reasons_updated_at\n BEFORE UPDATE ON public.plg_financial_cancellation_reasons\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\nSELECT app.scaffold_table('public.plg_financial_cancellation_reasons'::regclass,\n 'financial.invoice', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_cancellation_reasons'::regclass, 'tenant', true);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 2. O registro de cada cancelamento\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_cancellations (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n invoice_id uuid NOT NULL REFERENCES public.plg_financial_invoices(id) ON DELETE CASCADE,\n whole boolean NOT NULL,\n item_ids uuid[] NOT NULL DEFAULT '{}',\n reason_id uuid REFERENCES public.plg_financial_cancellation_reasons(id) ON DELETE SET NULL,\n reason text NOT NULL,\n amount numeric(14,2) NOT NULL,\n credit_amount numeric(14,2) NOT NULL DEFAULT 0,\n credit_movement_id uuid REFERENCES public.plg_financial_movements(id) ON DELETE SET NULL,\n -- [{item_id, product_id, destination, quantity, note, operation_ids}]\n stock jsonb NOT NULL DEFAULT '[]'::jsonb,\n idempotency_key text,\n created_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nCREATE INDEX IF NOT EXISTS plg_financial_cancellations_invoice_idx\n ON public.plg_financial_cancellations (invoice_id);\nCREATE UNIQUE INDEX IF NOT EXISTS plg_financial_cancellations_key_uq\n ON public.plg_financial_cancellations (tenant_id, idempotency_key) WHERE idempotency_key IS NOT NULL;\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_cancellations\n ADD CONSTRAINT plg_financial_cancellations_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\nDO $$\nBEGIN\n CREATE TRIGGER plg_financial_cancellations_updated_at\n BEFORE UPDATE ON public.plg_financial_cancellations\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\nSELECT app.scaffold_table('public.plg_financial_cancellations'::regclass,\n 'financial.invoice', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_cancellations'::regclass, 'unit_optional', true);\n\nALTER TABLE public.plg_financial_invoices\n ADD COLUMN IF NOT EXISTS cancel_reason_id uuid REFERENCES public.plg_financial_cancellation_reasons(id) ON DELETE SET NULL;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 3. A linha diz que foi cancelada\n-- ═══════════════════════════════════════════════════════════════════════════\n-- Colunas novas só no FIM: \\`CREATE OR REPLACE VIEW\\` não aceita outra ordem.\n\nCREATE OR REPLACE VIEW public.v_financial_invoice_lines AS\nSELECT i.id AS item_id, i.tenant_id, i.unit_id, i.invoice_id, f.number AS invoice_number,\n f.direction, f.issue_date, f.cancelled_at, i.product_id, i.name, i.description,\n i.quantity, i.unit_price, i.discount, i.total,\n COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,\n COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,\n x.tax_amount, x.cfop, x.ncm,\n a.from_item_id AS billed_from_order_item_id, a.quantity AS billed_quantity,\n i.cancelled_at AS item_cancelled_at, i.cancel_reason AS item_cancel_reason, i.metadata AS item_metadata\n FROM public.items i\n JOIN public.plg_financial_invoices f ON f.id = i.invoice_id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n LEFT JOIN public.item_associations a ON a.to_item_id = i.id\n WHERE i.invoice_id IS NOT NULL\nUNION ALL\nSELECT i.id AS item_id, i.tenant_id, i.unit_id, f.id AS invoice_id, f.number AS invoice_number,\n f.direction, f.issue_date, f.cancelled_at, i.product_id, i.name, i.description,\n i.quantity, i.unit_price, i.discount, i.total,\n COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,\n COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,\n x.tax_amount, x.cfop, x.ncm,\n a.from_item_id AS billed_from_order_item_id, a.quantity AS billed_quantity,\n i.cancelled_at AS item_cancelled_at, i.cancel_reason AS item_cancel_reason, i.metadata AS item_metadata\n FROM public.items i\n JOIN public.plg_financial_invoices f ON f.order_id = i.order_id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n LEFT JOIN public.item_associations a ON a.to_item_id = i.id\n WHERE i.invoice_id IS NULL AND i.order_id IS NOT NULL\n AND NOT EXISTS (SELECT 1 FROM public.items own WHERE own.invoice_id = f.id);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 4. A licença de emendar uma conta paga\n-- ═══════════════════════════════════════════════════════════════════════════\n-- Conta com pagamento é imutável (099/110) — certo para quem edita a conta,\n-- errado para quem a CANCELA ou lança o juros que o banco cobrou. As duas RPCs\n-- deste arquivo ligam \\`financial.amend\\` na própria transação; nenhum outro\n-- caminho liga.\n\nCREATE OR REPLACE FUNCTION public.trg_invoice_line_immutable() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_issued date; v_cancelled timestamptz; v_invoice uuid; v_paid numeric;\nBEGIN\n IF coalesce(current_setting('financial.amend', true), '') = 'on' THEN RETURN coalesce(NEW, OLD); END IF;\n v_invoice := coalesce(NEW.invoice_id, OLD.invoice_id);\n IF v_invoice IS NULL THEN RETURN coalesce(NEW, OLD); END IF;\n SELECT f.issue_date, f.cancelled_at INTO v_issued, v_cancelled\n FROM public.plg_financial_invoices f WHERE f.id = v_invoice;\n IF v_issued IS NOT NULL AND v_cancelled IS NULL THEN\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = v_invoice;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'items: the lines of invoice % cannot be altered — % already received', v_invoice, v_paid\n USING ERRCODE = '23514',\n HINT = 'A paid invoice is immutable (PRD 04). Reverse the payments, or raise a new invoice.';\n END IF;\n END IF;\n RETURN coalesce(NEW, OLD);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_immutable()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\nDECLARE v_paid numeric;\nBEGIN\n IF OLD.issue_date IS NULL OR OLD.cancelled_at IS NOT NULL THEN RETURN NEW; END IF;\n IF NEW.cancelled_at IS NOT NULL AND OLD.cancelled_at IS NULL THEN RETURN NEW; END IF;\n\n IF NEW.direction IS DISTINCT FROM OLD.direction\n OR NEW.order_id IS DISTINCT FROM OLD.order_id\n OR NEW.number IS DISTINCT FROM OLD.number\n OR NEW.issue_date IS DISTINCT FROM OLD.issue_date\n OR NEW.currency IS DISTINCT FROM OLD.currency THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % is issued and cannot be altered', OLD.number\n USING ERRCODE = '23514',\n HINT = 'A closed invoice is immutable (PRD 04). Correct it with a new invoice, or cancel this one through financial_cancel_invoice.';\n END IF;\n\n -- Só o TOTAL ganha a licença: cancelar itens e lançar juros mudam quanto a\n -- conta vale; competência e contraparte continuam travadas.\n IF NEW.competence_date IS DISTINCT FROM OLD.competence_date\n OR (NEW.total IS DISTINCT FROM OLD.total AND coalesce(current_setting('financial.amend', true), '') <> 'on')\n OR NEW.counterparty_person_id IS DISTINCT FROM OLD.counterparty_person_id THEN\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = OLD.id;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % has % already received and cannot be rewritten', OLD.number, v_paid\n USING ERRCODE = '23514',\n HINT = 'Reverse the payments first, or correct it with a new invoice.';\n END IF;\n END IF;\n\n RETURN NEW;\nEND $function$;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 5. Os motivos da casa, semeados na primeira pergunta\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE OR REPLACE FUNCTION public.financial_cancellation_reasons()\nRETURNS SETOF public.plg_financial_cancellation_reasons\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE v_tenant uuid := public.plg_financial_require_tenant();\nBEGIN\n PERFORM public.plg_financial_require_permission('financial.read');\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellation_reasons r WHERE r.tenant_id = v_tenant) THEN\n INSERT INTO public.plg_financial_cancellation_reasons (tenant_id, name)\n SELECT v_tenant, n FROM unnest(ARRAY['Outros', 'Solicitado pelo cliente', 'Solicitado pelo profissional']) n\n ON CONFLICT DO NOTHING;\n END IF;\n RETURN QUERY SELECT * FROM public.plg_financial_cancellation_reasons r\n WHERE r.tenant_id = v_tenant AND r.is_active ORDER BY r.name;\nEND $$;\nREVOKE ALL ON FUNCTION public.financial_cancellation_reasons() FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_cancellation_reasons() TO authenticated, service_role;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 6. De onde veio a mercadoria de cada linha\n-- ═══════════════════════════════════════════════════════════════════════════\n-- A nota é a ponte: a entrada no estoque diz «Entrada via DANFE NF-e <nº>»\n-- (fiscal-br/015, e as linhas migradas do V1 já diziam). O número sai da nota\n-- recebida pelo V2 ou, na conta que veio do V1, do pedido de compra. O produto\n-- casa pelo id e, se o V1 tiver duplicado o cadastro, pelo nome.\n--\n-- Leitura dinâmica: o financeiro não depende fisicamente do estoque; sem o\n-- plugin de estoque, nenhuma linha está «no estoque».\n\nCREATE OR REPLACE FUNCTION public.financial_invoice_stock_entries(p_invoice_id uuid)\nRETURNS TABLE (item_id uuid, product_id uuid, location_id uuid, entered numeric, available numeric, entered_on date)\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inv public.plg_financial_invoices%ROWTYPE;\n v_nf text;\nBEGIN\n SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;\n IF NOT FOUND OR v_inv.direction <> 'payable' THEN RETURN; END IF;\n PERFORM public.plg_financial_require_permission('financial.read', v_inv.unit_id);\n IF to_regclass('public.plg_inventory_stock_movements') IS NULL THEN RETURN; END IF;\n\n IF to_regclass('public.plg_fiscalbr_inbox_documents') IS NOT NULL THEN\n EXECUTE 'SELECT d.nfe_number FROM public.plg_fiscalbr_inbox_documents d WHERE d.tenant_id = $1 AND d.invoice_id = $2 LIMIT 1'\n INTO v_nf USING v_tenant, p_invoice_id;\n END IF;\n IF v_nf IS NULL AND v_inv.order_id IS NOT NULL THEN\n SELECT nullif(btrim(o.reference_number), '') INTO v_nf FROM public.orders o WHERE o.id = v_inv.order_id;\n END IF;\n v_nf := coalesce(v_nf, nullif(btrim(v_inv.metadata ->> 'fiscalNumber'), ''));\n IF v_nf IS NULL THEN RETURN; END IF;\n\n RETURN QUERY EXECUTE $q$\n WITH lines AS (\n SELECT l.item_id, l.product_id, lower(btrim(coalesce(p.name, l.name))) AS pname\n FROM public.v_financial_invoice_lines l\n LEFT JOIN public.products p ON p.id = l.product_id\n WHERE l.invoice_id = $2 AND l.item_cancelled_at IS NULL\n ), entries AS (\n SELECT m.product_id, m.source_location_id AS location_id, sum(m.quantity) AS qty,\n min(m.movement_date)::date AS entered_on, lower(btrim(p.name)) AS pname\n FROM public.plg_inventory_stock_movements m\n LEFT JOIN public.products p ON p.id = m.product_id\n WHERE m.tenant_id = $1 AND m.kind = 'in'\n AND (m.reason = 'Entrada via DANFE NF-e ' || $3 OR m.document_number = 'NF-e ' || $3)\n GROUP BY m.product_id, m.source_location_id, p.name\n ), by_product AS (\n SELECT l.item_id, e.* FROM lines l JOIN entries e ON e.product_id = l.product_id\n ), by_name AS (\n SELECT l.item_id, e.* FROM lines l JOIN entries e ON e.pname = l.pname\n WHERE l.item_id NOT IN (SELECT item_id FROM by_product)\n AND e.product_id NOT IN (SELECT product_id FROM by_product)\n ), rest_l AS (\n SELECT l.item_id FROM lines l\n WHERE l.item_id NOT IN (SELECT item_id FROM by_product UNION SELECT item_id FROM by_name)\n ), rest_e AS (\n SELECT e.* FROM entries e\n WHERE e.product_id NOT IN (SELECT product_id FROM by_product UNION SELECT product_id FROM by_name)\n ), by_elimination AS (\n -- The V1 sometimes booked a note's entry under another product record.\n -- One line and one product left over on the same note: they are the pair.\n SELECT l.item_id, e.* FROM rest_l l CROSS JOIN rest_e e\n WHERE (SELECT count(*) FROM rest_l) = 1 AND (SELECT count(DISTINCT product_id) FROM rest_e) = 1\n ), matched AS (\n SELECT * FROM by_product UNION ALL SELECT * FROM by_name UNION ALL SELECT * FROM by_elimination\n )\n SELECT m.item_id, m.product_id, m.location_id, m.qty,\n greatest(least(m.qty, coalesce((SELECT sum(sp.quantity) FROM public.plg_inventory_stock_positions sp\n WHERE sp.tenant_id = $1 AND sp.product_id = m.product_id\n AND sp.stock_location_id = m.location_id), 0)), 0),\n m.entered_on\n FROM matched m\n $q$ USING v_tenant, p_invoice_id, v_nf;\nEND $$;\nREVOKE ALL ON FUNCTION public.financial_invoice_stock_entries(uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_stock_entries(uuid) TO authenticated, service_role;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 7. Cancelar — itens ou a conta inteira\n-- ═══════════════════════════════════════════════════════════════════════════\n-- p_item_ids vazio/nulo = todos. p_stock = [{item_id, destination: 'devolver'\n-- | 'perda' | 'manter', note}] — obrigatório para cada linha que entrou no\n-- estoque; NULL só pelo caminho antigo (financial_cancel_invoice), que nunca\n-- perguntou e continua sem mexer no estoque.\n\nCREATE OR REPLACE FUNCTION public.financial_cancel_invoice_items(\n p_invoice_id uuid,\n p_item_ids uuid[] DEFAULT NULL,\n p_reason_id uuid DEFAULT NULL,\n p_detail text DEFAULT NULL,\n p_stock jsonb DEFAULT '[]'::jsonb,\n p_idempotency_key text DEFAULT NULL\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inv public.plg_financial_invoices%ROWTYPE;\n v_reason text;\n v_rname text;\n v_live uuid[];\n v_sel uuid[];\n v_whole boolean;\n v_amount numeric := 0;\n v_cut numeric;\n v_take numeric;\n v_credit numeric := 0;\n v_paid numeric;\n v_inst record;\n v_alloc record;\n v_entry record;\n v_dest jsonb;\n v_dest_k text;\n v_stock jsonb := '[]'::jsonb;\n v_op jsonb;\n v_ops jsonb;\n v_person uuid;\n v_cancel_acc uuid;\n v_mov uuid;\n v_log uuid;\n v_nf text;\nBEGIN\n SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: invoice % not found', p_invoice_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);\n\n IF p_idempotency_key IS NOT NULL THEN\n SELECT c.id INTO v_log FROM public.plg_financial_cancellations c\n WHERE c.tenant_id = v_tenant AND c.idempotency_key = p_idempotency_key;\n IF v_log IS NOT NULL THEN\n RETURN (SELECT jsonb_build_object('cancellation_id', c.id, 'invoice_id', c.invoice_id, 'whole', c.whole,\n 'amount', c.amount, 'credit_amount', c.credit_amount, 'replayed', true)\n FROM public.plg_financial_cancellations c WHERE c.id = v_log);\n END IF;\n END IF;\n IF v_inv.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: invoice % is already cancelled', v_inv.number USING ERRCODE = '55000';\n END IF;\n\n -- O motivo: da lista, com o detalhe ao lado — «Outros — nota devolvida».\n IF p_reason_id IS NOT NULL THEN\n SELECT r.name INTO v_rname FROM public.plg_financial_cancellation_reasons r\n WHERE r.id = p_reason_id AND r.tenant_id = v_tenant;\n IF v_rname IS NULL THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: reason % not found', p_reason_id USING ERRCODE = 'P0002';\n END IF;\n END IF;\n v_reason := nullif(concat_ws(' — ', v_rname, nullif(btrim(coalesce(p_detail, '')), '')), '');\n IF v_reason IS NULL THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: a reason is required' USING ERRCODE = '22023';\n END IF;\n\n SELECT coalesce(array_agg(l.item_id), '{}') INTO v_live\n FROM public.v_financial_invoice_lines l WHERE l.invoice_id = p_invoice_id AND l.item_cancelled_at IS NULL;\n IF p_item_ids IS NULL OR cardinality(p_item_ids) = 0 THEN\n v_sel := v_live;\n ELSE\n IF EXISTS (SELECT 1 FROM unnest(p_item_ids) x WHERE x <> ALL (v_live)) THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: an item is not a live line of invoice %', v_inv.number USING ERRCODE = '22023';\n END IF;\n SELECT array_agg(DISTINCT x) INTO v_sel FROM unnest(p_item_ids) x;\n END IF;\n v_whole := NOT EXISTS (SELECT 1 FROM unnest(v_live) x WHERE x <> ALL (v_sel));\n\n -- Quanto: a soma das linhas; a conta inteira cancela o que a conta vale (o\n -- V1 tem conta cujas linhas não somam o total, e o total é o que se deve).\n IF v_whole THEN\n v_amount := v_inv.total;\n ELSE\n SELECT round(coalesce(sum(l.total), 0), 2) INTO v_amount\n FROM public.v_financial_invoice_lines l WHERE l.invoice_id = p_invoice_id AND l.item_id = ANY (v_sel);\n v_amount := least(v_amount, v_inv.total);\n END IF;\n\n PERFORM set_config('financial.amend', 'on', true);\n\n -- ── a mercadoria ──────────────────────────────────────────────────────────\n IF p_stock IS NOT NULL THEN\n FOR v_entry IN\n SELECT e.* FROM public.financial_invoice_stock_entries(p_invoice_id) e\n WHERE e.item_id = ANY (v_sel) ORDER BY e.item_id, e.entered_on DESC\n LOOP\n SELECT d INTO v_dest FROM jsonb_array_elements(p_stock) d WHERE d ->> 'item_id' = v_entry.item_id::text LIMIT 1;\n v_dest_k := v_dest ->> 'destination';\n IF v_dest_k IS NULL OR v_dest_k NOT IN ('devolver', 'perda', 'manter') THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: choose what happens to the goods of item % (devolver | perda | manter)', v_entry.item_id\n USING ERRCODE = '22023', HINT = 'ESCOLHA_DESTINO';\n END IF;\n v_op := NULL;\n IF v_dest_k <> 'manter' AND v_entry.available > 0 THEN\n EXECUTE 'SELECT public.inventory_adjust($1, $2, $3, $4)' INTO v_op\n USING v_entry.location_id,\n jsonb_build_array(jsonb_strip_nulls(jsonb_build_object(\n 'product_id', v_entry.product_id, 'quantity', -v_entry.available,\n 'notes', nullif(btrim(coalesce(v_dest ->> 'note', '')), '')))),\n CASE v_dest_k WHEN 'devolver' THEN 'Devolução ao fornecedor' ELSE 'Perda de mercadoria da compra' END,\n 'cancel:' || p_invoice_id || ':' || v_entry.item_id || ':' || v_entry.location_id\n || coalesce(':' || p_idempotency_key, '');\n END IF;\n v_stock := v_stock || jsonb_build_object(\n 'item_id', v_entry.item_id, 'product_id', v_entry.product_id, 'location_id', v_entry.location_id,\n 'destination', v_dest_k, 'entered', v_entry.entered,\n 'moved', CASE WHEN v_op IS NULL THEN 0 ELSE v_entry.available END,\n 'note', nullif(btrim(coalesce(v_dest ->> 'note', '')), ''),\n 'operation_id', v_op ->> 'operation_id');\n END LOOP;\n END IF;\n\n -- ── as linhas ─────────────────────────────────────────────────────────────\n UPDATE public.items SET cancelled_at = now(), cancel_reason = v_reason\n WHERE id = ANY (v_sel) AND tenant_id = v_tenant AND cancelled_at IS NULL;\n\n -- ── o dinheiro ────────────────────────────────────────────────────────────\n IF v_whole THEN\n SELECT coalesce(sum(b.paid), 0) INTO v_paid FROM public.v_financial_installment_balances b WHERE b.invoice_id = p_invoice_id;\n v_credit := v_paid;\n UPDATE public.plg_financial_invoices\n SET cancelled_at = now(), cancel_reason = v_reason, cancel_reason_id = p_reason_id,\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('cancelled', true, 'at', now()))\n WHERE id = p_invoice_id;\n UPDATE public.plg_financial_invoice_installments\n SET cancelled_at = now(),\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('cancelled', true))\n WHERE invoice_id = p_invoice_id AND cancelled_at IS NULL;\n UPDATE public.plg_financial_movements SET status = 'cancelled', updated_at = now()\n WHERE financial_invoice_id = p_invoice_id AND movement_kind = 'bill' AND status <> 'cancelled';\n ELSE\n v_cut := v_amount;\n -- 1º a parte NÃO paga, da última parcela para trás.\n FOR v_inst IN\n SELECT i.id, i.amount, public.plg_financial_installment_paid(i.id) AS paid\n FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice_id AND i.cancelled_at IS NULL\n ORDER BY i.number DESC\n LOOP\n EXIT WHEN v_cut <= 0;\n v_take := least(greatest(v_inst.amount - v_inst.paid, 0), v_cut);\n CONTINUE WHEN v_take <= 0;\n UPDATE public.plg_financial_invoice_installments\n SET amount = amount - v_take,\n cancelled_at = CASE WHEN amount - v_take <= 0 AND v_inst.paid = 0 THEN now() END,\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true))\n WHERE id = v_inst.id;\n v_cut := v_cut - v_take;\n END LOOP;\n -- 2º o que sobrou sai do pago: o pagamento mais recente primeiro, e o que\n -- ele deixa de quitar vira crédito.\n FOR v_inst IN\n SELECT i.id, i.amount FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice_id AND i.cancelled_at IS NULL\n ORDER BY i.number DESC\n LOOP\n EXIT WHEN v_cut <= 0;\n FOR v_alloc IN\n SELECT a.id, a.amount FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = v_inst.id AND m.status <> 'cancelled' AND m.reversed_by_id IS NULL\n ORDER BY m.payment_date DESC NULLS LAST, a.created_at DESC\n LOOP\n EXIT WHEN v_cut <= 0;\n v_take := least(v_alloc.amount, v_cut);\n IF v_take >= v_alloc.amount THEN\n DELETE FROM public.plg_financial_allocations WHERE id = v_alloc.id;\n ELSE\n UPDATE public.plg_financial_allocations SET amount = amount - v_take,\n metadata = metadata || jsonb_build_object('reduced_by_cancellation', v_take)\n WHERE id = v_alloc.id;\n END IF;\n UPDATE public.plg_financial_invoice_installments\n SET amount = amount - v_take,\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true))\n WHERE id = v_inst.id;\n v_credit := v_credit + v_take;\n v_cut := v_cut - v_take;\n END LOOP;\n END LOOP;\n UPDATE public.plg_financial_invoices\n SET total = greatest(total - v_amount, 0),\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true, 'at', now()))\n WHERE id = p_invoice_id;\n PERFORM public.plg_financial_sync_mirror(i.id)\n FROM public.plg_financial_invoice_installments i WHERE i.invoice_id = p_invoice_id;\n END IF;\n\n -- ── o crédito ─────────────────────────────────────────────────────────────\n -- Fornecedor: ele passa a nos dever — débito na conta dele. Cliente: nós\n -- passamos a dever a ele — crédito na conta dele (como financial_cancel_paid_item).\n v_credit := round(v_credit, 2);\n IF v_credit > 0 THEN\n IF v_inv.counterparty_person_id IS NULL THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: % was paid and the invoice has no counterparty to hold the credit', v_credit\n USING ERRCODE = '55000';\n END IF;\n v_person := public.plg_financial_person_account(v_tenant, v_inv.counterparty_person_id);\n v_cancel_acc := public.plg_financial_system_account(v_tenant, 'cancellations');\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, financial_invoice_id, direction, movement_kind, amount, paid_amount, status, due_date, payment_date,\n method, reason, metadata, idempotency_key, created_by, debit_account_id, credit_account_id)\n VALUES\n (v_tenant, v_inv.unit_id, v_inv.id, CASE WHEN v_inv.direction = 'payable' THEN 'credit' ELSE 'debit' END,\n 'transfer', v_credit, v_credit, 'paid', current_date, current_date, 'credit', v_reason,\n jsonb_build_object('source', 'financial_cancel_invoice_items', 'credit_source', 'cancellation',\n 'cancelled_invoice_id', v_inv.id, 'cancelled_item_ids', to_jsonb(v_sel)),\n CASE WHEN p_idempotency_key IS NOT NULL THEN 'cancel-credit:' || p_idempotency_key END, auth.uid(),\n CASE WHEN v_inv.direction = 'payable' THEN v_person ELSE v_cancel_acc END,\n CASE WHEN v_inv.direction = 'payable' THEN v_cancel_acc ELSE v_person END)\n RETURNING id INTO v_mov;\n END IF;\n\n INSERT INTO public.plg_financial_cancellations\n (tenant_id, unit_id, invoice_id, whole, item_ids, reason_id, reason, amount, credit_amount, credit_movement_id,\n stock, idempotency_key, created_by)\n VALUES (v_tenant, v_inv.unit_id, v_inv.id, v_whole, v_sel, p_reason_id, v_reason, v_amount, v_credit, v_mov,\n v_stock, p_idempotency_key, auth.uid())\n RETURNING id INTO v_log;\n\n PERFORM public.plg_financial_audit(v_tenant,\n CASE WHEN v_whole THEN 'financial.invoice.cancel' ELSE 'financial.invoice.cancel_items' END,\n 'plg_financial_invoices', v_inv.id::text,\n jsonb_build_object('reason', v_reason, 'reason_id', p_reason_id, 'item_ids', to_jsonb(v_sel), 'amount', v_amount,\n 'credit_amount', v_credit, 'stock', v_stock, 'cancellation_id', v_log));\n\n RETURN jsonb_build_object('cancellation_id', v_log, 'invoice_id', v_inv.id, 'whole', v_whole, 'amount', v_amount,\n 'credit_amount', v_credit, 'credit_movement_id', v_mov, 'stock', v_stock, 'replayed', false);\nEND $$;\nREVOKE ALL ON FUNCTION public.financial_cancel_invoice_items(uuid, uuid[], uuid, text, jsonb, text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_cancel_invoice_items(uuid, uuid[], uuid, text, jsonb, text) TO authenticated, service_role;\n\n-- O caminho antigo (cancelar em lote na lista) vira a conta inteira do novo,\n-- com o texto como detalhe e sem tocar no estoque.\nCREATE OR REPLACE FUNCTION public.financial_cancel_invoice(p_invoice_id uuid, p_reason text, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_res jsonb;\nBEGIN\n IF nullif(btrim(coalesce(p_reason, '')), '') IS NULL THEN\n RAISE EXCEPTION 'financial_cancel_invoice: a reason is required' USING ERRCODE = '22023';\n END IF;\n IF EXISTS (SELECT 1 FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant AND cancelled_at IS NOT NULL) THEN\n RETURN jsonb_build_object('invoice_id', p_invoice_id, 'status', 'cancelled', 'replayed', true);\n END IF;\n v_res := public.financial_cancel_invoice_items(p_invoice_id, NULL, NULL, p_reason, NULL, p_idempotency_key);\n RETURN v_res || jsonb_build_object('status', 'cancelled');\nEND $$;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 8. Pagar com o crédito do fornecedor\n-- ═══════════════════════════════════════════════════════════════════════════\n-- O saldo da conta da pessoa NÃO serve: o porte do V1 lança os pagamentos a\n-- fornecedor contra essa mesma conta (R$ 102 mil na Artorius em 09-10-2026), e\n-- lido como crédito ele inventaria dinheiro. Crédito de fornecedor é só o que\n-- um cancelamento criou, menos o que já se pagou com ele. As pernas do\n-- pagamento já estavam certas; muda só a conferência. Remendo no corpo vivo,\n-- como a fiscal-br/015: reescrever 134 linhas de memória é como se perde regra.\n\nCREATE OR REPLACE FUNCTION public.plg_financial_supplier_credit(p_tenant uuid, p_person uuid)\nRETURNS numeric LANGUAGE sql SECURITY DEFINER SET search_path TO '' AS $$\n WITH acc AS (SELECT public.plg_financial_person_account(p_tenant, p_person) AS id)\n SELECT round(\n coalesce((SELECT sum(m.amount) FROM public.plg_financial_movements m, acc\n WHERE m.tenant_id = p_tenant AND m.debit_account_id = acc.id AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL AND m.metadata ->> 'credit_source' = 'cancellation'), 0)\n - coalesce((SELECT sum(m.amount) FROM public.plg_financial_movements m, acc\n WHERE m.tenant_id = p_tenant AND m.credit_account_id = acc.id AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL AND m.movement_kind = 'payment' AND m.method = 'credit'\n AND m.direction = 'debit'), 0), 2)\n$$;\nREVOKE ALL ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) TO service_role;\n\nCREATE OR REPLACE FUNCTION public.financial_supplier_credit(p_person_id uuid)\nRETURNS numeric LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE v_tenant uuid := public.plg_financial_require_tenant();\nBEGIN\n PERFORM public.plg_financial_require_permission('financial.read');\n RETURN public.plg_financial_supplier_credit(v_tenant, p_person_id);\nEND $$;\nREVOKE ALL ON FUNCTION public.financial_supplier_credit(uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_supplier_credit(uuid) TO authenticated, service_role;\n\nDO $m$\nDECLARE\n v_def text;\n v_antes text := 'SELECT balance INTO v_credit FROM public.v_financial_account_balances WHERE account_id = v_holder;';\n v_depois text := 'SELECT CASE WHEN v_invoice.direction = ''payable'' THEN public.plg_financial_supplier_credit(v_tenant, v_invoice.counterparty_person_id) ELSE balance END INTO v_credit FROM public.v_financial_account_balances WHERE account_id = v_holder;';\nBEGIN\n SELECT pg_get_functiondef(p.oid) INTO v_def\n FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'public' AND p.proname = 'financial_receive_payment';\n IF v_def IS NULL OR position('plg_financial_supplier_credit' in v_def) > 0 THEN RETURN; END IF;\n IF position(v_antes in v_def) = 0 THEN\n RAISE EXCEPTION 'financial/120: financial_receive_payment não é o esperado — confira à mão';\n END IF;\n EXECUTE replace(v_def, v_antes, v_depois);\nEND\n$m$;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 9. Juros, multa e desconto entram NA conta\n-- ═══════════════════════════════════════════════════════════════════════════\n-- No V1 o boleto pago com atraso ganha a linha «Multa/Juros» (plano 5.1.3) e\n-- a parcela passa a valer o que o banco cobrou. A conciliação do V2 criava\n-- uma conta avulsa só com os juros, sem fornecedor e sem caminho de volta.\n\nCREATE OR REPLACE FUNCTION public.financial_add_installment_charge(\n p_installment_id uuid,\n p_kind text,\n p_amount numeric,\n p_chart_account_id uuid DEFAULT NULL,\n p_description text DEFAULT NULL,\n p_idempotency_key text DEFAULT NULL\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inst public.plg_financial_invoice_installments%ROWTYPE;\n v_inv public.plg_financial_invoices%ROWTYPE;\n v_amount numeric := round(coalesce(p_amount, 0), 2);\n v_signed numeric;\n v_paid numeric;\n v_item uuid;\n v_own boolean;\n v_label text;\nBEGIN\n IF p_kind NOT IN ('interest', 'fine', 'discount') THEN\n RAISE EXCEPTION 'financial_add_installment_charge: kind must be interest | fine | discount' USING ERRCODE = '22023';\n END IF;\n IF v_amount <= 0 THEN\n RAISE EXCEPTION 'financial_add_installment_charge: amount must be positive' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_inst FROM public.plg_financial_invoice_installments WHERE id = p_installment_id AND tenant_id = v_tenant FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_add_installment_charge: installment % not found', p_installment_id USING ERRCODE = 'P0002';\n END IF;\n SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = v_inst.invoice_id FOR UPDATE;\n PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);\n IF v_inst.cancelled_at IS NOT NULL OR v_inv.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'financial_add_installment_charge: installment is cancelled' USING ERRCODE = '55000';\n END IF;\n IF p_idempotency_key IS NOT NULL THEN\n SELECT i.id INTO v_item FROM public.items i\n WHERE i.tenant_id = v_tenant AND i.metadata ->> 'charge_idempotency_key' = p_idempotency_key;\n IF v_item IS NOT NULL THEN\n RETURN jsonb_build_object('item_id', v_item, 'installment_id', v_inst.id, 'invoice_id', v_inv.id, 'replayed', true);\n END IF;\n END IF;\n\n v_signed := CASE WHEN p_kind = 'discount' THEN -v_amount ELSE v_amount END;\n v_paid := public.plg_financial_installment_paid(v_inst.id);\n IF v_inst.amount + v_signed < v_paid THEN\n RAISE EXCEPTION 'financial_add_installment_charge: the discount takes installment % below what was already paid (%)', v_inst.number, v_paid\n USING ERRCODE = '23514';\n END IF;\n v_label := coalesce(nullif(btrim(coalesce(p_description, '')), ''),\n CASE p_kind WHEN 'interest' THEN 'Juros por atraso' WHEN 'fine' THEN 'Multa por atraso' ELSE 'Desconto obtido' END);\n\n PERFORM set_config('financial.amend', 'on', true);\n\n -- A linha mora onde as outras moram: conta que veio de um pedido (o V1 todo)\n -- lista as linhas DO PEDIDO enquanto não tiver nenhuma própria — pôr a\n -- primeira na conta faria as outras sumirem da tela (v_financial_invoice_lines).\n v_own := EXISTS (SELECT 1 FROM public.items WHERE invoice_id = v_inv.id) OR v_inv.order_id IS NULL;\n INSERT INTO public.items (tenant_id, unit_id, invoice_id, order_id, name, description, quantity, unit_price, total,\n sort_order, chart_account_id, metadata)\n VALUES (v_tenant, v_inv.unit_id,\n CASE WHEN v_own THEN v_inv.id END, CASE WHEN v_own THEN NULL ELSE v_inv.order_id END,\n v_label, v_label, 1, v_signed, v_signed,\n 1000 + coalesce((SELECT count(*) FROM public.v_financial_invoice_lines l WHERE l.invoice_id = v_inv.id), 0),\n p_chart_account_id,\n jsonb_strip_nulls(jsonb_build_object('itemKind', 'other', 'charge', p_kind, 'installment_id', v_inst.id,\n 'source', 'financial_add_installment_charge',\n 'charge_idempotency_key', p_idempotency_key)))\n RETURNING id INTO v_item;\n\n UPDATE public.plg_financial_invoice_installments\n SET amount = amount + v_signed,\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true))\n WHERE id = v_inst.id;\n UPDATE public.plg_financial_invoices\n SET total = total + v_signed,\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true, 'at', now()))\n WHERE id = v_inv.id;\n PERFORM public.plg_financial_sync_mirror(v_inst.id);\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.charge', 'plg_financial_invoices', v_inv.id::text,\n jsonb_build_object('installment_id', v_inst.id, 'kind', p_kind, 'amount', v_signed, 'item_id', v_item,\n 'chart_account_id', p_chart_account_id));\n RETURN jsonb_build_object('item_id', v_item, 'installment_id', v_inst.id, 'invoice_id', v_inv.id,\n 'amount', v_signed, 'installment_amount', v_inst.amount + v_signed, 'replayed', false);\nEND $$;\nREVOKE ALL ON FUNCTION public.financial_add_installment_charge(uuid, text, numeric, uuid, text, text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_add_installment_charge(uuid, text, numeric, uuid, text, text) TO authenticated, service_role;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 10. A sincronia do V1 respeita o V2 — e para de dobrar parcela\n-- ═══════════════════════════════════════════════════════════════════════════\n-- Duas mudanças no corpo vivo (que difere do arquivo 071/079):\n--\n-- a) parcela que o V2 cancelou ou emendou (\\`v2_decision\\`) não volta a valer\n-- nem a ter o valor antigo quando o V1 regrava a linha;\n-- b) o V1 apaga e recria a parcela ao salvar a conta. Em convivência a\n-- exclusão não atravessa, e a recriada caía no \\`unique_violation\\` e\n-- ENTRAVA COMO Nº+1 — PAG-000330 com 5.656,40 + 5.656,48 para um aluguel\n-- de 5.656,48. Agora, se somar a nova estouraria o total da conta, ela é\n-- a substituta: a antiga (migrada, sem baixa) sai cancelada e a nova\n-- herda o número. Duas parcelas vivas de mesmo número que CABEM no total\n-- (a fatura #1869) continuam entrando como antes.\n\nCREATE OR REPLACE FUNCTION migration._fin_installment_from_bill(p_tenant uuid, p_id uuid, p_invoice uuid, p_unit uuid, p_number integer, p_due date, p_amount numeric, p_cancelled boolean, p_sync boolean)\n RETURNS void\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_number integer := greatest(coalesce(p_number, 1), 1);\n v_cancel timestamptz := CASE WHEN coalesce(p_cancelled, false)\n THEN coalesce(p_due, current_date)::timestamptz END;\n v_old record;\n v_total numeric;\n v_live numeric;\nBEGIN\n IF p_invoice IS NULL THEN RETURN; END IF;\n\n DELETE FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice\n AND i.id <> p_id\n AND NOT (i.metadata ? 'migration')\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations a WHERE a.installment_id = i.id)\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_movements m\n WHERE m.financial_invoice_installment_id = i.id);\n\n BEGIN\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)\n VALUES (p_id, p_tenant, p_unit, p_invoice, v_number, p_due,\n greatest(coalesce(p_amount, 0), 0), v_cancel,\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements')))\n ON CONFLICT (id) DO UPDATE SET\n number = EXCLUDED.number,\n due_date = EXCLUDED.due_date,\n amount = CASE WHEN public.plg_financial_invoice_installments.metadata ? 'v2_decision'\n THEN public.plg_financial_invoice_installments.amount ELSE EXCLUDED.amount END,\n cancelled_at = CASE WHEN public.plg_financial_invoice_installments.metadata ? 'v2_decision'\n THEN public.plg_financial_invoice_installments.cancelled_at ELSE EXCLUDED.cancelled_at END,\n unit_id = coalesce(public.plg_financial_invoice_installments.unit_id, EXCLUDED.unit_id),\n metadata = public.plg_financial_invoice_installments.metadata || EXCLUDED.metadata;\n EXCEPTION\n WHEN unique_violation THEN\n SELECT i.id, i.amount INTO v_old\n FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice AND i.number = v_number AND i.id <> p_id\n AND i.cancelled_at IS NULL\n AND i.metadata ? 'migration' AND NOT (i.metadata ? 'v2_decision')\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations a WHERE a.installment_id = i.id);\n SELECT f.total INTO v_total FROM public.plg_financial_invoices f WHERE f.id = p_invoice;\n SELECT coalesce(sum(i.amount), 0) INTO v_live FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice AND i.cancelled_at IS NULL;\n\n IF v_old.id IS NOT NULL AND v_cancel IS NULL\n AND v_live + greatest(coalesce(p_amount, 0), 0) > coalesce(v_total, 0) + 0.01 THEN\n -- A substituta: a antiga sai do número (cancelada) e a nova entra nele.\n UPDATE public.plg_financial_invoice_installments\n SET cancelled_at = now(),\n number = (SELECT coalesce(max(x.number), 0) + 1 FROM public.plg_financial_invoice_installments x WHERE x.invoice_id = p_invoice),\n metadata = metadata || jsonb_build_object('superseded_by', p_id)\n WHERE id = v_old.id;\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)\n VALUES (p_id, p_tenant, p_unit, p_invoice, v_number, p_due, greatest(coalesce(p_amount, 0), 0), NULL,\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements', 'supersedes', v_old.id)))\n ON CONFLICT (id) DO NOTHING;\n PERFORM public.plg_financial_sync_mirror(v_old.id);\n ELSE\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)\n SELECT p_id, p_tenant, p_unit, p_invoice, coalesce(max(i.number), 0) + 1, p_due,\n greatest(coalesce(p_amount, 0), 0), v_cancel,\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements', 'renumbered', true))\n FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice\n ON CONFLICT (id) DO NOTHING;\n END IF;\n END;\n\n IF p_sync THEN\n PERFORM public.plg_financial_sync_mirror(p_id);\n END IF;\nEND $function$;\n`\n\nexport const MIGRATION_121_O_MOTIVO_E_DO_ESTABELECIMENTO = `-- O terceiro motivo padrão é «Solicitado pelo estabelecimento», não «pelo\n-- profissional» (decisão do founder, 09-10-2026): numa conta a pagar quem pede\n-- o cancelamento é a casa, e o V1 herdou o rótulo da agenda de serviços.\n--\n-- Renomeia o semeado pela 120 só onde ninguém o usou — um motivo já gravado num\n-- cancelamento continua dizendo o que disse. Idempotente.\n\nUPDATE public.plg_financial_cancellation_reasons r\n SET name = 'Solicitado pelo estabelecimento'\n WHERE r.name = 'Solicitado pelo profissional'\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellations c WHERE c.reason_id = r.id)\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellation_reasons x\n WHERE x.tenant_id = r.tenant_id AND lower(x.name) = 'solicitado pelo estabelecimento');\n\nCREATE OR REPLACE FUNCTION public.financial_cancellation_reasons()\nRETURNS SETOF public.plg_financial_cancellation_reasons\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE v_tenant uuid := public.plg_financial_require_tenant();\nBEGIN\n PERFORM public.plg_financial_require_permission('financial.read');\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellation_reasons r WHERE r.tenant_id = v_tenant) THEN\n INSERT INTO public.plg_financial_cancellation_reasons (tenant_id, name)\n SELECT v_tenant, n FROM unnest(ARRAY['Outros', 'Solicitado pelo cliente', 'Solicitado pelo estabelecimento']) n\n ON CONFLICT DO NOTHING;\n END IF;\n RETURN QUERY SELECT * FROM public.plg_financial_cancellation_reasons r\n WHERE r.tenant_id = v_tenant AND r.is_active ORDER BY r.name;\nEND $$;\n`\n\nexport const MIGRATION_122_O_CANCELAMENTO_FECHA_AS_PORTAS_QUE_ABRIU = `-- ============================================================================\n-- 122 — cancelling closes the doors 120 left open\n--\n-- 120 (cancel by item, with reason, stock and supplier credit) shipped three\n-- regressions the gates caught on main and that were live on the cluster:\n--\n-- 1. \\`v_financial_invoice_lines\\` was recreated WITHOUT \\`security_invoker\\`. A\n-- view without it runs as its owner and skips RLS, so every signed-in user\n-- could read every tenant's invoice lines. Every earlier definition of this\n-- view (010 … 114) was security_invoker; this puts it back.\n--\n-- 2. \\`plg_financial_supplier_credit(p_tenant, p_person)\\` takes the tenant as an\n-- argument and was revoked only FROM PUBLIC. The pools' default privileges\n-- grant EXECUTE to \\`authenticated\\` on every new function, so any user could\n-- ask for any tenant's supplier credit. Only the definer wrappers\n-- (\\`financial_supplier_credit\\`, the payment check) call it, and they run as\n-- the owner, so closing it to clients changes nothing for them.\n--\n-- 3. Three money RPCs accept \\`p_idempotency_key\\` but did not claim it, so two\n-- concurrent calls with the same key could both pass the replay lookup and\n-- both write (#142). They now take the same advisory lock every other money\n-- RPC takes, right before that lookup. The bodies are patched in place, as\n-- 120 itself patched the payment check: copying 130 lines by hand is how a\n-- rule gets lost.\n--\n-- 1 and 2 were hot-fixed on klvfxzreepavcpyjiwla on 2026-10-09 with the same\n-- statements; this file is the record, and it is idempotent.\n-- ============================================================================\n\n-- ── 1. the view reads with the caller's rights again ───────────────────────\nALTER VIEW public.v_financial_invoice_lines SET (security_invoker = true);\n\n-- ── 2. the tenant-taking helper is not a client door ───────────────────────\nREVOKE EXECUTE ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) TO service_role;\n\n-- ── 3. an idempotency key is claimed before it is looked up ────────────────\nDO $patch$\nDECLARE\n v_fn regprocedure;\n v_def text;\n v_anchor text;\n v_claim constant text := 'PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);';\nBEGIN\n FOR v_fn, v_anchor IN\n SELECT f::regprocedure, a FROM (VALUES\n -- the replay lookup is the first thing that reads the key\n ('public.financial_cancel_invoice_items(uuid, uuid[], uuid, text, jsonb, text)', ' IF p_idempotency_key IS NOT NULL THEN'),\n ('public.financial_add_installment_charge(uuid, text, numeric, uuid, text, text)', ' IF p_idempotency_key IS NOT NULL THEN'),\n -- this one replays on \"already cancelled\" before it delegates, so the\n -- claim goes ahead of that check; the delegate re-takes the same lock,\n -- which a transaction-scoped advisory lock allows\n ('public.financial_cancel_invoice(uuid, text, text)', ' IF EXISTS (SELECT 1 FROM public.plg_financial_invoices WHERE id = p_invoice_id')\n ) AS t(f, a)\n LOOP\n v_def := pg_get_functiondef(v_fn);\n IF position('plg_financial_claim_idempotency' in v_def) > 0 THEN\n CONTINUE; -- already claims (a replay, or a later rewrite)\n END IF;\n IF position(v_anchor in v_def) = 0 THEN\n RAISE EXCEPTION '122: % no longer has the line the claim goes before (%); patch it by hand', v_fn, v_anchor;\n END IF;\n -- first occurrence only: the anchor is the replay lookup, not every later use of the key\n EXECUTE overlay(v_def PLACING ' ' || v_claim || E'\\\\n' || v_anchor\n FROM position(v_anchor in v_def) FOR length(v_anchor));\n END LOOP;\nEND $patch$;\n`\n\nexport const MIGRATION_123_THE_MIRROR_DOES_NOT_BILL_A_RECURRENCE_TWICE = `-- ============================================================================\n-- 123 — the mirror does not bill a recurrence twice\n--\n-- A tenant in coexistence has every recurring bill on both sides. When someone\n-- issues October in V2 and V1 then generates its own October bill, the mirror\n-- brought the V1 bill in as a second payable: same supplier, same amount, same\n-- due date, nothing tying the two together (Artorius, 2026-10-11: four pairs,\n-- R$ 16.599,50 owed twice on screen).\n--\n-- The tie exists, it was just never read: the occurrence is the SAME row on\n-- both sides, V2 stamps \\`invoice_id\\` on it when it issues, and V1 reports the\n-- bill it generated as \\`metadata.v1.invoice_id\\`. An occurrence carrying both\n-- names two bills for one month. The V2 one stays — it is the one somebody\n-- chose to issue — and the unpaid mirrored copy is cancelled as a V2 decision,\n-- so later passes of the mirror do not bring it back. A copy that already has\n-- a payment is left alone: which of two paid bills is wrong is not a sync's\n-- call.\n--\n-- Both writers ask, because either row can arrive last: the bill usually lands\n-- seconds before its occurrence is updated, but a bill held in quarantine\n-- lands batches later, when the occurrence no longer changes.\n-- ============================================================================\n\nCREATE INDEX IF NOT EXISTS plg_financial_recurring_bill_occurrences_v1_invoice_idx\n ON public.plg_financial_recurring_bill_occurrences (tenant_id, ((metadata -> 'v1' ->> 'invoice_id')))\n WHERE invoice_id IS NOT NULL;\n\nCREATE OR REPLACE FUNCTION migration._fin_cancel_recurring_duplicate(p_tenant uuid, p_v1_invoice text)\n RETURNS uuid\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_kept uuid;\n v_number text;\n v_dup uuid;\nBEGIN\n IF nullif(btrim(coalesce(p_v1_invoice, '')), '') IS NULL THEN\n RETURN NULL;\n END IF;\n\n SELECT f.id, f.number INTO v_kept, v_number\n FROM public.plg_financial_recurring_bill_occurrences o\n JOIN public.plg_financial_invoices f ON f.id = o.invoice_id AND f.cancelled_at IS NULL\n WHERE o.tenant_id = p_tenant\n AND o.invoice_id IS NOT NULL\n AND o.metadata -> 'v1' ->> 'invoice_id' = p_v1_invoice\n LIMIT 1;\n IF v_kept IS NULL THEN\n RETURN NULL;\n END IF;\n\n SELECT f.id INTO v_dup\n FROM public.plg_financial_invoices f\n WHERE f.tenant_id = p_tenant\n AND f.id <> v_kept\n AND f.cancelled_at IS NULL\n AND f.metadata -> 'migration' ->> 'source_table' = 'invoices'\n AND f.metadata -> 'migration' ->> 'source_numeric_id' = p_v1_invoice\n AND NOT EXISTS (SELECT 1\n FROM public.plg_financial_invoice_installments i\n JOIN public.plg_financial_allocations a ON a.installment_id = i.id\n WHERE i.invoice_id = f.id)\n LIMIT 1;\n IF v_dup IS NULL THEN\n RETURN NULL;\n END IF;\n\n -- Same three writes as the unpaid branch of financial_cancel_invoice_items,\n -- which cannot be called here: it reads the tenant from a session.\n UPDATE public.plg_financial_invoices\n SET cancelled_at = now(),\n cancel_reason = 'Duplicata: a mesma ocorrência da recorrência já foi lançada como ' || v_number,\n metadata = metadata || jsonb_build_object('v2_decision',\n jsonb_build_object('cancelled', true, 'at', now(), 'duplicate_of', v_kept))\n WHERE id = v_dup;\n UPDATE public.plg_financial_invoice_installments\n SET cancelled_at = now(),\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('cancelled', true))\n WHERE invoice_id = v_dup AND cancelled_at IS NULL;\n UPDATE public.plg_financial_movements SET status = 'cancelled', updated_at = now()\n WHERE financial_invoice_id = v_dup AND movement_kind = 'bill' AND status <> 'cancelled';\n\n RETURN v_dup;\nEND $function$;\nREVOKE ALL ON FUNCTION migration._fin_cancel_recurring_duplicate(uuid, text) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration._fin_cancel_recurring_duplicate(uuid, text) TO service_role;\n\n-- The two writers are patched in place, as 122 patched the money RPCs: their\n-- live bodies differ from the files that created them.\nDO $patch$\nDECLARE\n v_fn regprocedure;\n v_def text;\n v_anchor text;\n v_call text;\nBEGIN\n FOR v_fn, v_anchor, v_call IN\n SELECT f::regprocedure, a, c FROM (VALUES\n ('migration.upsert_fin_recurring_bill_occurrence(uuid, uuid, jsonb, jsonb)',\n ' IF v_standalone THEN PERFORM migration._audit(p_tenant, ''public.plg_financial_recurring_bill_occurrences'', p_id, ''{}''::jsonb); END IF;',\n ' PERFORM migration._fin_cancel_recurring_duplicate(p_tenant, v_meta -> ''v1'' ->> ''invoice_id'');'),\n ('migration.upsert_invoice(uuid, uuid, jsonb, jsonb)',\n ' IF v_standalone THEN PERFORM migration._audit(p_tenant, v_table, p_id, jsonb_build_object(''reason'', ''created'', ''installments'', v_n)); END IF;',\n ' PERFORM migration._fin_cancel_recurring_duplicate(p_tenant, p_row -> ''metadata'' -> ''migration'' ->> ''source_numeric_id'');')\n ) AS t(f, a, c)\n LOOP\n v_def := pg_get_functiondef(v_fn);\n IF position('_fin_cancel_recurring_duplicate' in v_def) > 0 THEN\n CONTINUE; -- already asks (a replay, or a later rewrite)\n END IF;\n IF position(v_anchor in v_def) = 0 THEN\n RAISE EXCEPTION '123: % no longer has the line the duplicate check goes before (%); patch it by hand', v_fn, v_anchor;\n END IF;\n EXECUTE overlay(v_def PLACING v_call || E'\\\\n' || v_anchor\n FROM position(v_anchor in v_def) FOR length(v_anchor));\n END LOOP;\nEND $patch$;\n`\n\nexport const MIGRATION_124_MONEY_KNOWS_ITS_CURRENCY = `-- ============================================================================\n-- 124 — money knows its currency\n--\n-- Invoices carried a \\`currency\\` nobody set (always the 'BRL' default) and the\n-- ledger had none at all: an account, a payment and a balance were bare\n-- numbers. One tenant with a shop in Rio and one in Barcelona would have had\n-- 30 EUR and 30 BRL add up to 60.\n--\n-- · an invoice takes the currency of the order it bills, else its unit's;\n-- · a bank or cash account has ONE currency (its unit's, unless said);\n-- · a money movement is in the currency of the account the money went\n-- through, and may not settle an invoice written in another one;\n-- · every movement and invoice is stamped with the day's rate to the\n-- tenant's own currency, when a rate is on file, so a report converted\n-- today still says tomorrow what it said today.\n--\n-- Rates are data the tenant keeps (\\`plg_financial_fx_rates\\`). Nothing here\n-- fetches one, and nothing here converts a report: the reading side comes\n-- after the data is right.\n--\n-- Additive. Every existing row reads as BRL, which is what it was.\n-- ============================================================================\n\n-- ── 1. rates ───────────────────────────────────────────────────────────────\nCREATE TABLE IF NOT EXISTS public.plg_financial_fx_rates (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n rate_date date NOT NULL,\n base_currency text NOT NULL CHECK (base_currency ~ '^[A-Z]{3}$'),\n quote_currency text NOT NULL CHECK (quote_currency ~ '^[A-Z]{3}$'),\n rate numeric NOT NULL CHECK (rate > 0),\n source text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n CHECK (base_currency <> quote_currency)\n);\nCOMMENT ON TABLE public.plg_financial_fx_rates IS '1 base_currency = rate quote_currency, on rate_date.';\nCREATE UNIQUE INDEX IF NOT EXISTS plg_financial_fx_rates_day_uq\n ON public.plg_financial_fx_rates (tenant_id, base_currency, quote_currency, rate_date);\n\nDO $$\nBEGIN\n CREATE TRIGGER plg_financial_fx_rates_updated_at\n BEFORE UPDATE ON public.plg_financial_fx_rates\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\nSELECT app.scaffold_table('public.plg_financial_fx_rates'::regclass,\n 'financial.invoice', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_fx_rates'::regclass, 'tenant', true);\n\n-- The latest rate on or before the day, in either direction. NULL when the\n-- tenant has none on file: a missing rate is not 1.\nCREATE OR REPLACE FUNCTION public.plg_financial_fx_rate(p_tenant uuid, p_from text, p_to text, p_on date DEFAULT NULL)\n RETURNS numeric\n LANGUAGE sql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\n SELECT CASE\n WHEN p_from IS NULL OR p_to IS NULL THEN NULL\n WHEN upper(p_from) = upper(p_to) THEN 1::numeric\n ELSE (\n SELECT CASE WHEN r.base_currency = upper(p_from) THEN r.rate ELSE round(1 / r.rate, 8) END\n FROM public.plg_financial_fx_rates r\n WHERE r.tenant_id = p_tenant\n AND r.rate_date <= coalesce(p_on, current_date)\n AND ((r.base_currency = upper(p_from) AND r.quote_currency = upper(p_to))\n OR (r.base_currency = upper(p_to) AND r.quote_currency = upper(p_from)))\n ORDER BY r.rate_date DESC, (r.base_currency = upper(p_from)) DESC\n LIMIT 1)\n END;\n$function$;\nREVOKE ALL ON FUNCTION public.plg_financial_fx_rate(uuid, text, text, date) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_financial_fx_rate(uuid, text, text, date) TO service_role;\n\n-- ── 2. columns ─────────────────────────────────────────────────────────────\n-- Added with the default so every existing row becomes BRL without a rewrite,\n-- then the default goes: a new row is filled by its trigger, from its unit.\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'plg_financial_accounts' AND column_name = 'currency') THEN\n ALTER TABLE public.plg_financial_accounts ADD COLUMN currency text NOT NULL DEFAULT 'BRL';\n ALTER TABLE public.plg_financial_accounts ALTER COLUMN currency DROP DEFAULT;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'plg_financial_movements' AND column_name = 'currency') THEN\n ALTER TABLE public.plg_financial_movements ADD COLUMN currency text NOT NULL DEFAULT 'BRL';\n ALTER TABLE public.plg_financial_movements ALTER COLUMN currency DROP DEFAULT;\n END IF;\nEND $$;\nALTER TABLE public.plg_financial_movements ADD COLUMN IF NOT EXISTS fx_rate numeric;\nALTER TABLE public.plg_financial_movements ADD COLUMN IF NOT EXISTS base_amount numeric(14,2);\n-- A pool that took the first cut of this file has it bare; money declares its scale.\nDO $scale$\nBEGIN\n IF (SELECT format_type(a.atttypid, a.atttypmod) FROM pg_attribute a\n WHERE a.attrelid = 'public.plg_financial_movements'::regclass AND a.attname = 'base_amount') = 'numeric' THEN\n ALTER TABLE public.plg_financial_movements ALTER COLUMN base_amount TYPE numeric(14,2);\n END IF;\nEND $scale$;\nALTER TABLE public.plg_financial_invoices ADD COLUMN IF NOT EXISTS fx_rate numeric;\nALTER TABLE public.plg_financial_invoices ALTER COLUMN currency DROP DEFAULT;\n\nCOMMENT ON COLUMN public.plg_financial_accounts.currency IS 'The one currency this account holds. Money in another currency arrives by a transfer with a rate.';\nCOMMENT ON COLUMN public.plg_financial_movements.currency IS 'Currency of \\`amount\\`: the account the money went through.';\nCOMMENT ON COLUMN public.plg_financial_movements.fx_rate IS 'Rate to the tenant''s own currency on the day, when one was on file.';\nCOMMENT ON COLUMN public.plg_financial_movements.base_amount IS 'amount × fx_rate, stored: what a consolidated report sums.';\nCOMMENT ON COLUMN public.plg_financial_invoices.fx_rate IS 'Rate to the tenant''s own currency on the issue date, when one was on file.';\n\n-- ── 3. an account is born in its unit's currency ───────────────────────────\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_accounts_currency()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nBEGIN\n IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN\n NEW.currency := app.unit_currency(NEW.tenant_id, NEW.unit_id);\n ELSE\n NEW.currency := upper(btrim(NEW.currency));\n END IF;\n RETURN NEW;\nEND $function$;\nDROP TRIGGER IF EXISTS plg_financial_accounts_currency ON public.plg_financial_accounts;\nCREATE TRIGGER plg_financial_accounts_currency\n BEFORE INSERT ON public.plg_financial_accounts\n FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_accounts_currency();\n\n-- ── 4. an invoice is in the currency of what it bills ──────────────────────\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_currency()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nBEGIN\n IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN\n IF NEW.order_id IS NOT NULL THEN\n SELECT nullif(btrim(o.currency), '') INTO NEW.currency FROM public.orders o WHERE o.id = NEW.order_id;\n END IF;\n NEW.currency := coalesce(NEW.currency, app.unit_currency(NEW.tenant_id, NEW.unit_id));\n END IF;\n NEW.currency := upper(btrim(NEW.currency));\n IF NEW.fx_rate IS NULL THEN\n NEW.fx_rate := public.plg_financial_fx_rate(\n NEW.tenant_id, NEW.currency, app.unit_currency(NEW.tenant_id, NULL), coalesce(NEW.issue_date, current_date));\n END IF;\n RETURN NEW;\nEND $function$;\n-- \"zz\": after \\`plg_financial_invoices_default_issuer\\`, which may set the unit.\nDROP TRIGGER IF EXISTS plg_financial_invoices_zz_currency ON public.plg_financial_invoices;\nCREATE TRIGGER plg_financial_invoices_zz_currency\n BEFORE INSERT ON public.plg_financial_invoices\n FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_invoices_currency();\n\n-- ── 5. a movement is in the currency of the account it went through ────────\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_movements_currency()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_holder text;\n v_invoice text;\nBEGIN\n IF NEW.financial_invoice_id IS NOT NULL THEN\n SELECT i.currency INTO v_invoice FROM public.plg_financial_invoices i WHERE i.id = NEW.financial_invoice_id;\n END IF;\n\n -- The holder is the leg that actually keeps money. System and person\n -- accounts are bookkeeping counterparts and have no currency of their own.\n SELECT a.currency INTO v_holder\n FROM public.plg_financial_accounts a\n WHERE a.id IN (NEW.debit_account_id, NEW.credit_account_id)\n AND a.kind NOT IN ('system', 'person')\n ORDER BY (a.id = NEW.bank_account_id) DESC\n LIMIT 1;\n\n IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN\n NEW.currency := coalesce(v_holder, v_invoice, app.unit_currency(NEW.tenant_id, NEW.unit_id));\n END IF;\n NEW.currency := upper(btrim(NEW.currency));\n\n -- A reversal mirrors the movement it undoes and is never the one refused.\n IF NEW.movement_kind = 'payment' AND NEW.reverses_id IS NULL\n AND v_holder IS NOT NULL AND v_invoice IS NOT NULL AND v_holder <> v_invoice THEN\n RAISE EXCEPTION 'Esta conta é em % e a conta bancária escolhida é em %. Pague por uma conta em % ou transfira com câmbio antes.',\n v_invoice, v_holder, v_invoice\n USING ERRCODE = '23514';\n END IF;\n\n IF NEW.fx_rate IS NULL THEN\n NEW.fx_rate := public.plg_financial_fx_rate(\n NEW.tenant_id, NEW.currency, app.unit_currency(NEW.tenant_id, NULL), coalesce(NEW.payment_date, NEW.due_date, current_date));\n END IF;\n IF NEW.base_amount IS NULL AND NEW.fx_rate IS NOT NULL THEN\n NEW.base_amount := round(NEW.amount * NEW.fx_rate, 2);\n END IF;\n RETURN NEW;\nEND $function$;\n-- Named to fire after \\`plg_financial_movements_before_insert\\`, which is the\n-- one that resolves the two legs this reads.\nDROP TRIGGER IF EXISTS plg_financial_movements_currency ON public.plg_financial_movements;\nCREATE TRIGGER plg_financial_movements_currency\n BEFORE INSERT ON public.plg_financial_movements\n FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_movements_currency();\n`\n\nexport const MIGRATION_125_THE_SUMMARY_ADDS_ONE_CURRENCY_AT_A_TIME = `-- ============================================================================\n-- 125 — the summary adds one currency at a time\n--\n-- \\`financial_summary\\` summed every amount it could see. With a purchase in\n-- reais and a sale in euros in the same tenant, the dashboard showed their sum\n-- as one number in whichever currency the screen printed.\n--\n-- The arithmetic now runs per currency (\\`financial_summary_by_currency\\`: one\n-- row each, with today's rate to the tenant's own currency), and\n-- \\`financial_summary\\` — same name, same nine columns, so every screen that\n-- reads it keeps working — returns the converted total.\n--\n-- An amount in a currency with no rate on file is LEFT OUT of that total, not\n-- counted at 1:1: the per-currency rows still carry it, and the screen says so.\n-- A tenant with one currency gets the row it always got, at rate 1.\n--\n-- The fence (who sees which movement and account) is the body of 077/085/086,\n-- unchanged.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.financial_summary_by_currency(p_from date DEFAULT NULL::date, p_to date DEFAULT NULL::date)\n RETURNS TABLE(currency text, house_currency text, fx_rate numeric,\n total_balance numeric, total_receivable numeric, total_payable numeric,\n monthly_inflow numeric, monthly_outflow numeric,\n overdue_receivable_count integer, overdue_receivable_amount numeric,\n overdue_payable_count integer, overdue_payable_amount numeric)\n LANGUAGE sql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\n WITH ctx AS (\n SELECT app.current_tenant_id() AS tenant,\n auth.uid() AS me,\n app.visible_units() AS units,\n app.has_permission('financial.read', NULL::uuid) AS read_null,\n app.has_permission('financial.read_all', NULL::uuid) AS read_all_null,\n app.owner_scoped('financial.movement') AS own_mov,\n app.owner_scoped('financial.account') AS own_acc,\n app.grants_exist('financial.movement', ARRAY['shared_with']) AS grants_mov,\n app.grants_exist('financial.account', ARRAY['shared_with']) AS grants_acc,\n coalesce(p_from, (current_date - 30)) AS de,\n coalesce(p_to, current_date) AS ate,\n app.unit_currency(app.current_tenant_id(), NULL::uuid) AS house\n ),\n u_read AS (\n SELECT u FROM ctx, unnest(ctx.units) u\n WHERE app.has_permission('financial.read', u)\n ),\n u_read_all AS (\n SELECT u FROM ctx, unnest(ctx.units) u\n WHERE app.has_permission('financial.read_all', u)\n ),\n mov AS NOT MATERIALIZED (\n SELECT m.*,\n CASE\n WHEN m.reverses_id IS NULL THEN\n (m.external_source IS NULL\n OR (m.reconciled_at IS NOT NULL AND m.matched_movement_id IS NULL))\n ELSE coalesce(\n (SELECT r.external_source IS NULL\n OR (r.reconciled_at IS NOT NULL AND r.matched_movement_id IS NULL)\n FROM public.plg_financial_movements r\n WHERE r.id = m.reverses_id),\n (m.external_source IS NULL\n OR (m.reconciled_at IS NOT NULL AND m.matched_movement_id IS NULL)))\n END AS caixa_visivel\n FROM public.plg_financial_movements m, ctx\n WHERE m.tenant_id = ctx.tenant\n AND (CASE WHEN m.unit_id IS NULL THEN ctx.read_null\n ELSE m.unit_id IN (SELECT u FROM u_read) END)\n AND (NOT ctx.own_mov\n OR m.owner_id IS NULL\n OR m.owner_id = ctx.me\n OR (ctx.grants_mov\n AND app.has_grant('financial.movement', m.id, ARRAY['shared_with']))\n OR (CASE WHEN m.unit_id IS NULL THEN ctx.read_all_null\n ELSE m.unit_id IN (SELECT u FROM u_read_all) END))\n ),\n acc AS (\n SELECT a.id, a.kind, a.currency\n FROM public.plg_financial_accounts a, ctx\n WHERE a.tenant_id = ctx.tenant\n AND a.is_active\n AND a.kind IN ('bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit')\n AND (CASE WHEN a.unit_id IS NULL THEN ctx.read_null\n ELSE a.unit_id IN (SELECT u FROM u_read) END)\n AND (NOT ctx.own_acc\n OR a.owner_id IS NULL\n OR a.owner_id = ctx.me\n OR (ctx.grants_acc\n AND app.has_grant('financial.account', a.id, ARRAY['shared_with']))\n OR (CASE WHEN a.unit_id IS NULL THEN ctx.read_all_null\n ELSE a.unit_id IN (SELECT u FROM u_read_all) END))\n ),\n -- A balance is in the currency of the ACCOUNT, whatever its movements say.\n saldo AS (\n SELECT acc.currency,\n coalesce(sum(\n coalesce((SELECT sum(d.amount) FROM mov d\n WHERE d.debit_account_id = acc.id\n AND d.status <> 'cancelled'\n AND d.movement_kind IN ('payment', 'transfer')\n AND d.caixa_visivel), 0)\n - coalesce((SELECT sum(c.amount) FROM mov c\n WHERE c.credit_account_id = acc.id\n AND c.status <> 'cancelled'\n AND c.movement_kind IN ('payment', 'transfer')\n AND c.caixa_visivel), 0)\n ), 0) AS v\n FROM acc\n GROUP BY acc.currency\n ),\n aberto AS (\n SELECT m.currency,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'), 0) AS receber,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'), 0) AS pagar,\n count(*) FILTER (WHERE m.direction = 'credit'\n AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_receber_n,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'\n AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_receber_v,\n count(*) FILTER (WHERE m.direction = 'debit'\n AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_pagar_n,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'\n AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_pagar_v\n FROM mov m\n WHERE m.status IN ('pending', 'partial', 'overdue')\n GROUP BY m.currency\n ),\n caixa AS (\n SELECT m.currency,\n coalesce(sum(m.paid_amount) FILTER (WHERE m.direction = 'credit'), 0) AS entrada,\n coalesce(sum(m.paid_amount) FILTER (WHERE m.direction = 'debit'), 0) AS saida\n FROM mov m, ctx\n WHERE m.movement_kind = 'payment'\n AND m.payment_date >= ctx.de AND m.payment_date <= ctx.ate\n AND m.caixa_visivel\n GROUP BY m.currency\n ),\n cur AS (\n SELECT s.currency FROM saldo s\n UNION SELECT a.currency FROM aberto a\n UNION SELECT c.currency FROM caixa c\n UNION SELECT ctx.house FROM ctx\n )\n SELECT cur.currency, ctx.house,\n public.plg_financial_fx_rate(ctx.tenant, cur.currency, ctx.house, current_date),\n coalesce(saldo.v, 0), coalesce(aberto.receber, 0), coalesce(aberto.pagar, 0),\n coalesce(caixa.entrada, 0), coalesce(caixa.saida, 0),\n coalesce(aberto.venc_receber_n, 0)::integer, coalesce(aberto.venc_receber_v, 0),\n coalesce(aberto.venc_pagar_n, 0)::integer, coalesce(aberto.venc_pagar_v, 0)\n FROM cur\n CROSS JOIN ctx\n LEFT JOIN saldo ON saldo.currency = cur.currency\n LEFT JOIN aberto ON aberto.currency = cur.currency\n LEFT JOIN caixa ON caixa.currency = cur.currency\n ORDER BY (cur.currency = ctx.house) DESC, cur.currency;\n$function$;\nREVOKE ALL ON FUNCTION public.financial_summary_by_currency(date, date) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_summary_by_currency(date, date) TO authenticated, service_role;\n\n-- Same door, same nine columns: the total in the tenant's own currency.\nCREATE OR REPLACE FUNCTION public.financial_summary(p_from date DEFAULT NULL::date, p_to date DEFAULT NULL::date)\n RETURNS TABLE(total_balance numeric, total_receivable numeric, total_payable numeric, monthly_inflow numeric, monthly_outflow numeric, overdue_receivable_count integer, overdue_receivable_amount numeric, overdue_payable_count integer, overdue_payable_amount numeric)\n LANGUAGE sql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\n SELECT coalesce(round(sum(s.total_balance * s.fx_rate), 2), 0),\n coalesce(round(sum(s.total_receivable * s.fx_rate), 2), 0),\n coalesce(round(sum(s.total_payable * s.fx_rate), 2), 0),\n coalesce(round(sum(s.monthly_inflow * s.fx_rate), 2), 0),\n coalesce(round(sum(s.monthly_outflow * s.fx_rate), 2), 0),\n coalesce(sum(s.overdue_receivable_count), 0)::integer,\n coalesce(round(sum(s.overdue_receivable_amount * s.fx_rate), 2), 0),\n coalesce(sum(s.overdue_payable_count), 0)::integer,\n coalesce(round(sum(s.overdue_payable_amount * s.fx_rate), 2), 0)\n FROM public.financial_summary_by_currency(p_from, p_to) s;\n$function$;\n`\n\nexport const MIGRATION_126_A_BILL_LINE_CAN_ENTER_STOCK = `-- ============================================================================\n-- 126 — a bill line can enter stock\n--\n-- A purchase was booked as a bill and the goods never reached stock unless\n-- someone typed them again on the stock screen. V1 had the gesture on the\n-- line; this is that gesture.\n--\n-- \\`financial_invoice_item_receive_stock\\` takes ONE line of a payable and\n-- receives it at a location, at the cost the line was billed at and in the\n-- bill's currency (the stock side converts to its unit's). A line that names\n-- no product is linked to one on the way — linking changes no value, so it is\n-- allowed on a bill that has already been paid.\n--\n-- The receipt is tied to the line, so the same line cannot enter twice, and\n-- \\`financial_invoice_stock_links\\` answers \"which lines entered\" without\n-- guessing from the invoice number (which \\`financial_invoice_stock_entries\\`\n-- still does for notes imported before this).\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.financial_invoice_item_receive_stock(\n p_item_id uuid, p_location_id uuid, p_product_id uuid DEFAULT NULL::uuid,\n p_quantity numeric DEFAULT NULL::numeric, p_fx_rate numeric DEFAULT NULL::numeric,\n p_idempotency_key text DEFAULT NULL::text)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_item public.items%ROWTYPE;\n v_inv public.plg_financial_invoices%ROWTYPE;\n v_product uuid;\n v_qty numeric;\n v_cost numeric;\n v_amend text := coalesce(current_setting('financial.amend', true), '');\n v_res jsonb;\nBEGIN\n SELECT * INTO v_item FROM public.items WHERE id = p_item_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_invoice_item_receive_stock: line % not found', p_item_id USING ERRCODE = 'P0002';\n END IF;\n SELECT f.* INTO v_inv FROM public.plg_financial_invoices f\n WHERE f.tenant_id = v_tenant\n AND (f.id = v_item.invoice_id OR (v_item.invoice_id IS NULL AND f.order_id = v_item.order_id))\n ORDER BY f.cancelled_at NULLS FIRST LIMIT 1;\n IF NOT FOUND OR v_inv.direction <> 'payable' THEN\n RAISE EXCEPTION 'Só a linha de uma conta a pagar dá entrada no estoque.' USING ERRCODE = '55000';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);\n IF v_inv.cancelled_at IS NOT NULL OR v_item.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'Esta linha está cancelada.' USING ERRCODE = '55000';\n END IF;\n IF to_regprocedure('public.inventory_receive(uuid,jsonb,jsonb,text)') IS NULL THEN\n RAISE EXCEPTION 'O estoque não está instalado nesta conta.' USING ERRCODE = '0A000';\n END IF;\n IF EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements m\n WHERE m.tenant_id = v_tenant AND m.source_item_id = v_item.id AND m.kind = 'in'\n AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)) THEN\n RAISE EXCEPTION 'Esta linha já deu entrada no estoque.' USING ERRCODE = '23505';\n END IF;\n\n v_product := coalesce(v_item.product_id, p_product_id);\n IF v_product IS NULL THEN\n RAISE EXCEPTION 'Escolha o produto desta linha antes de dar entrada.' USING ERRCODE = '22023';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.products p WHERE p.id = v_product AND p.tenant_id = v_tenant) THEN\n RAISE EXCEPTION 'Produto não encontrado nesta conta.' USING ERRCODE = '22023';\n END IF;\n IF v_item.product_id IS NULL THEN\n -- Naming the product changes no amount, so it passes the paid-bill lock.\n PERFORM set_config('financial.amend', 'on', true);\n UPDATE public.items SET product_id = v_product WHERE id = v_item.id;\n PERFORM set_config('financial.amend', v_amend, true);\n END IF;\n\n v_qty := coalesce(p_quantity, v_item.quantity);\n IF v_qty IS NULL OR v_qty <= 0 THEN\n RAISE EXCEPTION 'A quantidade precisa ser maior que zero.' USING ERRCODE = '22023';\n END IF;\n -- What the line cost per unit, discount included.\n v_cost := round(coalesce(v_item.total, v_item.unit_price * v_item.quantity, 0) / nullif(v_item.quantity, 0), 4);\n\n EXECUTE 'SELECT public.inventory_receive($1, $2, $3, $4)' INTO v_res USING\n p_location_id,\n jsonb_build_array(jsonb_strip_nulls(jsonb_build_object(\n 'product_id', v_product, 'variant_id', v_item.variant_id, 'quantity', v_qty, 'unit_cost', coalesce(v_cost, 0),\n 'source_item_type', 'invoice_item', 'source_item_id', v_item.id))),\n jsonb_strip_nulls(jsonb_build_object(\n 'type', 'manual', 'ref', v_inv.number, 'supplier_id', v_inv.counterparty_person_id,\n 'reason', 'Entrada pela conta ' || coalesce(v_inv.number, ''),\n 'currency', v_inv.currency, 'fx_rate', p_fx_rate)),\n coalesce(nullif(btrim(p_idempotency_key), ''), 'invoice-item:' || v_item.id::text);\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.item.receive_stock', 'items', v_item.id::text,\n jsonb_build_object('invoice_id', v_inv.id, 'invoice_number', v_inv.number, 'product_id', v_product,\n 'location_id', p_location_id, 'quantity', v_qty, 'unit_cost', v_cost, 'currency', v_inv.currency));\n RETURN jsonb_build_object('item_id', v_item.id, 'product_id', v_product, 'quantity', v_qty, 'receipt', v_res);\nEND $function$;\nREVOKE ALL ON FUNCTION public.financial_invoice_item_receive_stock(uuid, uuid, uuid, numeric, numeric, text) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_item_receive_stock(uuid, uuid, uuid, numeric, numeric, text) TO authenticated, service_role;\n\nCREATE OR REPLACE FUNCTION public.financial_invoice_stock_links(p_invoice_id uuid)\n RETURNS TABLE(item_id uuid, product_id uuid, location_id uuid, entered numeric, available numeric, entered_on date)\n LANGUAGE plpgsql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inv public.plg_financial_invoices%ROWTYPE;\nBEGIN\n SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;\n IF NOT FOUND OR v_inv.direction <> 'payable' THEN RETURN; END IF;\n PERFORM public.plg_financial_require_permission('financial.read', v_inv.unit_id);\n IF to_regclass('public.plg_inventory_stock_movements') IS NULL THEN RETURN; END IF;\n\n RETURN QUERY EXECUTE $q$\n SELECT m.source_item_id, m.product_id, m.source_location_id, sum(m.quantity),\n greatest(least(sum(m.quantity), coalesce((SELECT sum(sp.quantity) FROM public.plg_inventory_stock_positions sp\n WHERE sp.tenant_id = $1 AND sp.product_id = m.product_id\n AND sp.stock_location_id = m.source_location_id), 0)), 0),\n min(m.movement_date)::date\n FROM public.plg_inventory_stock_movements m\n WHERE m.tenant_id = $1 AND m.kind = 'in'\n AND m.source_item_id IN (SELECT i.id FROM public.items i\n WHERE i.tenant_id = $1 AND (i.invoice_id = $2 OR ($3::uuid IS NOT NULL AND i.order_id = $3)))\n AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)\n GROUP BY m.source_item_id, m.product_id, m.source_location_id\n $q$ USING v_tenant, p_invoice_id, v_inv.order_id;\nEND $function$;\nREVOKE ALL ON FUNCTION public.financial_invoice_stock_links(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_stock_links(uuid) TO authenticated, service_role;\n`\n\nexport const MIGRATION_127_A_BILL_LINE_ENTERS_STOCK_AS_IT_REALLY_ARRIVED = `-- ============================================================================\n-- 127 — a bill line enters stock as it really arrived\n--\n-- 126 received one line once, in the billed quantity, on the default variant.\n-- Goods do not arrive like the bill reads: 24 shirts billed, 27 delivered,\n-- some M, some G, some GG, and not always on the same day.\n--\n-- \\`financial_invoice_item_receive_stock_lines\\` takes any number of entries\n-- (variant + quantity) for one line, and may be called again later for what\n-- arrives next; each call is one receipt tied to the line. The quantity is\n-- what was counted, not what was billed. The unit cost is the billed one\n-- unless the caller says otherwise — when the supplier sent more for the same\n-- money, the real cost is lower, and only a person knows that.\n--\n-- The single-entry door of 126 stays, as a call to this one.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.financial_invoice_item_receive_stock_lines(\n p_item_id uuid, p_location_id uuid, p_lines jsonb,\n p_product_id uuid DEFAULT NULL::uuid, p_unit_cost numeric DEFAULT NULL::numeric,\n p_fx_rate numeric DEFAULT NULL::numeric, p_idempotency_key text DEFAULT NULL::text)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_item public.items%ROWTYPE;\n v_inv public.plg_financial_invoices%ROWTYPE;\n v_product uuid;\n v_cost numeric;\n v_amend text := coalesce(current_setting('financial.amend', true), '');\n v_line jsonb;\n v_qty numeric;\n v_total numeric := 0;\n v_variant uuid;\n v_out jsonb := '[]'::jsonb;\n v_res jsonb;\nBEGIN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n IF p_lines IS NULL OR jsonb_typeof(p_lines) <> 'array' OR jsonb_array_length(p_lines) = 0 THEN\n RAISE EXCEPTION 'Informe ao menos uma quantidade para dar entrada.' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_item FROM public.items WHERE id = p_item_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_invoice_item_receive_stock_lines: line % not found', p_item_id USING ERRCODE = 'P0002';\n END IF;\n SELECT f.* INTO v_inv FROM public.plg_financial_invoices f\n WHERE f.tenant_id = v_tenant\n AND (f.id = v_item.invoice_id OR (v_item.invoice_id IS NULL AND f.order_id = v_item.order_id))\n ORDER BY f.cancelled_at NULLS FIRST LIMIT 1;\n IF NOT FOUND OR v_inv.direction <> 'payable' THEN\n RAISE EXCEPTION 'Só a linha de uma conta a pagar dá entrada no estoque.' USING ERRCODE = '55000';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);\n IF v_inv.cancelled_at IS NOT NULL OR v_item.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'Esta linha está cancelada.' USING ERRCODE = '55000';\n END IF;\n IF to_regprocedure('public.inventory_receive(uuid,jsonb,jsonb,text)') IS NULL THEN\n RAISE EXCEPTION 'O estoque não está instalado nesta conta.' USING ERRCODE = '0A000';\n END IF;\n\n v_product := coalesce(v_item.product_id, p_product_id);\n IF v_product IS NULL THEN\n RAISE EXCEPTION 'Escolha o produto desta linha antes de dar entrada.' USING ERRCODE = '22023';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.products p WHERE p.id = v_product AND p.tenant_id = v_tenant) THEN\n RAISE EXCEPTION 'Produto não encontrado nesta conta.' USING ERRCODE = '22023';\n END IF;\n IF v_item.product_id IS NULL THEN\n -- Naming the product changes no amount, so it passes the paid-bill lock.\n PERFORM set_config('financial.amend', 'on', true);\n UPDATE public.items SET product_id = v_product WHERE id = v_item.id;\n PERFORM set_config('financial.amend', v_amend, true);\n END IF;\n\n -- The billed unit cost, discount included, unless the caller knows better.\n v_cost := coalesce(p_unit_cost,\n round(coalesce(v_item.total, v_item.unit_price * v_item.quantity, 0) / nullif(v_item.quantity, 0), 4), 0);\n IF v_cost < 0 THEN\n RAISE EXCEPTION 'O custo não pode ser negativo.' USING ERRCODE = '22023';\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(p_lines) LOOP\n BEGIN\n v_qty := (v_line ->> 'quantity')::numeric;\n v_variant := nullif(v_line ->> 'variant_id', '')::uuid;\n EXCEPTION WHEN OTHERS THEN\n RAISE EXCEPTION 'Entrada inválida: %', v_line USING ERRCODE = '22023';\n END;\n CONTINUE WHEN v_qty IS NULL OR v_qty = 0;\n IF v_qty < 0 THEN\n RAISE EXCEPTION 'A quantidade precisa ser maior que zero.' USING ERRCODE = '22023';\n END IF;\n v_total := v_total + v_qty;\n v_out := v_out || jsonb_strip_nulls(jsonb_build_object(\n 'product_id', v_product, 'variant_id', v_variant, 'quantity', v_qty, 'unit_cost', v_cost,\n 'source_item_type', 'invoice_item', 'source_item_id', v_item.id));\n END LOOP;\n IF v_total <= 0 THEN\n RAISE EXCEPTION 'Informe ao menos uma quantidade para dar entrada.' USING ERRCODE = '22023';\n END IF;\n\n EXECUTE 'SELECT public.inventory_receive($1, $2, $3, $4)' INTO v_res USING\n p_location_id, v_out,\n jsonb_strip_nulls(jsonb_build_object(\n 'type', 'manual', 'ref', v_inv.number, 'supplier_id', v_inv.counterparty_person_id,\n 'reason', 'Entrada pela conta ' || coalesce(v_inv.number, ''),\n 'currency', v_inv.currency, 'fx_rate', p_fx_rate)),\n -- No key from the caller means a new receipt each time: a line can be\n -- received more than once.\n coalesce(nullif(btrim(p_idempotency_key), ''), 'invoice-item:' || v_item.id::text || ':' || gen_random_uuid()::text);\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.item.receive_stock', 'items', v_item.id::text,\n jsonb_build_object('invoice_id', v_inv.id, 'invoice_number', v_inv.number, 'product_id', v_product,\n 'location_id', p_location_id, 'quantity', v_total, 'unit_cost', v_cost,\n 'currency', v_inv.currency, 'entries', jsonb_array_length(v_out)));\n RETURN jsonb_build_object('item_id', v_item.id, 'product_id', v_product, 'quantity', v_total, 'unit_cost', v_cost, 'receipt', v_res);\nEND $function$;\nREVOKE ALL ON FUNCTION public.financial_invoice_item_receive_stock_lines(uuid, uuid, jsonb, uuid, numeric, numeric, text) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_item_receive_stock_lines(uuid, uuid, jsonb, uuid, numeric, numeric, text) TO authenticated, service_role;\n\n-- 126's door: one entry, in the quantity given or the billed one.\nCREATE OR REPLACE FUNCTION public.financial_invoice_item_receive_stock(\n p_item_id uuid, p_location_id uuid, p_product_id uuid DEFAULT NULL::uuid,\n p_quantity numeric DEFAULT NULL::numeric, p_fx_rate numeric DEFAULT NULL::numeric,\n p_idempotency_key text DEFAULT NULL::text)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nBEGIN\n -- Same key, same lock as the door it delegates to (re-entrant in one transaction).\n PERFORM public.plg_financial_claim_idempotency(public.plg_financial_require_tenant(), p_idempotency_key);\n RETURN public.financial_invoice_item_receive_stock_lines(\n p_item_id, p_location_id,\n jsonb_build_array(jsonb_build_object('quantity', coalesce(p_quantity,\n (SELECT i.quantity FROM public.items i WHERE i.id = p_item_id)))),\n p_product_id, NULL::numeric, p_fx_rate, p_idempotency_key);\nEND $function$;\n\n-- Which variant each receipt was for, so the screen can say \"10 M, 9 G\".\n-- Dropped first: 128 widens what this returns, and OR REPLACE cannot narrow it back on a replay.\nDROP FUNCTION IF EXISTS public.financial_invoice_stock_link_details(uuid);\nCREATE OR REPLACE FUNCTION public.financial_invoice_stock_link_details(p_invoice_id uuid)\n RETURNS TABLE(item_id uuid, product_id uuid, variant_id uuid, variant_name text, location_id uuid,\n quantity numeric, unit_cost numeric, currency text, entered_on date)\n LANGUAGE plpgsql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inv public.plg_financial_invoices%ROWTYPE;\nBEGIN\n SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;\n IF NOT FOUND OR v_inv.direction <> 'payable' THEN RETURN; END IF;\n PERFORM public.plg_financial_require_permission('financial.read', v_inv.unit_id);\n IF to_regclass('public.plg_inventory_stock_movements') IS NULL THEN RETURN; END IF;\n\n RETURN QUERY EXECUTE $q$\n SELECT m.source_item_id, m.product_id, m.variant_id, v.name, m.source_location_id,\n sum(m.quantity), max(m.unit_cost), max(m.currency), min(m.movement_date)::date\n FROM public.plg_inventory_stock_movements m\n LEFT JOIN public.product_variants v ON v.id = m.variant_id\n WHERE m.tenant_id = $1 AND m.kind = 'in'\n AND m.source_item_id IN (SELECT i.id FROM public.items i\n WHERE i.tenant_id = $1 AND (i.invoice_id = $2 OR ($3::uuid IS NOT NULL AND i.order_id = $3)))\n AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)\n GROUP BY m.source_item_id, m.product_id, m.variant_id, v.name, m.source_location_id\n ORDER BY v.name NULLS FIRST\n $q$ USING v_tenant, p_invoice_id, v_inv.order_id;\nEND $function$;\nREVOKE ALL ON FUNCTION public.financial_invoice_stock_link_details(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_stock_link_details(uuid) TO authenticated, service_role;\n`\n\nexport const MIGRATION_128_A_STOCK_RECEIPT_SHOWS_WHAT_WAS_PAID = `-- ============================================================================\n-- 128 — a stock receipt shows what was paid\n--\n-- The bill is in reais and the stock is kept in euros. The ledger records both\n-- sides of every receipt (cost in the stock's currency, and what was paid, in\n-- which currency, at which rate), but the reading the bill's screen used only\n-- returned the converted cost — so the screen could not show the tie it was\n-- asked to prove.\n--\n-- Same reading, with the paid side, the rate and the location's name. The\n-- return type changes, so the function is dropped first; only the bill's\n-- screen calls it.\n-- ============================================================================\n\nDROP FUNCTION IF EXISTS public.financial_invoice_stock_link_details(uuid);\n\nCREATE FUNCTION public.financial_invoice_stock_link_details(p_invoice_id uuid)\n RETURNS TABLE(item_id uuid, product_id uuid, variant_id uuid, variant_name text,\n location_id uuid, location_name text, quantity numeric,\n unit_cost numeric, currency text,\n source_unit_cost numeric, source_currency text, fx_rate numeric,\n entered_on date)\n LANGUAGE plpgsql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inv public.plg_financial_invoices%ROWTYPE;\nBEGIN\n SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;\n IF NOT FOUND OR v_inv.direction <> 'payable' THEN RETURN; END IF;\n PERFORM public.plg_financial_require_permission('financial.read', v_inv.unit_id);\n IF to_regclass('public.plg_inventory_stock_movements') IS NULL THEN RETURN; END IF;\n\n RETURN QUERY EXECUTE $q$\n SELECT m.source_item_id, m.product_id, m.variant_id, v.name, m.source_location_id, l.name,\n sum(m.quantity), m.unit_cost, m.currency, m.source_unit_cost, m.source_currency, m.fx_rate,\n m.movement_date::date\n FROM public.plg_inventory_stock_movements m\n LEFT JOIN public.product_variants v ON v.id = m.variant_id\n LEFT JOIN public.plg_inventory_stock_locations l ON l.id = m.source_location_id\n WHERE m.tenant_id = $1 AND m.kind = 'in'\n AND m.source_item_id IN (SELECT i.id FROM public.items i\n WHERE i.tenant_id = $1 AND (i.invoice_id = $2 OR ($3::uuid IS NOT NULL AND i.order_id = $3)))\n AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)\n GROUP BY m.source_item_id, m.product_id, m.variant_id, v.name, v.sort_order, m.source_location_id, l.name,\n m.unit_cost, m.currency, m.source_unit_cost, m.source_currency, m.fx_rate, m.movement_date\n ORDER BY m.movement_date, v.sort_order NULLS FIRST\n $q$ USING v_tenant, p_invoice_id, v_inv.order_id;\nEND $function$;\nREVOKE ALL ON FUNCTION public.financial_invoice_stock_link_details(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_stock_link_details(uuid) TO authenticated, service_role;\n`\n\nexport const MIGRATION_129_A_BILL_CAN_BE_WRITTEN_IN_ANOTHER_CURRENCY = `-- ============================================================================\n-- 129 — a bill can be written in another currency\n-- ============================================================================\n-- An invoice took the currency of its order or of its unit, and nothing else.\n-- A house that sells in two currencies writes a bill by hand in either one, so\n-- the door now accepts the choice: \\`metadata.currency\\` on creation. An order\n-- still wins — a bill raised from a sale is in the currency of that sale.\n-- The choice is made once: an issued invoice's currency stays immutable.\n-- Idempotent: CREATE OR REPLACE only.\n\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_currency()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_asked text := upper(btrim(coalesce(NEW.metadata->>'currency', '')));\nBEGIN\n IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN\n IF NEW.order_id IS NOT NULL THEN\n SELECT nullif(btrim(o.currency), '') INTO NEW.currency FROM public.orders o WHERE o.id = NEW.order_id;\n END IF;\n IF NEW.currency IS NULL AND v_asked ~ '^[A-Z]{3}$' THEN\n NEW.currency := v_asked;\n END IF;\n NEW.currency := coalesce(NEW.currency, app.unit_currency(NEW.tenant_id, NEW.unit_id));\n END IF;\n NEW.currency := upper(btrim(NEW.currency));\n IF NEW.fx_rate IS NULL THEN\n NEW.fx_rate := public.plg_financial_fx_rate(\n NEW.tenant_id, NEW.currency, app.unit_currency(NEW.tenant_id, NULL), coalesce(NEW.issue_date, current_date));\n END IF;\n RETURN NEW;\nEND $function$;\n`\n\nexport const MIGRATION_130_A_BILL_CAN_BE_TAGGED = `-- ============================================================================\n-- 130 — a bill can be tagged\n-- ============================================================================\n-- Every record of the core has \\`tags\\` since spine 150: how THIS house slices\n-- its records, for a purpose the product did not foresee. Bills were the one\n-- place the operator looked for the tag and did not find it. Same shape as\n-- there — \\`text[]\\` read and written with the row, a GIN index for \"who has\n-- this one\". Tags are not part of what an issued invoice freezes.\n-- Idempotent: IF NOT EXISTS throughout.\n\nALTER TABLE public.plg_financial_invoices\n ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];\n\nCOMMENT ON COLUMN public.plg_financial_invoices.tags IS 'Free labels the house puts on a bill to slice its own lists.';\n\nCREATE INDEX IF NOT EXISTS plg_financial_invoices_tags_idx\n ON public.plg_financial_invoices USING gin (tags);\n`\n\nexport const MIGRATION_131_THE_BILL_FOLLOWS_THE_PRICE_OF_THE_SALE = `-- 131 — The bill follows the price of the sale.\n--\n-- A receivable is raised when the sale closes, at the total of that instant.\n-- Both bridges (\\`order_raise_receivable\\`, \\`fn_invoice_from_order_internal\\`) are\n-- idempotent: once the invoice exists they return without looking at the\n-- amount. So a price changed AFTER closing never reached the bill — a clinic\n-- had an appointment at R$ 700 whose receivable still asked for R$ 140, and\n-- the only symptom was two different numbers on two screens.\n--\n-- The rule, in one place:\n-- * nothing received yet → the invoice, its lines and its open instalments\n-- take the sale's total;\n-- * something received → nothing moves. A paid invoice is immutable\n-- (PRD 04, 120); the screens show both numbers and a person decides;\n-- * the sale drops to zero → the unpaid invoice is cancelled, not left at 0.\n--\n-- The lines follow the model the bill already has. A bill raised here owns a\n-- copy of the sale's lines (010) and that copy is refreshed. A bill ported\n-- from V1 has no lines of its own — \\`v_financial_invoice_lines\\` reads them\n-- from the order — and it stays that way: giving it a copy would freeze lines\n-- the sync still writes on the order.\n--\n-- Receivables of SELLING orders only. A purchase's bill diverges from its\n-- order on purpose (interest, supplier credit — 120).\n\n-- ── 1. the reconciliation ───────────────────────────────────────────────────\nCREATE OR REPLACE FUNCTION public.order_reconcile_receivable_internal(p_order_id uuid)\n RETURNS text\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_order public.orders%ROWTYPE;\n v_invoice public.plg_financial_invoices%ROWTYPE;\n v_total numeric;\n v_paid numeric;\n v_open_sum numeric;\n v_last uuid;\n v_summary text;\n v_own boolean;\nBEGIN\n SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;\n IF NOT FOUND OR NOT (v_order.kind = ANY (public.order_selling_kinds())) THEN\n RETURN 'none';\n END IF;\n\n -- (tenant_id, order_id) is the index; naming the tenant is what uses it.\n SELECT * INTO v_invoice FROM public.plg_financial_invoices\n WHERE tenant_id = v_order.tenant_id AND order_id = p_order_id\n AND cancelled_at IS NULL AND direction = 'receivable'\n ORDER BY created_at LIMIT 1;\n IF NOT FOUND THEN RETURN 'none'; END IF;\n\n -- A sale billed across several live invoices has no single bill that should\n -- carry its whole total. Nobody does this today; deciding it here would be a guess.\n IF (SELECT count(*) FROM public.plg_financial_invoices\n WHERE tenant_id = v_order.tenant_id AND order_id = p_order_id\n AND cancelled_at IS NULL AND direction = 'receivable') > 1 THEN\n RETURN 'none';\n END IF;\n\n v_total := round(coalesce(v_order.total, 0), 2);\n\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = v_invoice.id;\n\n v_own := EXISTS (SELECT 1 FROM public.items WHERE invoice_id = v_invoice.id);\n\n IF round(coalesce(v_invoice.total, 0), 2) = v_total THEN\n -- Same amount, but the lines may still be the old ones: a screen that\n -- writes the total and THEN the lines reaches here with the bill already\n -- at the new amount. A line with no live sale line behind it is stale.\n IF v_paid = 0 AND v_own AND (\n (SELECT count(*) FROM public.items WHERE invoice_id = v_invoice.id)\n IS DISTINCT FROM (SELECT count(*) FROM public.items WHERE order_id = p_order_id)\n OR EXISTS (\n SELECT 1 FROM public.items l\n WHERE l.invoice_id = v_invoice.id\n AND NOT EXISTS (\n SELECT 1 FROM public.item_associations a\n JOIN public.items s ON s.id = a.from_item_id\n WHERE a.to_item_id = l.id AND s.order_id = p_order_id AND s.total = l.total))\n ) THEN\n DELETE FROM public.items WHERE invoice_id = v_invoice.id;\n PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice.id);\n END IF;\n RETURN 'in_sync';\n END IF;\n\n IF v_paid > 0 THEN RETURN 'kept_paid'; END IF;\n\n IF v_total <= 0 THEN\n UPDATE public.plg_financial_invoices\n SET cancelled_at = now(),\n cancel_reason = coalesce(cancel_reason, 'The sale it was raised from no longer has an amount')\n WHERE id = v_invoice.id;\n RETURN 'amended';\n END IF;\n\n SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary\n FROM public.items WHERE order_id = p_order_id;\n\n UPDATE public.plg_financial_invoices\n SET total = v_total,\n description = coalesce(nullif(v_summary, ''), description)\n WHERE id = v_invoice.id;\n\n -- A stale copy is replaced whole. The copier refuses to run over existing\n -- lines, hence the delete first.\n IF v_own THEN\n DELETE FROM public.items WHERE invoice_id = v_invoice.id;\n PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice.id);\n END IF;\n\n -- The open instalments keep their proportions; the last one takes the cents.\n SELECT coalesce(sum(amount), 0) INTO v_open_sum\n FROM public.plg_financial_invoice_installments\n WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL;\n SELECT id INTO v_last FROM public.plg_financial_invoice_installments\n WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL\n ORDER BY number DESC LIMIT 1;\n\n IF v_last IS NULL THEN\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount)\n VALUES (v_invoice.tenant_id, v_invoice.unit_id, v_invoice.id,\n coalesce((SELECT max(number) FROM public.plg_financial_invoice_installments\n WHERE invoice_id = v_invoice.id), 0) + 1,\n current_date, v_total);\n ELSE\n IF v_open_sum > 0 THEN\n UPDATE public.plg_financial_invoice_installments\n SET amount = round(v_total * amount / v_open_sum, 2)\n WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL AND id <> v_last;\n END IF;\n UPDATE public.plg_financial_invoice_installments\n SET amount = v_total - coalesce((\n SELECT sum(amount) FROM public.plg_financial_invoice_installments\n WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL AND id <> v_last), 0)\n WHERE id = v_last;\n END IF;\n\n RETURN 'amended';\nEXCEPTION WHEN OTHERS THEN\n -- A closed period, or any other refusal from the books, never fails the\n -- sale's own write (same rule as raising the receivable, 001). The block\n -- rolls back whatever it had started, so the bill is left whole.\n RAISE WARNING 'order_reconcile_receivable: sale % kept its bill as it was: %', p_order_id, SQLERRM;\n RETURN 'kept_closed';\nEND $$;\n\nCOMMENT ON FUNCTION public.order_reconcile_receivable_internal(uuid) IS\n 'Makes an UNPAID receivable take its sale''s current total, lines and instalments. Returns none | in_sync | amended | kept_paid | kept_closed. A paid invoice is never touched (131).';\n\nREVOKE ALL ON FUNCTION public.order_reconcile_receivable_internal(uuid) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.order_reconcile_receivable_internal(uuid) TO service_role;\n\n-- ── 2. the door a screen uses after rewriting the sale's lines ──────────────\n-- The trigger below fires on the total; a screen that writes the total and\n-- THEN the lines (the agenda does) calls this once it is done, so the bill's\n-- lines are the final ones.\nCREATE OR REPLACE FUNCTION public.order_reconcile_receivable(p_order_id uuid)\n RETURNS text\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE v_tenant uuid;\nBEGIN\n SELECT tenant_id INTO v_tenant FROM public.orders WHERE id = p_order_id;\n IF v_tenant IS NULL OR v_tenant IS DISTINCT FROM app.current_tenant_id() THEN\n RAISE EXCEPTION 'order_reconcile_receivable: forbidden' USING ERRCODE = '42501';\n END IF;\n RETURN public.order_reconcile_receivable_internal(p_order_id);\nEND $$;\n\nCOMMENT ON FUNCTION public.order_reconcile_receivable(uuid) IS\n 'Tenant-checked door to order_reconcile_receivable_internal: it only ever aligns a bill with a sale the caller''s tenant already owns (131).';\n\nREVOKE ALL ON FUNCTION public.order_reconcile_receivable(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.order_reconcile_receivable(uuid) TO authenticated, service_role;\n\n-- ── 3. the guarantee: any writer of the total, known or future ──────────────\nCREATE OR REPLACE FUNCTION public.trg_order_total_reaches_its_invoice()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nBEGIN\n PERFORM public.order_reconcile_receivable_internal(NEW.id);\n RETURN NULL;\nEND $$;\n\nDROP TRIGGER IF EXISTS orders_total_reaches_the_invoice ON public.orders;\nCREATE TRIGGER orders_total_reaches_the_invoice\n AFTER UPDATE OF total ON public.orders\n FOR EACH ROW\n WHEN (NEW.total IS DISTINCT FROM OLD.total)\n EXECUTE FUNCTION public.trg_order_total_reaches_its_invoice();\n\n-- ── 4. raising again is the natural repair, and now it repairs the amount ───\nCREATE OR REPLACE FUNCTION public.order_raise_receivable(p_order_id uuid, p_due_date date DEFAULT NULL::date)\n RETURNS uuid\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_order public.orders%ROWTYPE;\n v_invoice_id uuid;\n v_number text;\n v_summary text;\n v_total numeric;\nBEGIN\n SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;\n IF NOT FOUND THEN RETURN NULL; END IF;\n\n SELECT id INTO v_invoice_id FROM public.plg_financial_invoices\n WHERE order_id = p_order_id AND cancelled_at IS NULL LIMIT 1;\n IF v_invoice_id IS NOT NULL THEN\n -- Re-raising stamps the lines on a bill that has none, as before (010),\n -- and now also repairs the amount.\n PERFORM public.order_reconcile_receivable_internal(p_order_id);\n PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);\n RETURN v_invoice_id;\n END IF;\n\n v_total := round(coalesce(v_order.total, 0), 2);\n IF v_total <= 0 THEN RETURN NULL; END IF;\n\n SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary\n FROM public.items WHERE order_id = p_order_id;\n\n v_number := public.plg_financial_next_invoice_number(v_order.tenant_id, 'receivable');\n\n INSERT INTO public.plg_financial_invoices\n (tenant_id, unit_id, direction, counterparty_person_id, order_id, number,\n competence_date, issue_date, total, description, metadata, created_by)\n VALUES\n (v_order.tenant_id, v_order.unit_id, 'receivable', v_order.party_id, p_order_id, v_number,\n coalesce(v_order.created_at::date, current_date), current_date,\n v_total, nullif(v_summary, ''),\n jsonb_build_object('source', 'order_raise_receivable', 'orderKind', v_order.kind),\n auth.uid())\n RETURNING id INTO v_invoice_id;\n\n PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);\n\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount)\n VALUES (v_order.tenant_id, v_order.unit_id, v_invoice_id, 1,\n coalesce(p_due_date, current_date), v_total);\n\n UPDATE public.orders SET\n stage = 'invoiced',\n direction = 'credit',\n metadata = coalesce(metadata, '{}'::jsonb)\n || jsonb_build_object('invoiceNumber', v_number, 'installmentCount', 1),\n updated_at = now()\n WHERE id = p_order_id;\n\n RETURN v_invoice_id;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.fn_invoice_from_order_internal(p_order_id uuid, p_due_date date DEFAULT NULL::date, p_status text DEFAULT NULL::text, p_installments integer DEFAULT 1, p_direction text DEFAULT 'credit'::text)\n RETURNS uuid\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_order public.orders%ROWTYPE;\n v_invoice_id uuid;\n v_dir text;\n v_number text;\n v_summary text;\n v_contact text;\n v_n int := GREATEST(COALESCE(p_installments, 1), 1);\n v_total numeric;\n v_base numeric;\n v_amt numeric;\n v_alloc numeric := 0;\n v_due date;\n v_status text;\n i int;\nBEGIN\n SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;\n IF NOT FOUND THEN RAISE EXCEPTION 'fn_invoice_from_order: order % not found', p_order_id; END IF;\n\n SELECT id INTO v_invoice_id FROM public.plg_financial_invoices\n WHERE order_id = p_order_id AND cancelled_at IS NULL LIMIT 1;\n IF v_invoice_id IS NOT NULL THEN\n -- Idempotent, and the natural repair: an order invoiced before 010 gets its\n -- lines, and one whose price moved since gets its amount (131). The\n -- reconciliation is a no-op on anything but an unpaid receivable.\n PERFORM public.order_reconcile_receivable_internal(p_order_id);\n PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);\n RETURN p_order_id;\n END IF;\n IF EXISTS (SELECT 1 FROM public.plg_financial_movements\n WHERE invoice_id = p_order_id AND movement_kind = 'bill'\n AND status <> 'cancelled') THEN\n RETURN p_order_id;\n END IF;\n\n v_dir := CASE WHEN p_direction = 'debit' THEN 'payable' ELSE 'receivable' END;\n v_number := public.plg_financial_next_invoice_number(v_order.tenant_id, v_dir);\n\n SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary\n FROM public.items WHERE order_id = p_order_id;\n SELECT name INTO v_contact FROM public.people WHERE id = v_order.party_id;\n\n v_status := v_order.status;\n IF p_status IS NOT NULL AND EXISTS (\n SELECT 1 FROM public.order_kinds k\n WHERE k.kind = v_order.kind\n AND (k.statuses IS NULL OR p_status = ANY (k.statuses))\n ) THEN\n v_status := p_status;\n END IF;\n\n UPDATE public.orders SET\n status = v_status,\n stage = 'invoiced',\n direction = p_direction,\n metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object(\n 'itemsSummary', COALESCE(v_summary, ''),\n 'contactName', v_contact,\n 'installmentCount', v_n,\n 'quoteNumber', v_order.reference_number,\n 'direction', p_direction,\n 'invoiceNumber', v_number\n ),\n updated_at = now()\n WHERE id = p_order_id;\n\n v_total := round(COALESCE(v_order.total, 0), 2);\n v_due := COALESCE(p_due_date, current_date);\n\n INSERT INTO public.plg_financial_invoices\n (tenant_id, unit_id, direction, counterparty_person_id, order_id, number, competence_date, issue_date,\n total, description, metadata, created_by)\n VALUES\n (v_order.tenant_id, v_order.unit_id, v_dir, v_order.party_id, p_order_id, v_number,\n COALESCE(v_order.created_at::date, current_date), current_date,\n v_total, NULLIF(v_summary, ''),\n jsonb_build_object('source', 'fn_invoice_from_order', 'orderKind', v_order.kind),\n auth.uid())\n RETURNING id INTO v_invoice_id;\n\n PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);\n\n v_base := round(v_total / v_n, 2);\n FOR i IN 1..v_n LOOP\n IF i = v_n THEN v_amt := round(v_total - v_alloc, 2);\n ELSE v_amt := v_base; v_alloc := v_alloc + v_base; END IF;\n\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount)\n VALUES (v_order.tenant_id, v_order.unit_id, v_invoice_id, i,\n (v_due + ((i - 1) || ' months')::interval)::date, v_amt);\n END LOOP;\n\n RETURN p_order_id;\nEND $$;\n`\n\nexport const MIGRATION_132_THE_STATEMENT_SOURCE_KNOWS_ITS_PROVIDER_ACCOUNT = `-- ---------------------------------------------------------------------------\n-- A statement source knows how its provider names the account.\n-- ---------------------------------------------------------------------------\n-- The V2 pulls Open Finance statements itself (financial-statement-sync), from\n-- the same PlugBank store the V1 reads. Rows there are keyed by an account\n-- hash (and the card's last four). \\`external_account_ref\\` was meant for it but\n-- the port filled it with the V1 row reference, and a re-port rewrites it; this\n-- column is the provider's key and nothing else writes it.\n--\n-- Backfilled from the lines already here: every PlugBank line carries the hash\n-- of the account it came from, and each account has exactly one.\n-- ---------------------------------------------------------------------------\n\nALTER TABLE public.plg_financial_account_statement_sources\n ADD COLUMN IF NOT EXISTS provider_account_hash text;\n\nCOMMENT ON COLUMN public.plg_financial_account_statement_sources.provider_account_hash IS\n 'PlugBank/Tecnospeed account_hash (V1: bank_account_openfinance.account_hash). Matches the provider rows to this account.';\n\nUPDATE public.plg_financial_account_statement_sources s\n SET provider_account_hash = h.hash, updated_at = now()\n FROM (\n SELECT l.tenant_id, l.bank_account_id, min(l.raw_payload ->> 'account_hash') AS hash\n FROM public.plg_financial_bank_statement_lines l\n WHERE l.raw_payload ? 'account_hash'\n GROUP BY 1, 2\n HAVING count(DISTINCT l.raw_payload ->> 'account_hash') = 1\n ) h\n WHERE h.tenant_id = s.tenant_id AND h.bank_account_id = s.account_id\n AND s.provider_account_hash IS NULL;\n`\n\nexport const MIGRATION_133_REVERSING_A_PAYMENT_RELEASES_ITS_BANK_LINE = `-- 133: reversing a payment also releases its bank-statement evidence. A\n-- reconciled payment that was reversed left its statement line \"conciliada\",\n-- pointing at money that was undone, so the line never came back to be matched\n-- again. The rest of the function is 116's, unchanged (a reversal is never\n-- reversed; reversing twice replays the first reversal).\n\nCREATE OR REPLACE FUNCTION public.financial_reverse_movement(p_movement_id uuid, p_reason text, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_m public.plg_financial_movements%ROWTYPE;\n v_rev uuid;\nBEGIN\n IF nullif(btrim(coalesce(p_reason, '')), '') IS NULL THEN\n RAISE EXCEPTION 'financial_reverse_movement: a reason is required' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_m FROM public.plg_financial_movements WHERE id = p_movement_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_reverse_movement: movement % not found', p_movement_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_m.unit_id);\n IF v_m.movement_kind NOT IN ('payment', 'transfer') OR v_m.debit_account_id IS NULL THEN\n RAISE EXCEPTION 'financial_reverse_movement: % is not a money event', p_movement_id USING ERRCODE = '55000';\n END IF;\n IF v_m.reverses_id IS NOT NULL THEN\n RAISE EXCEPTION 'Este lançamento já é um estorno e não pode ser estornado.' USING ERRCODE = '55000';\n END IF;\n IF v_m.reversed_by_id IS NOT NULL THEN\n RETURN jsonb_build_object('reversal_id', v_m.reversed_by_id, 'movement_id', v_m.id, 'replayed', true);\n END IF;\n IF p_idempotency_key IS NOT NULL THEN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT m.id INTO v_rev FROM public.plg_financial_movements m WHERE m.tenant_id = v_tenant AND m.idempotency_key = p_idempotency_key;\n IF v_rev IS NOT NULL THEN\n RETURN jsonb_build_object('reversal_id', v_rev, 'movement_id', v_m.id, 'replayed', true);\n END IF;\n END IF;\n\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, invoice_id, financial_invoice_id, financial_invoice_installment_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, method, cash_session_id, bank_account_id, reason, notes, metadata,\n idempotency_key, created_by, reverses_id, debit_account_id, credit_account_id)\n VALUES\n (v_m.tenant_id, v_m.unit_id, v_m.invoice_id, v_m.financial_invoice_id, v_m.financial_invoice_installment_id,\n CASE WHEN v_m.direction = 'credit' THEN 'debit' ELSE 'credit' END, v_m.movement_kind, v_m.amount, v_m.amount, 'paid',\n current_date, current_date, v_m.installment_number, v_m.method, v_m.cash_session_id, v_m.bank_account_id, btrim(p_reason),\n 'Estorno de ' || v_m.id::text,\n jsonb_build_object('source', 'financial_reverse_movement', 'reversal_of', v_m.id, 'reason', btrim(p_reason)),\n p_idempotency_key, auth.uid(), v_m.id, v_m.credit_account_id, v_m.debit_account_id)\n RETURNING id INTO v_rev;\n\n UPDATE public.plg_financial_movements SET reversed_by_id = v_rev, reason = coalesce(reason, btrim(p_reason)), updated_at = now()\n WHERE id = v_m.id;\n IF v_m.financial_invoice_installment_id IS NOT NULL THEN PERFORM public.plg_financial_sync_mirror(v_m.financial_invoice_installment_id); END IF;\n\n -- The bank line that proved this payment no longer proves anything: it goes\n -- back to the reconciliation queue instead of pointing at money that left.\n UPDATE public.plg_financial_bank_statement_lines\n SET matched_movement_id = NULL, reconciled_at = NULL\n WHERE tenant_id = v_tenant AND matched_movement_id = v_m.id;\n UPDATE public.plg_financial_movements SET reconciled_at = NULL WHERE id = v_m.id AND reconciled_at IS NOT NULL;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.movement.reverse', 'plg_financial_movements', v_m.id::text,\n jsonb_build_object('reversal_id', v_rev, 'amount', v_m.amount, 'reason', btrim(p_reason), 'idempotency_key', p_idempotency_key));\n RETURN jsonb_build_object('reversal_id', v_rev, 'movement_id', v_m.id, 'replayed', false);\nEND $$;\n\n`\n\nexport const MIGRATION_134_A_BILL_KNOWS_WHO_FILED_IT = `-- 134: a bill knows who filed it. \\`plg_financial_invoices.created_by\\` has been\n-- in the table since the baseline and the screens read it for the record\n-- footer (\"Criado em … por …\"), but nothing ever wrote it: movements and\n-- allocations stamp their author in a trigger, invoices never got one. Every\n-- bill came out authorless. From now on the signed-in person who inserts the\n-- row is its author; an explicit value (an import, an RPC that knows better)\n-- still wins. Nothing is backfilled — who filed an old bill is not knowable.\n-- Additive and idempotent.\n\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_created_by()\nRETURNS trigger\nLANGUAGE plpgsql\nSET search_path TO ''\nAS $$\nBEGIN\n NEW.created_by := coalesce(NEW.created_by, auth.uid());\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS plg_financial_invoices_created_by ON public.plg_financial_invoices;\nCREATE TRIGGER plg_financial_invoices_created_by\n BEFORE INSERT ON public.plg_financial_invoices\n FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_invoices_created_by();\n`\n\nexport const MIGRATION_135_A_BILL_AND_ITS_LINES_ARE_WRITTEN_TOGETHER = `-- 135: a bill's lines are written by the server, with the bill's own tenant and\n-- unit, in one statement. The client used to insert them into public.items\n-- itself, stamped with ITS idea of the active tenant; when that lagged behind\n-- the session (a workspace switch), RLS refused the lines, the error was\n-- ignored, and the bill kept its total with no lines. Replacing lines deleted\n-- the old ones first, so a refused insert lost them too. Here the delete and the\n-- insert are one transaction, under the same permission the bill itself needs.\n-- Additive and idempotent.\n\nCREATE OR REPLACE FUNCTION public.financial_set_invoice_lines(p_invoice_id uuid, p_lines jsonb, p_replace boolean DEFAULT false)\nRETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_invoice public.plg_financial_invoices%ROWTYPE;\n v_out jsonb;\nBEGIN\n SELECT * INTO v_invoice FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_set_invoice_lines: invoice % not found in this tenant', p_invoice_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission(CASE WHEN p_replace THEN 'financial.edit' ELSE 'financial.create' END, v_invoice.unit_id);\n IF jsonb_typeof(coalesce(p_lines, '[]'::jsonb)) <> 'array' THEN\n RAISE EXCEPTION 'financial_set_invoice_lines: lines must be an array' USING ERRCODE = '22023';\n END IF;\n\n -- Cancelled lines are history: they are never replaced.\n IF p_replace THEN\n DELETE FROM public.items WHERE invoice_id = p_invoice_id AND tenant_id = v_tenant AND cancelled_at IS NULL;\n END IF;\n\n WITH ins AS (\n INSERT INTO public.items (tenant_id, unit_id, invoice_id, name, quantity, unit_price, discount, total, sort_order, metadata)\n SELECT v_tenant, v_invoice.unit_id, p_invoice_id,\n nullif(l ->> 'name', ''),\n coalesce((l ->> 'quantity')::numeric, 1),\n coalesce((l ->> 'unit_price')::numeric, 0),\n coalesce((l ->> 'discount')::numeric, 0),\n coalesce((l ->> 'total')::numeric, 0),\n coalesce((l ->> 'sort_order')::int, (o - 1)::int),\n coalesce(l -> 'metadata', '{}'::jsonb)\n FROM jsonb_array_elements(coalesce(p_lines, '[]'::jsonb)) WITH ORDINALITY AS t(l, o)\n RETURNING id, sort_order\n )\n SELECT coalesce(jsonb_agg(jsonb_build_object('id', id, 'sort_order', sort_order) ORDER BY sort_order), '[]'::jsonb) INTO v_out FROM ins;\n RETURN v_out;\nEND $$;\nREVOKE ALL ON FUNCTION public.financial_set_invoice_lines(uuid, jsonb, boolean) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_set_invoice_lines(uuid, jsonb, boolean) TO authenticated, service_role;\n`\n\nexport const MIGRATION_136_A_BANK_STATEMENT_ARRIVES_FROM_THE_SERVICE = `-- ============================================================================\n-- 136 — a bank statement arrives from the integration service\n--\n-- The V2 sink of the banking module (ADR 0003): the doors through which the\n-- integration service hands this product the lines of a store's bank or\n-- credit-card statement, withdraws the ones the bank took back, reports the\n-- connection's state, and reads which lines are already here. The provider\n-- calls, the schedule and the waiting are the service's; what a line IS here\n-- is decided here.\n--\n-- A line is keyed as before: (tenant, external_source, external_id), where\n-- external_source is the statement source's provider ('openbanking:pagbank')\n-- and external_id the provider's transaction id. Lines already brought in by\n-- the V1 migration match the ones the service brings, so nothing doubles.\n--\n-- Providers revise statements for days: an Open Finance bank can drop a\n-- transaction and bring it back under a new id. A withdrawn line is marked\n-- (provider_deleted), never deleted, and comes back if the provider brings\n-- the same id again. A line already reconciled keeps its money and its date:\n-- a revision of either is recorded beside it, for a person to look at.\n--\n-- No person is behind these calls; the actor is the service, acting for the\n-- tenant it names. The functions are closed to every signed-in user and\n-- opened only to the service's role, \\`fayz_integration\\`, when it exists.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.financial_statement_source_get(p_tenant uuid, p_source uuid)\nRETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER\nSET search_path TO ''\nAS $$\n SELECT jsonb_build_object(\n 'id', s.id, 'tenantId', s.tenant_id, 'unitId', s.unit_id, 'accountId', s.account_id,\n 'provider', s.provider, 'kind', s.statement_kind, 'cardLast4', s.card_last4,\n 'accountRef', s.provider_account_hash, 'active', s.is_active,\n 'lastSyncedAt', s.last_synced_at, 'lastSyncStatus', s.last_sync_status\n )\n FROM public.plg_financial_account_statement_sources s\n WHERE s.id = p_source AND s.tenant_id = p_tenant\n$$;\n\n-- Every source tied to a provider account, across tenants: how the service\n-- finds the connections the V1 migration left, to adopt them.\nCREATE OR REPLACE FUNCTION public.financial_statement_sources_linked(p_provider_prefix text)\nRETURNS SETOF jsonb\nLANGUAGE sql STABLE SECURITY DEFINER\nSET search_path TO ''\nAS $$\n SELECT jsonb_build_object(\n 'id', s.id, 'tenantId', s.tenant_id, 'unitId', s.unit_id, 'accountId', s.account_id,\n 'provider', s.provider, 'kind', s.statement_kind, 'cardLast4', s.card_last4,\n 'accountRef', s.provider_account_hash, 'active', s.is_active\n )\n FROM public.plg_financial_account_statement_sources s\n WHERE s.provider LIKE p_provider_prefix || '%' AND s.provider_account_hash IS NOT NULL\n ORDER BY s.tenant_id, s.created_at\n$$;\n\n-- Ties a source to the provider's account, once the store has authorized it.\nCREATE OR REPLACE FUNCTION public.financial_statement_source_link(p_tenant uuid, p_source uuid, p_account_ref text)\nRETURNS void\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nBEGIN\n IF p_account_ref IS NULL OR btrim(p_account_ref) = '' THEN\n RAISE EXCEPTION 'financial_statement_source_link: account ref is required' USING ERRCODE = '22023';\n END IF;\n UPDATE public.plg_financial_account_statement_sources\n SET provider_account_hash = btrim(p_account_ref), updated_at = now()\n WHERE id = p_source AND tenant_id = p_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_statement_source_link: source % is not of tenant %', p_source, p_tenant USING ERRCODE = 'P0002';\n END IF;\nEND\n$$;\n\nCREATE OR REPLACE FUNCTION public.financial_statement_lines_ingest(p_tenant uuid, p_source uuid, p_lines jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_source public.plg_financial_account_statement_sources%ROWTYPE;\n v_line jsonb;\n v_amount numeric;\n v_day date;\n v_method text;\n v_meta jsonb;\n v_existing public.plg_financial_bank_statement_lines%ROWTYPE;\n v_inserted integer := 0;\n v_updated integer := 0;\nBEGIN\n SELECT * INTO v_source FROM public.plg_financial_account_statement_sources\n WHERE id = p_source AND tenant_id = p_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_statement_lines_ingest: source % is not of tenant %', p_source, p_tenant USING ERRCODE = 'P0002';\n END IF;\n IF jsonb_typeof(p_lines) <> 'array' THEN\n RAISE EXCEPTION 'financial_statement_lines_ingest: lines must be an array' USING ERRCODE = '22023';\n END IF;\n\n FOR v_line IN SELECT value FROM jsonb_array_elements(p_lines) LOOP\n IF coalesce(v_line ->> 'externalId', '') = '' OR (v_line ->> 'direction') NOT IN ('credit', 'debit') THEN\n RAISE EXCEPTION 'financial_statement_lines_ingest: a line needs externalId and direction' USING ERRCODE = '22023';\n END IF;\n v_amount := round((v_line ->> 'amountCents')::numeric / 100, 2);\n v_day := (v_line ->> 'postedOn')::date;\n IF v_amount IS NULL OR v_amount < 0 OR v_day IS NULL THEN\n RAISE EXCEPTION 'financial_statement_lines_ingest: line % needs a positive amount and a day', v_line ->> 'externalId' USING ERRCODE = '22023';\n END IF;\n v_method := nullif(v_line ->> 'paymentMethod', '');\n v_meta := jsonb_strip_nulls(jsonb_build_object(\n 'statement', jsonb_build_object('ref', v_line ->> 'statementRef', 'via', 'integration-service'),\n 'paymentMethod', v_method,\n 'currency', coalesce(v_line ->> 'currency', 'BRL'),\n 'boleto', v_line -> 'boleto',\n 'card', v_line -> 'card'\n ));\n\n SELECT * INTO v_existing FROM public.plg_financial_bank_statement_lines\n WHERE tenant_id = p_tenant AND external_source = v_source.provider AND external_id = v_line ->> 'externalId';\n\n IF NOT FOUND THEN\n INSERT INTO public.plg_financial_bank_statement_lines (\n tenant_id, unit_id, bank_account_id, external_source, external_id, posted_on, direction, amount,\n description, counterparty_name, counterparty_document, document, code, category, transaction_type,\n provider_deleted, raw_payload, metadata\n ) VALUES (\n p_tenant, v_source.unit_id, v_source.account_id, v_source.provider, v_line ->> 'externalId', v_day,\n v_line ->> 'direction', v_amount,\n nullif(v_line ->> 'description', ''), nullif(v_line #>> '{counterparty,name}', ''),\n nullif(v_line #>> '{counterparty,document}', ''), nullif(v_line ->> 'reference', ''),\n nullif(v_line ->> 'code', ''), nullif(v_line ->> 'category', ''),\n coalesce(v_method, upper(v_line ->> 'direction')),\n false, coalesce(v_line -> 'raw', '{}'::jsonb), v_meta\n );\n v_inserted := v_inserted + 1;\n ELSE\n UPDATE public.plg_financial_bank_statement_lines l SET\n -- Money, day and direction move only while nobody has reconciled the line.\n posted_on = CASE WHEN l.reconciled_at IS NULL THEN v_day ELSE l.posted_on END,\n amount = CASE WHEN l.reconciled_at IS NULL THEN v_amount ELSE l.amount END,\n direction = CASE WHEN l.reconciled_at IS NULL THEN v_line ->> 'direction' ELSE l.direction END,\n description = coalesce(nullif(v_line ->> 'description', ''), l.description),\n counterparty_name = coalesce(nullif(v_line #>> '{counterparty,name}', ''), l.counterparty_name),\n counterparty_document = coalesce(nullif(v_line #>> '{counterparty,document}', ''), l.counterparty_document),\n document = coalesce(nullif(v_line ->> 'reference', ''), l.document),\n code = coalesce(nullif(v_line ->> 'code', ''), l.code),\n category = coalesce(nullif(v_line ->> 'category', ''), l.category),\n transaction_type = coalesce(v_method, l.transaction_type),\n provider_deleted = false,\n raw_payload = coalesce(v_line -> 'raw', l.raw_payload),\n metadata = (l.metadata - 'withdrawn') || v_meta\n || CASE WHEN l.reconciled_at IS NOT NULL AND (l.amount <> v_amount OR l.posted_on <> v_day OR l.direction <> v_line ->> 'direction')\n THEN jsonb_build_object('revisedAfterReconciliation', jsonb_build_object(\n 'amount', v_amount, 'postedOn', v_day, 'direction', v_line ->> 'direction', 'at', now()))\n ELSE '{}'::jsonb END,\n updated_at = now()\n WHERE l.id = v_existing.id;\n v_updated := v_updated + 1;\n END IF;\n END LOOP;\n\n RETURN jsonb_build_object('inserted', v_inserted, 'updated', v_updated);\nEND\n$$;\n\nCREATE OR REPLACE FUNCTION public.financial_statement_lines_withdraw(p_tenant uuid, p_source uuid, p_external_ids text[], p_reason text)\nRETURNS integer\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_source public.plg_financial_account_statement_sources%ROWTYPE;\n v_count integer;\nBEGIN\n SELECT * INTO v_source FROM public.plg_financial_account_statement_sources\n WHERE id = p_source AND tenant_id = p_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_statement_lines_withdraw: source % is not of tenant %', p_source, p_tenant USING ERRCODE = 'P0002';\n END IF;\n UPDATE public.plg_financial_bank_statement_lines l\n SET provider_deleted = true,\n metadata = l.metadata || jsonb_build_object('withdrawn', jsonb_build_object('reason', p_reason, 'at', now())),\n updated_at = now()\n WHERE l.tenant_id = p_tenant AND l.external_source = v_source.provider\n AND l.bank_account_id = v_source.account_id\n AND l.external_id = ANY (p_external_ids) AND NOT l.provider_deleted;\n GET DIAGNOSTICS v_count = ROW_COUNT;\n RETURN v_count;\nEND\n$$;\n\n-- The lines here for a source in a window, withdrawn ones left out: what the\n-- service compares with the provider to find the ones the bank took back.\nCREATE OR REPLACE FUNCTION public.financial_statement_lines_known(p_tenant uuid, p_source uuid, p_from date, p_to date)\nRETURNS SETOF text\nLANGUAGE sql STABLE SECURITY DEFINER\nSET search_path TO ''\nAS $$\n SELECT l.external_id\n FROM public.plg_financial_bank_statement_lines l\n JOIN public.plg_financial_account_statement_sources s\n ON s.id = p_source AND s.tenant_id = p_tenant\n WHERE l.tenant_id = p_tenant AND l.external_source = s.provider AND l.bank_account_id = s.account_id\n AND l.posted_on BETWEEN p_from AND p_to AND NOT l.provider_deleted\n$$;\n\nCREATE OR REPLACE FUNCTION public.financial_statement_source_report(\n p_tenant uuid, p_source uuid, p_status text, p_error text, p_details jsonb\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nBEGIN\n IF p_status NOT IN ('ok', 'error', 'pending', 'revoked') THEN\n RAISE EXCEPTION 'financial_statement_source_report: unknown status %', p_status USING ERRCODE = '22023';\n END IF;\n UPDATE public.plg_financial_account_statement_sources s\n SET last_sync_status = p_status,\n last_sync_error = CASE WHEN p_status = 'error' THEN left(p_error, 500) ELSE NULL END,\n last_synced_at = CASE WHEN p_status = 'ok' THEN now() ELSE s.last_synced_at END,\n metadata = s.metadata || jsonb_build_object('connection', coalesce(p_details, '{}'::jsonb)),\n updated_at = now()\n WHERE s.id = p_source AND s.tenant_id = p_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_statement_source_report: source % is not of tenant %', p_source, p_tenant USING ERRCODE = 'P0002';\n END IF;\nEND\n$$;\n\nREVOKE ALL ON FUNCTION public.financial_statement_source_get(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.financial_statement_sources_linked(text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.financial_statement_source_link(uuid, uuid, text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.financial_statement_lines_ingest(uuid, uuid, jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.financial_statement_lines_withdraw(uuid, uuid, text[], text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.financial_statement_lines_known(uuid, uuid, date, date) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.financial_statement_source_report(uuid, uuid, text, text, jsonb) FROM PUBLIC, anon, authenticated;\n\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN\n GRANT USAGE ON SCHEMA public TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.financial_statement_source_get(uuid, uuid) TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.financial_statement_sources_linked(text) TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.financial_statement_source_link(uuid, uuid, text) TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.financial_statement_lines_ingest(uuid, uuid, jsonb) TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.financial_statement_lines_withdraw(uuid, uuid, text[], text) TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.financial_statement_lines_known(uuid, uuid, date, date) TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.financial_statement_source_report(uuid, uuid, text, text, jsonb) TO fayz_integration;\n END IF;\nEND\n$$;\n`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"000_baseline\", sql: MIGRATION_000_BASELINE },\n { id: \"001_the_comanda_becomes_a_receivable_when_it_closes\", sql: MIGRATION_001_THE_COMANDA_BECOMES_A_RECEIVABLE_WHEN_IT_CLOSES },\n { id: \"002_payment_types_are_the_country_not_the_tenant\", sql: MIGRATION_002_PAYMENT_TYPES_ARE_THE_COUNTRY_NOT_THE_TENANT },\n { id: \"003_the_commission_rule_gets_a_table\", sql: MIGRATION_003_THE_COMMISSION_RULE_GETS_A_TABLE },\n { id: \"004_one_bridge_for_three_doors\", sql: MIGRATION_004_ONE_BRIDGE_FOR_THREE_DOORS },\n { id: \"005_the_appointment_closes_like_the_comanda\", sql: MIGRATION_005_THE_APPOINTMENT_CLOSES_LIKE_THE_COMANDA },\n { id: \"006_the_month_can_be_closed_and_the_book_can_be_proven\", sql: MIGRATION_006_THE_MONTH_CAN_BE_CLOSED_AND_THE_BOOK_CAN_BE_PROVEN },\n { id: \"007_the_invoice_knows_who_issued_it_and_what_it_owes\", sql: MIGRATION_007_THE_INVOICE_KNOWS_WHO_ISSUED_IT_AND_WHAT_IT_OWES },\n { id: \"008_the_commission_rule_learns_to_choose_and_to_split\", sql: MIGRATION_008_THE_COMMISSION_RULE_LEARNS_TO_CHOOSE_AND_TO_SPLIT },\n { id: \"009_the_commission_is_calculated_line_by_line\", sql: MIGRATION_009_THE_COMMISSION_IS_CALCULATED_LINE_BY_LINE },\n { id: \"010_the_invoice_raised_from_a_sale_gets_its_own_lines\", sql: MIGRATION_010_THE_INVOICE_RAISED_FROM_A_SALE_GETS_ITS_OWN_LINES },\n { id: \"011_a_new_tenant_can_take_money_on_the_first_day\", sql: MIGRATION_011_A_NEW_TENANT_CAN_TAKE_MONEY_ON_THE_FIRST_DAY },\n { id: \"012_the_commissions_become_one_bill_per_person\", sql: MIGRATION_012_THE_COMMISSIONS_BECOME_ONE_BILL_PER_PERSON },\n { id: \"013_a_discount_has_a_reason_and_someone_who_allowed_it\", sql: MIGRATION_013_A_DISCOUNT_HAS_A_REASON_AND_SOMEONE_WHO_ALLOWED_IT },\n { id: \"014_the_bill_that_comes_back_every_month\", sql: MIGRATION_014_THE_BILL_THAT_COMES_BACK_EVERY_MONTH },\n { id: \"015_the_sale_knows_what_it_cost\", sql: MIGRATION_015_THE_SALE_KNOWS_WHAT_IT_COST },\n { id: \"016_the_overdue_bill_gets_a_letter\", sql: MIGRATION_016_THE_OVERDUE_BILL_GETS_A_LETTER },\n { id: \"017_the_receipt_is_a_query_not_a_layout\", sql: MIGRATION_017_THE_RECEIPT_IS_A_QUERY_NOT_A_LAYOUT },\n { id: \"018_the_caixa_shows_what_it_should_have\", sql: MIGRATION_018_THE_CAIXA_SHOWS_WHAT_IT_SHOULD_HAVE },\n { id: \"019_who_did_it_is_already_known\", sql: MIGRATION_019_WHO_DID_IT_IS_ALREADY_KNOWN },\n { id: \"020_a_rule_with_nobody_to_pay_pays_the_executor\", sql: MIGRATION_020_A_RULE_WITH_NOBODY_TO_PAY_PAYS_THE_EXECUTOR },\n { id: \"021_the_most_specific_rule_wins\", sql: MIGRATION_021_THE_MOST_SPECIFIC_RULE_WINS },\n { id: \"022_an_unknown_attribute_does_not_match\", sql: MIGRATION_022_AN_UNKNOWN_ATTRIBUTE_DOES_NOT_MATCH },\n { id: \"023_reapurar_nao_cria_um_lote_orfao\", sql: MIGRATION_023_REAPURAR_NAO_CRIA_UM_LOTE_ORFAO },\n { id: \"024_o_bordero_paga_o_que_ele_soma\", sql: MIGRATION_024_O_BORDERO_PAGA_O_QUE_ELE_SOMA },\n { id: \"025_a_meta_do_mes_tem_tres_degraus\", sql: MIGRATION_025_A_META_DO_MES_TEM_TRES_DEGRAUS },\n { id: \"026_quem_fecha_o_caixa_nao_e_quem_confere\", sql: MIGRATION_026_QUEM_FECHA_O_CAIXA_NAO_E_QUEM_CONFERE },\n { id: \"027_o_vale_presente_nasce_em_lote_e_e_um_passivo\", sql: MIGRATION_027_O_VALE_PRESENTE_NASCE_EM_LOTE_E_E_UM_PASSIVO },\n { id: \"028_o_cartao_tem_cor\", sql: MIGRATION_028_O_CARTAO_TEM_COR },\n { id: \"029_the_kiosk_sale_is_a_sale_too\", sql: MIGRATION_029_THE_KIOSK_SALE_IS_A_SALE_TOO },\n { id: \"030_the_census_can_see_the_kiosk\", sql: MIGRATION_030_THE_CENSUS_CAN_SEE_THE_KIOSK },\n { id: \"031_the_bank_line_is_evidence_not_a_second_payment\", sql: MIGRATION_031_THE_BANK_LINE_IS_EVIDENCE_NOT_A_SECOND_PAYMENT },\n { id: \"032_contas_a_pagar_ganham_onde_morar\", sql: MIGRATION_032_CONTAS_A_PAGAR_GANHAM_ONDE_MORAR },\n { id: \"033_o_vencimento_aponta_para_o_titulo\", sql: MIGRATION_033_O_VENCIMENTO_APONTA_PARA_O_TITULO },\n { id: \"034_a_unidade_tem_conta_e_a_pessoa_tem_uma_so\", sql: MIGRATION_034_A_UNIDADE_TEM_CONTA_E_A_PESSOA_TEM_UMA_SO },\n { id: \"035_a_classificacao_contabil_chega_inteira\", sql: MIGRATION_035_A_CLASSIFICACAO_CONTABIL_CHEGA_INTEIRA },\n { id: \"036_a_fusao_para_de_dobrar_a_propria_memoria\", sql: MIGRATION_036_A_FUSAO_PARA_DE_DOBRAR_A_PROPRIA_MEMORIA },\n { id: \"037_a_venda_de_balcao_tem_conta\", sql: MIGRATION_037_A_VENDA_DE_BALCAO_TEM_CONTA },\n { id: \"038_a_linha_da_fatura_alcanca_quem_veio_pelo_pedido\", sql: MIGRATION_038_A_LINHA_DA_FATURA_ALCANCA_QUEM_VEIO_PELO_PEDIDO },\n { id: \"039_o_titulo_de_uma_venda_cancelada_nasce_cancelado\", sql: MIGRATION_039_O_TITULO_DE_UMA_VENDA_CANCELADA_NASCE_CANCELADO },\n { id: \"040_a_dre_le_a_linha_da_venda\", sql: MIGRATION_040_A_DRE_LE_A_LINHA_DA_VENDA },\n { id: \"041_o_caixa_do_v1_nao_segura_o_movimento\", sql: MIGRATION_041_O_CAIXA_DO_V1_NAO_SEGURA_O_MOVIMENTO },\n { id: \"042_o_cancelado_do_v1_e_o_cancelled_do_v2\", sql: MIGRATION_042_O_CANCELADO_DO_V1_E_O_CANCELLED_DO_V2 },\n { id: \"043_o_movimento_sabe_de_que_unidade_ele_e\", sql: MIGRATION_043_O_MOVIMENTO_SABE_DE_QUE_UNIDADE_ELE_E },\n { id: \"044_a_dre_nao_paga_pedagio_por_item\", sql: MIGRATION_044_A_DRE_NAO_PAGA_PEDAGIO_POR_ITEM },\n { id: \"045_a_conta_so_classifica_se_a_natureza_casa\", sql: MIGRATION_045_A_CONTA_SO_CLASSIFICA_SE_A_NATUREZA_CASA },\n { id: \"046_o_demonstrativo_mostra_o_plano_inteiro\", sql: MIGRATION_046_O_DEMONSTRATIVO_MOSTRA_O_PLANO_INTEIRO },\n { id: \"047_a_conta_reconhece_a_contraparte_na_segunda_oferta\", sql: MIGRATION_047_A_CONTA_RECONHECE_A_CONTRAPARTE_NA_SEGUNDA_OFERTA },\n { id: \"048_a_parcela_e_a_conta_do_v1_e_a_baixa_e_a_alocacao\", sql: MIGRATION_048_A_PARCELA_E_A_CONTA_DO_V1_E_A_BAIXA_E_A_ALOCACAO },\n { id: \"049_a_parcela_carrega_o_cancelamento\", sql: MIGRATION_049_A_PARCELA_CARREGA_O_CANCELAMENTO },\n { id: \"050_o_saldo_do_titulo_se_calcula_uma_vez\", sql: MIGRATION_050_O_SALDO_DO_TITULO_SE_CALCULA_UMA_VEZ },\n { id: \"051_o_saldo_se_calcula_so_na_linha_que_a_tela_mostra\", sql: MIGRATION_051_O_SALDO_SE_CALCULA_SO_NA_LINHA_QUE_A_TELA_MOSTRA },\n { id: \"052_a_lista_de_contas_fala_vencimento\", sql: MIGRATION_052_A_LISTA_DE_CONTAS_FALA_VENCIMENTO },\n { id: \"053_um_titulo_parcelado_vence_varias_vezes\", sql: MIGRATION_053_UM_TITULO_PARCELADO_VENCE_VARIAS_VEZES },\n { id: \"054_as_datas_em_que_o_titulo_vence\", sql: MIGRATION_054_AS_DATAS_EM_QUE_O_TITULO_VENCE },\n { id: \"055_a_lista_de_contas_lista_parcelas\", sql: MIGRATION_055_A_LISTA_DE_CONTAS_LISTA_PARCELAS },\n { id: \"056_what_the_port_left_open\", sql: MIGRATION_056_WHAT_THE_PORT_LEFT_OPEN },\n { id: \"057_a_linha_diz_que_tem_recibo\", sql: MIGRATION_057_A_LINHA_DIZ_QUE_TEM_RECIBO },\n { id: \"058_o_clipe_usa_o_indice\", sql: MIGRATION_058_O_CLIPE_USA_O_INDICE },\n { id: \"059_o_clipe_sai_da_view\", sql: MIGRATION_059_O_CLIPE_SAI_DA_VIEW },\n { id: \"060_o_recebivel_volta_da_arvore_derrubada\", sql: MIGRATION_060_O_RECEBIVEL_VOLTA_DA_ARVORE_DERRUBADA },\n { id: \"061_contas_a_pagar_ganham_onde_morar\", sql: MIGRATION_061_CONTAS_A_PAGAR_GANHAM_ONDE_MORAR },\n { id: \"062_o_vencimento_aponta_para_o_titulo\", sql: MIGRATION_062_O_VENCIMENTO_APONTA_PARA_O_TITULO },\n { id: \"063_a_unidade_tem_conta_e_a_pessoa_tem_uma_so\", sql: MIGRATION_063_A_UNIDADE_TEM_CONTA_E_A_PESSOA_TEM_UMA_SO },\n { id: \"064_a_classificacao_contabil_chega_inteira\", sql: MIGRATION_064_A_CLASSIFICACAO_CONTABIL_CHEGA_INTEIRA },\n { id: \"065_a_fusao_para_de_dobrar_a_propria_memoria\", sql: MIGRATION_065_A_FUSAO_PARA_DE_DOBRAR_A_PROPRIA_MEMORIA },\n { id: \"066_a_venda_de_balcao_tem_conta\", sql: MIGRATION_066_A_VENDA_DE_BALCAO_TEM_CONTA },\n { id: \"067_o_recibo_da_conta_atravessa\", sql: MIGRATION_067_O_RECIBO_DA_CONTA_ATRAVESSA },\n { id: \"068_a_unidade_entra_com_a_linha_nao_depois_dela\", sql: MIGRATION_068_A_UNIDADE_ENTRA_COM_A_LINHA_NAO_DEPOIS_DELA },\n { id: \"069_o_titulo_que_ja_existe_esta_migrado\", sql: MIGRATION_069_O_TITULO_QUE_JA_EXISTE_ESTA_MIGRADO },\n { id: \"070_a_parcela_nasce_antes_do_movimento\", sql: MIGRATION_070_A_PARCELA_NASCE_ANTES_DO_MOVIMENTO },\n { id: \"071_a_parcela_tem_uma_assinatura_so\", sql: MIGRATION_071_A_PARCELA_TEM_UMA_ASSINATURA_SO },\n { id: \"072_o_caixa_do_v1_guarda_quem_o_abriu\", sql: MIGRATION_072_O_CAIXA_DO_V1_GUARDA_QUEM_O_ABRIU },\n { id: \"073_o_caixa_pertence_a_conta_que_o_v1_lhe_deu\", sql: MIGRATION_073_O_CAIXA_PERTENCE_A_CONTA_QUE_O_V1_LHE_DEU },\n { id: \"074_o_movimento_volta_a_apontar_para_o_caixa\", sql: MIGRATION_074_O_MOVIMENTO_VOLTA_A_APONTAR_PARA_O_CAIXA },\n { id: \"075_a_venda_cancelada_cancela_o_titulo_dela\", sql: MIGRATION_075_A_VENDA_CANCELADA_CANCELA_O_TITULO_DELA },\n { id: \"076_o_plano_de_contas_do_cliente_vence_o_do_modelo\", sql: MIGRATION_076_O_PLANO_DE_CONTAS_DO_CLIENTE_VENCE_O_DO_MODELO },\n { id: \"077_o_resumo_financeiro_se_soma_no_banco\", sql: MIGRATION_077_O_RESUMO_FINANCEIRO_SE_SOMA_NO_BANCO },\n { id: \"078_o_saldo_da_conta_se_calcula_so_na_conta_pedida\", sql: MIGRATION_078_O_SALDO_DA_CONTA_SE_CALCULA_SO_NA_CONTA_PEDIDA },\n { id: \"079_as_portas_de_porte_novas_nao_nascem_abertas\", sql: MIGRATION_079_AS_PORTAS_DE_PORTE_NOVAS_NAO_NASCEM_ABERTAS },\n { id: \"080_a_remessa_e_a_comissao_a_pagar_tem_onde_morar\", sql: MIGRATION_080_A_REMESSA_E_A_COMISSAO_A_PAGAR_TEM_ONDE_MORAR },\n { id: \"081_a_remessa_e_a_comissao_atravessam\", sql: MIGRATION_081_A_REMESSA_E_A_COMISSAO_ATRAVESSAM },\n { id: \"082_o_beneficiario_da_comissao_e_uma_conta\", sql: MIGRATION_082_O_BENEFICIARIO_DA_COMISSAO_E_UMA_CONTA },\n { id: \"083_a_conta_do_banco_e_uma_conta_e_a_fatura_tem_dois_destinos\", sql: MIGRATION_083_A_CONTA_DO_BANCO_E_UMA_CONTA_E_A_FATURA_TEM_DOIS_DESTINOS },\n { id: \"084_a_nota_de_entrada_tem_um_titulo_a_pagar\", sql: MIGRATION_084_A_NOTA_DE_ENTRADA_TEM_UM_TITULO_A_PAGAR },\n { id: \"085_o_resumo_le_cinco_mil_linhas_e_nao_duzentas_e_oitenta_mil\", sql: MIGRATION_085_O_RESUMO_LE_CINCO_MIL_LINHAS_E_NAO_DUZENTAS_E_OITENTA_MIL },\n { id: \"086_a_cerca_se_pergunta_uma_vez_e_nao_uma_vez_por_linha\", sql: MIGRATION_086_A_CERCA_SE_PERGUNTA_UMA_VEZ_E_NAO_UMA_VEZ_POR_LINHA },\n { id: \"087_o_saldo_de_uma_fatura_nao_soma_o_razao_inteiro\", sql: MIGRATION_087_O_SALDO_DE_UMA_FATURA_NAO_SOMA_O_RAZAO_INTEIRO },\n { id: \"088_a_conta_sem_pessoa_e_do_sistema_nao_de_ninguem\", sql: MIGRATION_088_A_CONTA_SEM_PESSOA_E_DO_SISTEMA_NAO_DE_NINGUEM },\n { id: \"089_o_fornecedor_aponta_para_o_plano_migrado\", sql: MIGRATION_089_O_FORNECEDOR_APONTA_PARA_O_PLANO_MIGRADO },\n { id: \"090_o_dre_recorta_antes_de_somar\", sql: MIGRATION_090_O_DRE_RECORTA_ANTES_DE_SOMAR },\n { id: \"091_a_recorrencia_repete_os_itens\", sql: MIGRATION_091_A_RECORRENCIA_REPETE_OS_ITENS },\n { id: \"092_a_conta_paga_no_cartao_sai_do_cartao\", sql: MIGRATION_092_A_CONTA_PAGA_NO_CARTAO_SAI_DO_CARTAO },\n { id: \"093_o_pagamento_diz_qual_conta_ele_quitou\", sql: MIGRATION_093_O_PAGAMENTO_DIZ_QUAL_CONTA_ELE_QUITOU },\n { id: \"094_o_titulo_da_nota_de_entrada_sabe_de_quem_ele_e\", sql: MIGRATION_094_O_TITULO_DA_NOTA_DE_ENTRADA_SABE_DE_QUEM_ELE_E },\n { id: \"095_o_caixa_do_v1_e_registro_nao_referencia\", sql: MIGRATION_095_O_CAIXA_DO_V1_E_REGISTRO_NAO_REFERENCIA },\n { id: \"096_o_caixa_do_v1_ja_tem_dono_e_pode_entrar\", sql: MIGRATION_096_O_CAIXA_DO_V1_JA_TEM_DONO_E_PODE_ENTRAR },\n { id: \"097_o_caixa_conta_linha_por_linha\", sql: MIGRATION_097_O_CAIXA_CONTA_LINHA_POR_LINHA },\n { id: \"098_a_conta_que_ninguem_pagou_ainda_muda_de_data_e_de_parcelas\", sql: MIGRATION_098_A_CONTA_QUE_NINGUEM_PAGOU_AINDA_MUDA_DE_DATA_E_DE_PARCELAS },\n { id: \"099_os_itens_de_uma_conta_sem_recebimento_ainda_se_corrigem\", sql: MIGRATION_099_OS_ITENS_DE_UMA_CONTA_SEM_RECEBIMENTO_AINDA_SE_CORRIGEM },\n { id: \"100_a_conta_bancaria_diz_de_quem_ela_e\", sql: MIGRATION_100_A_CONTA_BANCARIA_DIZ_DE_QUEM_ELA_E },\n { id: \"101_o_cartao_se_reconhece_pelos_quatro_ultimos\", sql: MIGRATION_101_O_CARTAO_SE_RECONHECE_PELOS_QUATRO_ULTIMOS },\n { id: \"102_a_bandeira_do_cartao_e_do_pais_nao_do_lojista\", sql: MIGRATION_102_A_BANDEIRA_DO_CARTAO_E_DO_PAIS_NAO_DO_LOJISTA },\n { id: \"103_o_rail_da_ficha_e_igual_em_todo_cadastro\", sql: MIGRATION_103_O_RAIL_DA_FICHA_E_IGUAL_EM_TODO_CADASTRO },\n { id: \"104_servico_e_o_que_a_casa_vende\", sql: MIGRATION_104_SERVICO_E_O_QUE_A_CASA_VENDE },\n { id: \"105_a_ocorrencia_provisionada_vira_conta_quando_alguem_manda\", sql: MIGRATION_105_A_OCORRENCIA_PROVISIONADA_VIRA_CONTA_QUANDO_ALGUEM_MANDA },\n { id: \"106_a_recorrencia_nova_tambem_provisiona\", sql: MIGRATION_106_A_RECORRENCIA_NOVA_TAMBEM_PROVISIONA },\n { id: \"107_a_previsao_comeca_onde_a_regua_comeca\", sql: MIGRATION_107_A_PREVISAO_COMECA_ONDE_A_REGUA_COMECA },\n { id: \"108_mudou_a_regua_muda_a_previsao\", sql: MIGRATION_108_MUDOU_A_REGUA_MUDA_A_PREVISAO },\n { id: \"109_a_regua_grava_camelCase_e_o_sql_lia_snake\", sql: MIGRATION_109_A_REGUA_GRAVA_CAMELCASE_E_O_SQL_LIA_SNAKE },\n { id: \"110_trocar_de_fornecedor_enquanto_ninguem_pagou\", sql: MIGRATION_110_TROCAR_DE_FORNECEDOR_ENQUANTO_NINGUEM_PAGOU },\n { id: \"111_reparcelar_sem_apagar_a_parcela\", sql: MIGRATION_111_REPARCELAR_SEM_APAGAR_A_PARCELA },\n { id: \"112_a_linha_do_extrato_nao_e_um_lancamento_ainda\", sql: MIGRATION_112_A_LINHA_DO_EXTRATO_NAO_E_UM_LANCAMENTO_AINDA },\n { id: \"113_so_a_conta_integrada_aparece_na_conciliacao\", sql: MIGRATION_113_SO_A_CONTA_INTEGRADA_APARECE_NA_CONCILIACAO },\n { id: \"114_a_parcela_diz_por_qual_conta_ela_foi_paga\", sql: MIGRATION_114_A_PARCELA_DIZ_POR_QUAL_CONTA_ELA_FOI_PAGA },\n { id: \"115_a_remessa_aponta_para_a_conta_que_existe\", sql: MIGRATION_115_A_REMESSA_APONTA_PARA_A_CONTA_QUE_EXISTE },\n { id: \"116_um_estorno_nao_se_estorna\", sql: MIGRATION_116_UM_ESTORNO_NAO_SE_ESTORNA },\n { id: \"117_copying_lines_is_an_internal_step\", sql: MIGRATION_117_COPYING_LINES_IS_AN_INTERNAL_STEP },\n { id: \"118_gift_card_found_by_exact_code\", sql: MIGRATION_118_GIFT_CARD_FOUND_BY_EXACT_CODE },\n { id: \"119_an_unpaid_legacy_bill_can_be_rescheduled\", sql: MIGRATION_119_AN_UNPAID_LEGACY_BILL_CAN_BE_RESCHEDULED },\n { id: \"120_cancelar_e_por_item_com_motivo_e_destino\", sql: MIGRATION_120_CANCELAR_E_POR_ITEM_COM_MOTIVO_E_DESTINO },\n { id: \"121_o_motivo_e_do_estabelecimento\", sql: MIGRATION_121_O_MOTIVO_E_DO_ESTABELECIMENTO },\n { id: \"122_o_cancelamento_fecha_as_portas_que_abriu\", sql: MIGRATION_122_O_CANCELAMENTO_FECHA_AS_PORTAS_QUE_ABRIU },\n { id: \"123_the_mirror_does_not_bill_a_recurrence_twice\", sql: MIGRATION_123_THE_MIRROR_DOES_NOT_BILL_A_RECURRENCE_TWICE },\n { id: \"124_money_knows_its_currency\", sql: MIGRATION_124_MONEY_KNOWS_ITS_CURRENCY },\n { id: \"125_the_summary_adds_one_currency_at_a_time\", sql: MIGRATION_125_THE_SUMMARY_ADDS_ONE_CURRENCY_AT_A_TIME },\n { id: \"126_a_bill_line_can_enter_stock\", sql: MIGRATION_126_A_BILL_LINE_CAN_ENTER_STOCK },\n { id: \"127_a_bill_line_enters_stock_as_it_really_arrived\", sql: MIGRATION_127_A_BILL_LINE_ENTERS_STOCK_AS_IT_REALLY_ARRIVED },\n { id: \"128_a_stock_receipt_shows_what_was_paid\", sql: MIGRATION_128_A_STOCK_RECEIPT_SHOWS_WHAT_WAS_PAID },\n { id: \"129_a_bill_can_be_written_in_another_currency\", sql: MIGRATION_129_A_BILL_CAN_BE_WRITTEN_IN_ANOTHER_CURRENCY },\n { id: \"130_a_bill_can_be_tagged\", sql: MIGRATION_130_A_BILL_CAN_BE_TAGGED },\n { id: \"131_the_bill_follows_the_price_of_the_sale\", sql: MIGRATION_131_THE_BILL_FOLLOWS_THE_PRICE_OF_THE_SALE },\n { id: \"132_the_statement_source_knows_its_provider_account\", sql: MIGRATION_132_THE_STATEMENT_SOURCE_KNOWS_ITS_PROVIDER_ACCOUNT },\n { id: \"133_reversing_a_payment_releases_its_bank_line\", sql: MIGRATION_133_REVERSING_A_PAYMENT_RELEASES_ITS_BANK_LINE },\n { id: \"134_a_bill_knows_who_filed_it\", sql: MIGRATION_134_A_BILL_KNOWS_WHO_FILED_IT },\n { id: \"135_a_bill_and_its_lines_are_written_together\", sql: MIGRATION_135_A_BILL_AND_ITS_LINES_ARE_WRITTEN_TOGETHER },\n { id: \"136_a_bank_statement_arrives_from_the_service\", sql: MIGRATION_136_A_BANK_STATEMENT_ARRIVES_FROM_THE_SERVICE },\n]\n"]}
|