@fayz-ai/plugin-clinical 0.4.14 → 0.4.16

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/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/migrations/index.ts","../src/settings/attendance-settings.ts","../src/index.ts"],"names":[],"mappings":";;;;;;AAIO,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;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAmiB3D,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;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AA0vBnD,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,CAAA;AAwExE,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;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAualD,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,CAAA;AAoF1D,IAAM,mCAAA,GAAsC,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,CAAA;AA2G5C,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;AAAA;;AAAA;AAAA,CAAA;AAyHhD,IAAM,oCAAA,GAAuC,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,CAAA;AAiE7C,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,CAAA;AAwD9C,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,CAAA;AAqGnD,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,CAAA;AA4E3D,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,CAAA;AA6EvD,IAAM,+BAAA,GAAkC,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,CAAA;AA0DxC,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,CAAA;AA6D/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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA,CAAA;AA2FpD,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,CAAA;AAsFpE,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,CAAA;AA0G7D,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,CAAA;AAuJvD,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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA,CAAA;AAyH1C,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,CAAA;AA6FxE,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,CAAA;AA8HjE,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,CAAA;AAqE/D,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,CAAA;AAwFtD,IAAM,oEAAA,GAAuE,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,CAAA;AAiM7E,IAAM,uDAAA,GAA0D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAgBhE,IAAM,UAAA,GAAiD;AAAA,EAC5D,EAAE,EAAA,EAAI,0CAAA,EAA4C,GAAA,EAAK,kDAAA,EAAmD;AAAA,EAC1G,EAAE,EAAA,EAAI,kCAAA,EAAoC,GAAA,EAAK,0CAAA,EAA2C;AAAA,EAC1F,EAAE,EAAA,EAAI,uDAAA,EAAyD,GAAA,EAAK,+DAAA,EAAgE;AAAA,EACpI,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,2BAAA,EAA6B,GAAA,EAAK,mCAAA,EAAoC;AAAA,EAC5E,EAAE,EAAA,EAAI,+BAAA,EAAiC,GAAA,EAAK,uCAAA,EAAwC;AAAA,EACpF,EAAE,EAAA,EAAI,4BAAA,EAA8B,GAAA,EAAK,oCAAA,EAAqC;AAAA,EAC9E,EAAE,EAAA,EAAI,6BAAA,EAA+B,GAAA,EAAK,qCAAA,EAAsC;AAAA,EAChF,EAAE,EAAA,EAAI,kCAAA,EAAoC,GAAA,EAAK,0CAAA,EAA2C;AAAA,EAC1F,EAAE,EAAA,EAAI,0CAAA,EAA4C,GAAA,EAAK,kDAAA,EAAmD;AAAA,EAC1G,EAAE,EAAA,EAAI,sCAAA,EAAwC,GAAA,EAAK,8CAAA,EAA+C;AAAA,EAClG,EAAE,EAAA,EAAI,uBAAA,EAAyB,GAAA,EAAK,+BAAA,EAAgC;AAAA,EACpE,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,mDAAA,EAAqD,GAAA,EAAK,2DAAA,EAA4D;AAAA,EAC5H,EAAE,EAAA,EAAI,4CAAA,EAA8C,GAAA,EAAK,oDAAA,EAAqD;AAAA,EAC9G,EAAE,EAAA,EAAI,sCAAA,EAAwC,GAAA,EAAK,8CAAA,EAA+C;AAAA,EAClG,EAAE,EAAA,EAAI,yBAAA,EAA2B,GAAA,EAAK,iCAAA,EAAkC;AAAA,EACxE,EAAE,EAAA,EAAI,uDAAA,EAAyD,GAAA,EAAK,+DAAA,EAAgE;AAAA,EACpI,EAAE,EAAA,EAAI,gDAAA,EAAkD,GAAA,EAAK,wDAAA,EAAyD;AAAA,EACtH,EAAE,EAAA,EAAI,8CAAA,EAAgD,GAAA,EAAK,sDAAA,EAAuD;AAAA,EAClH,EAAE,EAAA,EAAI,qCAAA,EAAuC,GAAA,EAAK,6CAAA,EAA8C;AAAA,EAChG,EAAE,EAAA,EAAI,4DAAA,EAA8D,GAAA,EAAK,oEAAA,EAAqE;AAAA,EAC9I,EAAE,EAAA,EAAI,+CAAA,EAAiD,GAAA,EAAK,uDAAA;AAC9D,CAAA;AC/rHO,IAAM,wBAAwB,iBAAA,CAAkB;AAAA,EACrD,EAAA,EAAI,2BAAA;AAAA,EACJ,KAAA,EAAO,EAAA,CAAG,OAAA,EAAS,QAAQ,CAAA;AAAA,EAC3B,IAAA,EAAM,gBAAA;AAAA,EACN,OAAA,EAAS,YAAA;AAAA,EACT,QAAQ,wBAAA,EAAyB;AAAA,EACjC,KAAA,EAAO,CAAA;AAAA,EACP,QAAA,EAAU,CAAC,EAAA,CAAG,kBAAA,EAAoB,uBAAuB,CAAA,EAAG,EAAA,CAAG,YAAA,EAAc,iBAAW,CAAC,CAAA;AAAA,EACzF,QAAA,EAAU;AAAA,IACR,6BAAA,EAA+B,KAAA;AAAA,IAC/B,qDAAA,EAAuD,KAAA;AAAA,IACvD,wCAAA,EAA0C;AAAA,GAC5C;AAAA,EACA,UAAU,CAAC;AAAA,IACT,WAAA,EAAa,EAAA,CAAG,4DAAA,EAA8D,2EAAwE,CAAA;AAAA,IACtJ,MAAA,EAAQ;AAAA,MACN,EAAE,GAAA,EAAK,6BAAA,EAA+B,IAAA,EAAM,QAAA,EAAU,OAAO,EAAA,CAAG,wBAAA,EAA0B,iCAAwB,CAAA,EAAG,OAAA,EAAS;AAAA,QAC5H,EAAE,KAAA,EAAO,KAAA,EAAO,OAAO,EAAA,CAAG,KAAA,EAAO,WAAW,CAAA,EAAE;AAAA,QAC9C,EAAE,KAAA,EAAO,QAAA,EAAU,OAAO,EAAA,CAAG,WAAA,EAAa,cAAW,CAAA,EAAE;AAAA,QACvD,EAAE,KAAA,EAAO,IAAA,EAAM,OAAO,EAAA,CAAG,SAAA,EAAW,QAAQ,CAAA;AAAE,OAChD,EAAE;AAAA,MACF,EAAE,KAAK,qDAAA,EAAuD,IAAA,EAAM,WAAW,KAAA,EAAO,EAAA,CAAG,oCAAA,EAAsC,sDAA6C,CAAA,EAAE;AAAA,MAC9K,EAAE,KAAK,wCAAA,EAA0C,IAAA,EAAM,WAAW,KAAA,EAAO,EAAA,CAAG,+CAAA,EAAiD,wDAAkD,CAAA;AAAE;AACnL,GACD;AACH,CAAC,CAAA;;;ACCM,SAAS,oBAAA,CAAqB,QAAA,GAAkC,EAAC,EAAmB;AACzF,EAAA,OAAO,YAAA,CAAa;AAAA,IAClB,EAAA,EAAI,UAAA;AAAA,IACJ,IAAA,EAAM,eAAA;AAAA,IACN,IAAA,EAAM,aAAA;AAAA,IACN,OAAA,EAAS,OAAA;AAAA,IACT,UAAA,EAAY,CAAA;AAAA,IACZ,KAAA,EAAO,OAAA;AAAA,IACP,cAAA,EAAgB,KAAA;AAAA,IAChB,YAAA,EAAc,CAAC,QAAQ,CAAA;AAAA,IACvB,YAAY,EAAC;AAAA,IACb,QAAQ,EAAC;AAAA,IACT,QAAA,EAAU,CAAC,qBAAqB,CAAA;AAAA,IAChC,UAAA,EAAY,UAAA,CAAW,GAAA,CAAI,CAAC,KAAA,MAAW;AAAA,MACrC,EAAA,EAAI,CAAA,SAAA,EAAY,KAAA,CAAM,EAAE,CAAA,CAAA;AAAA,MACxB,OAAA,EAAS,OAAA;AAAA,MACT,KAAK,KAAA,CAAM,GAAA;AAAA,MACX,WAAA,EAAa;AAAA,KACf,CAAE;AAAA,GACH,CAAA;AACH;AAEA,IAAO,WAAA,GAAQ","file":"index.js","sourcesContent":["// AUTO-GENERATED from 001_o_atendimento_clinico_tem_onde_morar.sql, 002_o_prontuario_do_v1_atravessa.sql, 003_a_permissao_se_pergunta_por_unidade_nao_por_linha.sql, 004_a_regua_volta_a_ser_apurada.sql, 005_comecar_o_atendimento_e_um_gesto_so.sql, 006_contratar_nao_e_pagar.sql, 007_finalizar_acende_o_balcao.sql, 008_conferir_nao_e_receber.sql, 009_o_botao_de_cobrar_cobra.sql, 010_a_indicacao_vai_para_o_funil.sql, 011_interesse_e_indicacao_na_mesma_linha.sql, 012_o_que_foi_usado_espera_aprovacao.sql, 013_o_que_couber_hoje.sql, 014_o_pago_a_executar_deixa_de_ser_invisivel.sql, 015_a_revisao_aponta_o_que_revisa.sql, 016_a_compra_antecipada_nao_tem_agendamento_atras.sql, 017_o_que_ja_foi_pago_nao_se_cobra_de_novo.sql, 018_o_que_foi_contratado_vira_sessao.sql, 019_adiar_e_uma_escolha.sql, 020_cobrar_um_agendamento_sem_pedido_levanta_o_pedido.sql, 021_agendamento_nascido_no_v2_tambem_tem_regua.sql, 022_a_vista_do_que_executar_respeita_a_cerca.sql, 023_a_revisao_se_escolhe_ao_agendar.sql, 024_a_avaliacao_desta_visita_vale_para_o_que_ela_contratou.sql, 025_as_regras_do_atendimento_sao_configuracao.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_001_O_ATENDIMENTO_CLINICO_TEM_ONDE_MORAR = `-- O atendimento clínico tem onde morar.\n--\n-- 358.577 linhas do Espaço Facial não tinham destino nenhum no V2 — 96% de tudo\n-- que ainda falta portar daquele tenant. Não era uma tabela esquecida: era um\n-- DOMÍNIO inteiro sem casa.\n--\n-- journey_executions 233.934 a sessão de execução\n-- execution_products 117.011 o produto consumido na sessão\n-- execution_requirements 6.590 o que trava começar e terminar\n-- service_revisions 110 a versão do serviço (preço, prazo)\n-- service_revision_forms 87 os formulários daquela versão\n-- prepaid_item_outcomes 30 o desfecho do item contratado\n--\n-- ── Por que não é a agenda ──────────────────────────────────────────────────\n--\n-- A agenda termina em QUEM, QUANDO e ONDE — \\`appointments\\` e\n-- \\`appointment_items\\`, que já atravessaram na migration 008. O que está aqui é\n-- O QUE FOI FEITO, e tem ciclo de vida próprio: revisão com pai\n-- (\\`parent_execution_id\\`), requisitos que bloqueiam início e fim, consumo de\n-- produto auditado contra o estoque, foto por pose.\n--\n-- A jornada está documentada em\n-- ~/dev/beautyplace/docs/v1-guide/jornadas/avaliacao-e-contratacao/FLOW.md\n-- (\\`v2.status: missing\\`): anamnese COM VALIDADE → avaliação com desenho do rosto\n-- e foto → indicação com quantidade → \"o que o cliente deseja contratar\" →\n-- contrato assinado e termo de sessão → executar só o que couber hoje → régua de\n-- etapas com cronômetro e evolução clínica.\n--\n-- É capacidade de VERTICAL (estética, saúde), não do núcleo. Por isso plugin\n-- próprio e entrada em \\`optional\\` no chain.json, como o workforce.\n--\n-- ── O que NÃO é deste domínio, apesar do nome ───────────────────────────────\n--\n-- \\`journeys\\` e \\`journey_steps\\` compartilham o prefixo com \\`journey_executions\\` e\n-- não têm relação nenhuma com ele: ZERO chave estrangeira entre os dois, e\n-- \\`journey_steps\\` aponta para \\`message_templates\\` carregando \\`wait_value\\`,\n-- \\`channel\\`, \\`quiet_hours_start\\` e \\`cooldown_days\\`. É automação de COMUNICAÇÃO,\n-- e o destino dela é \\`plg_automations_flows\\`, que já existe. São 7 linhas, e a\n-- tela delas no V1 nem abre (V1-027).\n--\n-- ── As colunas que ficaram para trás, e por quê ─────────────────────────────\n--\n-- Medido no tenant 1 em 21/09/2026:\n--\n-- \\`execution_products.stock_deducted\\` 0 de 117.015 verdadeiras\n-- \\`execution_products.stock_movement_id\\` 0 preenchidas\n-- \\`execution_products.stock_position_id\\` 0 preenchidas\n-- \\`position_allocations\\` / \\`unit_label_allocations\\` 0 preenchidas\n--\n-- O consumo NUNCA deu baixa no estoque neste tenant. Isso não é defeito do\n-- porte: é o que o próprio Guia do V1 descreve na jornada\n-- \\`auditoria-de-uso-e-estoque\\`, onde a auditoria é portão PRÉ-baixa e a baixa\n-- fica pendurada. Trazer colunas vazias faria a tela do V2 afirmar que existe um\n-- vínculo com o razão que nunca existiu. O dia em que houver baixa, ela vira\n-- \\`plg_inventory_stock_movements\\` com \\`subject_id\\` apontando para a sessão.\n--\n-- \\`journey_executions.product_id\\` 0 de 233.950 — coluna morta\n-- \\`journey_executions.status\\` redundante com \\`journey_status\\`, que\n-- é mais granular (separa quoted de\n-- contracted). Vai para metadata.\n-- \\`execution_photos\\` / \\`consumption_photos\\` são ARQUIVO, e arquivo no V2 é\n-- \\`public.documents\\`. São muitos por sessão, cada um com a sua pose, então o\n-- vínculo é \\`documents.subject_type\\`/\\`subject_id\\` — e não uma coluna de\n-- array que nenhuma RLS de arquivo alcança.\n--\n-- Idempotente.\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 1. A sessão\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_clinical_sessions (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n -- O \\`journey_status\\` do V1. Fechado num CHECK porque é vocabulário de\n -- plugin, não arquétipo — \\`schedule_kinds\\`/\\`appointment_kinds\\` são registros\n -- de espécie, e uma sessão clínica não é espécie de nada.\n status text DEFAULT 'pending'::text NOT NULL,\n -- O agendamento: 226.551 de 233.950 (96,8%). Os que faltam são sessão sem\n -- agenda — contratada e ainda não marcada.\n booking_id uuid,\n -- O item do agendamento: 2.511 de 233.950. Raro no V1 e mantido assim: o\n -- vínculo forte com o que foi vendido é \\`order_item_id\\`.\n booking_item_id uuid,\n party_id uuid,\n -- Quem estava marcado e quem de fato executou são duas perguntas. O V1\n -- guarda as duas (\\`professional_id\\`, \\`execution_professional_id\\`) e a\n -- segunda só existe em 148.414 — é ela que a comissão lê.\n assignee_id uuid,\n performed_by_id uuid,\n service_id uuid,\n order_item_id uuid,\n quote_item_id uuid,\n -- 233.281 de 233.950 (99,7%) têm oportunidade. A sessão pendura no NEGÓCIO\n -- antes de pendurar na agenda, e é por isso que ela existe para o que foi\n -- orçado e nunca marcado.\n deal_id uuid,\n revision_number integer DEFAULT 1 NOT NULL,\n parent_session_id uuid,\n source_evaluation_id uuid,\n scheduled_for date,\n started_at timestamp with time zone,\n ended_at timestamp with time zone,\n products_initialized boolean DEFAULT false NOT NULL,\n notes text,\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 CONSTRAINT plg_clinical_sessions_status_check CHECK (status = ANY (ARRAY[\n 'quoted'::text, 'contracted'::text, 'scheduled'::text, 'in_service'::text,\n 'in_progress'::text, 'executed'::text, 'pending'::text])),\n CONSTRAINT plg_clinical_sessions_revision_check CHECK (revision_number >= 1),\n -- A sessão termina depois de começar. O V1 não cobra isso e por isso a\n -- recusa vive no writer do porte, não aqui — mas daqui para frente vale.\n CONSTRAINT plg_clinical_sessions_window_check\n CHECK (started_at IS NULL OR ended_at IS NULL OR ended_at >= started_at)\n);\n\nCOMMENT ON TABLE public.plg_clinical_sessions IS\n 'A sessão de execução do atendimento clínico. V1: journey_executions — que NÃO tem relação com journeys, apesar do nome.';\nCOMMENT ON COLUMN public.plg_clinical_sessions.performed_by_id IS\n 'V1: execution_professional_id — quem de fato executou, que pode não ser quem estava marcado.';\nCOMMENT ON COLUMN public.plg_clinical_sessions.source_evaluation_id IS\n 'V1: source_evaluation_execution_id — a avaliação que originou esta indicação.';\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 2. O produto consumido\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_clinical_session_products (\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 product_id uuid NOT NULL,\n order_item_id uuid,\n assignee_id uuid,\n -- O \"Padrão: N\" da ficha técnica contra o que a profissional realmente usou.\n -- É a diferença entre os dois que a auditoria de consumo lê.\n default_quantity numeric(14,4),\n used_quantity numeric(14,4),\n used_unit_id uuid,\n executed_on date,\n is_audited boolean DEFAULT false NOT NULL,\n audited_at timestamp with time zone,\n audited_by uuid,\n audit_notes 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 CONSTRAINT plg_clinical_session_products_qty_check\n CHECK (used_quantity IS NULL OR used_quantity >= 0)\n);\n\nCOMMENT ON TABLE public.plg_clinical_session_products IS\n 'O produto consumido na sessão, com o padrão da ficha técnica ao lado do usado. V1: execution_products. As colunas de baixa de estoque não vieram: 0 de 117.015 estavam preenchidas.';\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 3. O que trava começar e terminar\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_clinical_requirements (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n session_id uuid,\n booking_id uuid,\n party_id uuid,\n service_id uuid,\n kind text NOT NULL,\n scope text NOT NULL,\n -- As duas travas do V1, e elas são diferentes: a anamnese impede COMEÇAR, o\n -- termo de sessão impede TERMINAR. 1.107 e 1.023 linhas no tenant 1.\n blocks_start boolean DEFAULT false NOT NULL,\n blocks_finish boolean DEFAULT false NOT NULL,\n status text DEFAULT 'pending'::text NOT NULL,\n -- O que resolveu a exigência é polimórfico no V1 (\\`resolved_table\\` +\n -- \\`resolved_id\\`) e continua sendo aqui, com o mesmo par que\n -- \\`public.documents\\` já usa. Um formulário resolve por uma linha de\n -- plg_forms_documents; um contrato, por outra.\n subject_type text,\n subject_id uuid,\n candidate_type text,\n candidate_id uuid,\n resolved_via text,\n resolved_at timestamp with time zone,\n -- A anamnese vale até uma data (\"Válido até 08/03/2027\"), e é isso que\n -- solta o resto da régua.\n valid_until timestamp with time zone,\n frozen_at timestamp with time zone,\n rule_sources 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 CONSTRAINT plg_clinical_requirements_kind_check CHECK (kind = ANY (ARRAY[\n 'form'::text, 'evaluation'::text, 'contract'::text, 'session_term'::text,\n 'products'::text, 'invoice_item'::text, 'revision_link'::text])),\n CONSTRAINT plg_clinical_requirements_scope_check\n CHECK (scope = ANY (ARRAY['service'::text, 'attendance'::text])),\n CONSTRAINT plg_clinical_requirements_status_check CHECK (status = ANY (ARRAY[\n 'pending'::text, 'resolved'::text, 'not_applicable'::text]))\n);\n\nCOMMENT ON TABLE public.plg_clinical_requirements IS\n 'A exigência que trava o início ou o fim do atendimento — anamnese, avaliação, contrato, termo, kit. V1: execution_requirements.';\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 4. A versão do serviço\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- O serviço muda de preço e de prazo, e o que foi contratado tem de continuar\n-- valendo o que valia. A revisão é o congelamento — e ela carrega os\n-- formulários que aquela versão exigia.\nCREATE TABLE IF NOT EXISTS public.plg_clinical_service_revisions (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n service_id uuid NOT NULL,\n revision_number integer NOT NULL,\n price numeric(14,2),\n duration_minutes integer,\n deadline_days integer,\n -- Os \\`service_revision_forms\\` daquela revisão. São 87 linhas em 110\n -- revisões: uma lista curta, sem vida própria e sem tela — jsonb em vez de\n -- uma quarta tabela que ninguém consulta sozinha.\n form_template_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 CONSTRAINT plg_clinical_service_revisions_number_check CHECK (revision_number >= 1)\n);\n\nCOMMENT ON TABLE public.plg_clinical_service_revisions IS\n 'A versão congelada do serviço — preço, duração e prazo no momento da contratação. V1: service_revisions + service_revision_forms.';\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 5. O desfecho do que foi contratado e não executado\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_clinical_prepaid_outcomes (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n booking_id uuid,\n order_item_id uuid,\n outcome text NOT NULL,\n reason text,\n decided_by uuid,\n decided_at timestamp with time zone DEFAULT now() 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 CONSTRAINT plg_clinical_prepaid_outcomes_check CHECK (outcome = ANY (ARRAY[\n 'contracted'::text, 'declined'::text, 'postponed'::text]))\n);\n\nCOMMENT ON TABLE public.plg_clinical_prepaid_outcomes IS\n 'O que aconteceu com o serviço contratado e não executado no dia — contratado, recusado ou adiado. V1: prepaid_item_outcomes.';\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 6. As chaves\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Toda FK para uma tabela escopada por unidade é COMPOSTA (tenant_id, id): é\n-- ela que impede a sessão de uma casa apontar para o agendamento de outra. Onde\n-- a referência é opcional, \\`ON DELETE SET NULL\\` — apagar o agendamento não pode\n-- levar o prontuário junto.\nDO $$\nDECLARE\n t text;\n tabelas text[] := ARRAY['plg_clinical_sessions', 'plg_clinical_session_products',\n 'plg_clinical_requirements', 'plg_clinical_service_revisions',\n 'plg_clinical_prepaid_outcomes'];\nBEGIN\n FOREACH t IN ARRAY tabelas LOOP\n IF NOT EXISTS (SELECT 1 FROM pg_constraint\n 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 -- sessões\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_sessions_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_sessions ADD CONSTRAINT plg_clinical_sessions_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_clinical_sessions_booking_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_sessions ADD CONSTRAINT plg_clinical_sessions_booking_fkey\n FOREIGN KEY (booking_id) REFERENCES public.appointments(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_sessions_booking_item_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_sessions ADD CONSTRAINT plg_clinical_sessions_booking_item_fkey\n FOREIGN KEY (booking_item_id) REFERENCES public.appointment_items(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_sessions_party_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_sessions ADD CONSTRAINT plg_clinical_sessions_party_fkey\n FOREIGN KEY (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_clinical_sessions_assignee_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_sessions ADD CONSTRAINT plg_clinical_sessions_assignee_fkey\n FOREIGN KEY (assignee_id) REFERENCES public.people(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_sessions_performed_by_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_sessions ADD CONSTRAINT plg_clinical_sessions_performed_by_fkey\n FOREIGN KEY (performed_by_id) REFERENCES public.people(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_sessions_service_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_sessions ADD CONSTRAINT plg_clinical_sessions_service_fkey\n FOREIGN KEY (service_id) REFERENCES public.products(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_sessions_order_item_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_sessions ADD CONSTRAINT plg_clinical_sessions_order_item_fkey\n FOREIGN KEY (order_item_id) REFERENCES public.items(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_sessions_quote_item_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_sessions ADD CONSTRAINT plg_clinical_sessions_quote_item_fkey\n FOREIGN KEY (quote_item_id) REFERENCES public.items(id) ON DELETE SET NULL;\n END IF;\n -- O pai e a avaliação de origem são a MESMA tabela. CASCADE aqui apagaria a\n -- corrente de revisões inteira ao apagar a primeira; SET NULL deixa as filhas\n -- órfãs e legíveis, que é o que um prontuário pede.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_sessions_parent_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_sessions ADD CONSTRAINT plg_clinical_sessions_parent_fkey\n FOREIGN KEY (parent_session_id) REFERENCES public.plg_clinical_sessions(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_sessions_source_eval_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_sessions ADD CONSTRAINT plg_clinical_sessions_source_eval_fkey\n FOREIGN KEY (source_evaluation_id) REFERENCES public.plg_clinical_sessions(id) ON DELETE SET NULL;\n END IF;\n\n -- produtos da sessão: a sessão some, o consumo some junto — ele não existe fora dela\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_session_products_session_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_session_products ADD CONSTRAINT plg_clinical_session_products_session_fkey\n FOREIGN KEY (tenant_id, session_id) REFERENCES public.plg_clinical_sessions(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_session_products_product_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_session_products ADD CONSTRAINT plg_clinical_session_products_product_fkey\n FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_session_products_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_session_products ADD CONSTRAINT plg_clinical_session_products_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n -- A UNIDADE DE MEDIDA (SR, UI, mL) MORA NO ESTOQUE, E FICA SEM CHAVE.\n --\n -- Havia uma FK para \\`plg_inventory_measurement_units\\`. Ela amarra dois ciclos\n -- de release e torna impossível desinstalar o estoque sem derrubar o\n -- prontuário — a clínica que não usa estoque passaria a precisar dele. O\n -- \\`used_unit_id\\` continua sendo o uuid de lá; quem quiser o símbolo resolve\n -- na leitura, e unidade apagada vira número sem rótulo em vez de escrita\n -- recusada.\n ALTER TABLE public.plg_clinical_session_products\n DROP CONSTRAINT IF EXISTS plg_clinical_session_products_uom_fkey;\n\n -- exigências\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_requirements_session_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_requirements ADD CONSTRAINT plg_clinical_requirements_session_fkey\n FOREIGN KEY (tenant_id, session_id) REFERENCES public.plg_clinical_sessions(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_requirements_booking_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_requirements ADD CONSTRAINT plg_clinical_requirements_booking_fkey\n FOREIGN KEY (booking_id) REFERENCES public.appointments(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_requirements_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_requirements ADD CONSTRAINT plg_clinical_requirements_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n\n -- revisão do serviço: uma por (serviço, número)\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_service_revisions_service_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_service_revisions ADD CONSTRAINT plg_clinical_service_revisions_service_fkey\n FOREIGN KEY (service_id) REFERENCES public.products(id) ON DELETE CASCADE;\n END IF;\n\n -- desfecho\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_prepaid_outcomes_booking_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_prepaid_outcomes ADD CONSTRAINT plg_clinical_prepaid_outcomes_booking_fkey\n FOREIGN KEY (booking_id) REFERENCES public.appointments(id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_prepaid_outcomes_item_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_prepaid_outcomes ADD CONSTRAINT plg_clinical_prepaid_outcomes_item_fkey\n FOREIGN KEY (order_item_id) REFERENCES public.items(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_clinical_prepaid_outcomes_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_clinical_prepaid_outcomes ADD CONSTRAINT plg_clinical_prepaid_outcomes_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\nCREATE UNIQUE INDEX IF NOT EXISTS plg_clinical_service_revisions_service_number_key\n ON public.plg_clinical_service_revisions USING btree (tenant_id, service_id, revision_number);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 7. Os índices das três perguntas que a tela faz\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- \"o que aconteceu com esta paciente\" · \"o que falta neste atendimento\" ·\n-- \"quanto deste produto saiu\". Com 233 mil sessões numa rede de 63 unidades,\n-- cada uma dessas sem índice é uma varredura.\nCREATE INDEX IF NOT EXISTS plg_clinical_sessions_party_idx\n ON public.plg_clinical_sessions USING btree (tenant_id, party_id, scheduled_for DESC);\nCREATE INDEX IF NOT EXISTS plg_clinical_sessions_booking_idx\n ON public.plg_clinical_sessions USING btree (tenant_id, booking_id) WHERE booking_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_clinical_sessions_unit_status_idx\n ON public.plg_clinical_sessions USING btree (tenant_id, unit_id, status, scheduled_for DESC);\nCREATE INDEX IF NOT EXISTS plg_clinical_sessions_deal_idx\n ON public.plg_clinical_sessions USING btree (tenant_id, deal_id) WHERE deal_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_clinical_session_products_session_idx\n ON public.plg_clinical_session_products USING btree (tenant_id, session_id);\nCREATE INDEX IF NOT EXISTS plg_clinical_session_products_product_idx\n ON public.plg_clinical_session_products USING btree (tenant_id, product_id, executed_on DESC);\nCREATE INDEX IF NOT EXISTS plg_clinical_requirements_session_idx\n ON public.plg_clinical_requirements USING btree (tenant_id, session_id, status);\nCREATE INDEX IF NOT EXISTS plg_clinical_requirements_booking_pending_idx\n ON public.plg_clinical_requirements USING btree (tenant_id, booking_id)\n WHERE status = 'pending'::text;\n\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_clinical_sessions', 'plg_clinical_session_products',\n 'plg_clinical_requirements', 'plg_clinical_service_revisions',\n 'plg_clinical_prepaid_outcomes'] 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-- ═══════════════════════════════════════════════════════════════════════════\n-- 8. Quem enxerga o prontuário\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Escopo de UNIDADE nas quatro tabelas que têm unidade, e permissão própria:\n-- \\`clinical.read\\` não é \\`agenda.read\\`. Quem marca horário não lê anamnese.\n--\n-- A revisão do serviço é a exceção: ela é do CATÁLOGO, não de uma filial — o\n-- preço congelado de um serviço vale na rede inteira, e escondê-lo por unidade\n-- faria a fatura de ontem virar um número sem origem.\n-- As cinco, uma a uma e por extenso. O laço abaixo já liga tudo isto — mas ele\n-- liga por \\`EXECUTE format(...)\\`, e o portão que revisa plugin lê o ARQUIVO,\n-- não o banco: para ele, tabela com tenant_id e sem um ENABLE literal é tabela\n-- aberta. Uma garantia que o revisor não consegue ler é uma garantia que\n-- ninguém confere.\nALTER TABLE public.plg_clinical_sessions ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_clinical_session_products ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_clinical_requirements ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_clinical_service_revisions ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_clinical_prepaid_outcomes ENABLE ROW LEVEL SECURITY;\n\nDO $$\nDECLARE\n t text;\n unidade boolean;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_clinical_sessions', 'plg_clinical_session_products',\n 'plg_clinical_requirements', 'plg_clinical_service_revisions',\n 'plg_clinical_prepaid_outcomes'] LOOP\n unidade := t <> 'plg_clinical_service_revisions';\n EXECUTE format('ALTER TABLE public.%I FORCE ROW LEVEL SECURITY', t);\n EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);\n EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.%I TO authenticated', t);\n EXECUTE format('GRANT ALL ON TABLE public.%I TO service_role', t);\n\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_select', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (%s)\n AND (SELECT app.has_permission('clinical.read', %s)))$f$,\n t || '_authz_select', t,\n CASE WHEN unidade THEN format('unit_id IS NULL OR (SELECT app.has_unit(%I.unit_id))', t) ELSE 'true' END,\n CASE WHEN unidade THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_insert', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR INSERT TO authenticated WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (%s)\n AND (SELECT app.has_permission('clinical.create', %s)))$f$,\n t || '_authz_insert', t,\n CASE WHEN unidade THEN format('unit_id IS NULL OR (SELECT app.has_unit(%I.unit_id))', t) ELSE 'true' END,\n CASE WHEN unidade THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_update', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR UPDATE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (%s)\n AND (SELECT app.has_permission('clinical.edit', %s)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('clinical.edit', %s)))$f$,\n t || '_authz_update', t,\n CASE WHEN unidade THEN format('unit_id IS NULL OR (SELECT app.has_unit(%I.unit_id))', t) ELSE 'true' END,\n CASE WHEN unidade THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END,\n CASE WHEN unidade THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_delete', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR DELETE TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (%s)\n AND (SELECT app.has_permission('clinical.delete', %s)))$f$,\n t || '_authz_delete', t,\n CASE WHEN unidade THEN format('unit_id IS NULL OR (SELECT app.has_unit(%I.unit_id))', t) ELSE 'true' END,\n CASE WHEN unidade THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n END LOOP;\nEND $$;\n\n-- \\`owner_id\\` só na sessão, e \\`shareable\\` falso: um prontuário não se compartilha\n-- por tupla — ou a pessoa atende naquela unidade, ou não lê.\nINSERT INTO public.scoped_resources (resource_table, unit_column, owner_column, shareable, default_visibility, label)\nVALUES ('plg_clinical_sessions', 'unit_id', 'owner_id', false, 'unit', 'Atendimentos clínicos')\nON CONFLICT (resource_table) DO UPDATE\n SET unit_column = EXCLUDED.unit_column, default_visibility = EXCLUDED.default_visibility, label = EXCLUDED.label;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 9. A permissão existe antes da política precisar dela\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- \\`clinical.read\\` não é \\`agenda.read\\`, e a diferença é o ponto: a recepção marca\n-- horário e NÃO lê anamnese. Quem hoje tem \\`agenda.*\\` num papel herda o\n-- equivalente clínico, porque é quem atende — e quem quiser separar recepção de\n-- profissional tira a linha do papel, que é uma decisão de uma tela, não de uma\n-- migration.\nINSERT INTO app.permissions (key, plugin, description, owner_only) VALUES\n ('clinical.read', 'clinical', 'Prontuário: ler o atendimento, a anamnese e o consumo', false),\n ('clinical.read_all', 'clinical', 'Prontuário: ler o de todas as unidades', false),\n ('clinical.create', 'clinical', 'Prontuário: abrir sessão e registrar consumo', false),\n ('clinical.edit', 'clinical', 'Prontuário: corrigir sessão, consumo e exigência', false),\n ('clinical.delete', 'clinical', 'Prontuário: apagar', false),\n ('clinical.manage', 'clinical', 'Prontuário: administrar', false)\nON CONFLICT (key) DO UPDATE SET plugin = EXCLUDED.plugin, description = EXCLUDED.description;\n\nINSERT INTO app.role_permissions (tenant_id, role_id, permission)\nSELECT rp.tenant_id, rp.role_id, replace(rp.permission, 'agenda.', 'clinical.')\n FROM app.role_permissions rp\n WHERE rp.permission IN ('agenda.read', 'agenda.read_all', 'agenda.create',\n 'agenda.edit', 'agenda.delete', 'agenda.manage')\nON CONFLICT DO NOTHING;\n`\n\nexport const MIGRATION_002_O_PRONTUARIO_DO_V1_ATRAVESSA = `-- O prontuário do V1 atravessa.\n--\n-- A 001 fez as cinco tabelas. Esta faz o TRANSPORTE: os writers do motor de\n-- porte e as cinco linhas do de-para. Sem ela as tabelas existem vazias.\n--\n-- journey_executions 233.934 → plg_clinical_sessions\n-- execution_products 117.011 → plg_clinical_session_products\n-- execution_requirements 6.590 → plg_clinical_requirements\n-- service_revisions 110 → plg_clinical_service_revisions\n-- service_revision_forms 87 → (jsonb da revisão)\n-- prepaid_item_outcomes 30 → plg_clinical_prepaid_outcomes\n--\n-- ── Duas tabelas do V1 não têm \\`uuid\\` ───────────────────────────────────────\n--\n-- \\`execution_requirements\\` e \\`prepaid_item_outcomes\\` não guardam uuid nenhum —\n-- só o id numérico. O motor já sabe lidar com isso (o id do destino nasce\n-- determinístico da chave da origem), e é por isso que o de-para delas não\n-- mapeia \\`uuid\\`.\n--\n-- ── A referência polimórfica, resolvida em tempo de escrita ─────────────────\n--\n-- \\`execution_requirements.resolved_table\\` + \\`resolved_id\\` apontam para uma\n-- tabela QUALQUER, e o \\`@tabela\\` do de-para é estático. Então quem resolve é o\n-- writer, lendo o ledger com o nome da tabela que chegou na linha. Medido no\n-- tenant 1:\n--\n-- client_form_responses 2.685 ainda não migrou → fica NULO, com o\n-- número cru em metadata\n-- journey_executions 1.048 resolve\n-- generated_contracts 845 ainda não migrou\n-- invoice_items 423 resolve\n-- appointments 80 resolve\n--\n-- O número cru fica em \\`metadata.clinical.v1_resolved_ref\\` de propósito: no dia\n-- em que \\`client_form_responses\\` atravessar, uma passada de costura preenche os\n-- 2.685 sem reler a origem — foi o que a 011 da agenda fez com a sala.\n--\n-- ── A revisão e os formulários dela ─────────────────────────────────────────\n--\n-- 87 linhas de \\`service_revision_forms\\` em 110 revisões. Uma lista curta, sem\n-- tela e sem vida própria, então ela entra como jsonb DENTRO da revisão — e o\n-- writer que a escreve é um facet: acha a revisão pelo ledger e acrescenta,\n-- sem duplicar quando a onda roda de novo.\n--\n-- Idempotente.\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 1. O vocabulário fechado aprende quatro palavras\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Por ACRÉSCIMO, lendo a definição vigente. Lista literal apaga o que um passo\n-- anterior acrescentou — é a armadilha registrada em financial/072 e agenda/008.\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, 'clinical_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, ''clinical_session''::text, ''clinical_session_product''::text, '\n '''clinical_requirement''::text, ''clinical_service_revision''::text, '\n '''clinical_revision_form''::text, ''clinical_prepaid_outcome''::text');\nEND $$;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 2. Uma ajuda: resolver referência polimórfica pelo ledger\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- SECURITY DEFINER porque lê \\`migration.ledger\\`, que o papel do porte já\n-- alcança mas nenhuma política de tenant descreve. Devolve NULO quando a linha\n-- de origem ainda não atravessou — e NULO aqui é resposta, não erro.\nCREATE OR REPLACE FUNCTION migration._clinical_resolve(\n p_tenant uuid, p_table text, p_id text\n) RETURNS uuid\nLANGUAGE sql STABLE SECURITY DEFINER SET search_path TO ''\nAS $function$\n SELECT l.target_id\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = p_table\n AND l.status = 'migrated'\n AND l.source_numeric_id = nullif(p_id, '')::bigint\n LIMIT 1\n$function$;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 3. O writer da sessão\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE OR REPLACE FUNCTION migration.upsert_clinical_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 := nullif(p_row ->> 'status', '');\n v_unit 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 clinical session needs an id');\n END IF;\n\n -- O vocabulário é fechado na tabela. Recusar aqui, com a palavra na mão, é\n -- melhor do que deixar a CHECK derrubar o lote sem dizer qual linha.\n IF v_status IS NULL OR v_status NOT IN ('quoted','contracted','scheduled','in_service','in_progress','executed','pending') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('clinical session status %L is not one of the seven the V1 writes', v_status));\n END IF;\n\n BEGIN v_unit := (p_row ->> 'unit_id')::uuid; EXCEPTION WHEN OTHERS THEN v_unit := NULL; END;\n -- Sem unidade na linha, a do agendamento serve: é o mesmo atendimento.\n IF v_unit IS NULL AND (p_row ->> 'booking_id') IS NOT NULL THEN\n SELECT a.unit_id INTO v_unit FROM public.appointments a\n WHERE a.id = (p_row ->> 'booking_id')::uuid AND a.tenant_id = p_tenant;\n END IF;\n\n SELECT tenant_id INTO v_existing FROM public.plg_clinical_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('clinical 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_clinical_sessions\n (id, tenant_id, unit_id, status, booking_id, booking_item_id, party_id,\n assignee_id, performed_by_id, service_id, order_item_id, quote_item_id, deal_id,\n revision_number, parent_session_id, source_evaluation_id,\n scheduled_for, started_at, ended_at, products_initialized, notes,\n metadata, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_unit, v_status,\n nullif(p_row ->> 'booking_id', '')::uuid,\n nullif(p_row ->> 'booking_item_id', '')::uuid,\n nullif(p_row ->> 'party_id', '')::uuid,\n nullif(p_row ->> 'assignee_id', '')::uuid,\n nullif(p_row ->> 'performed_by_id', '')::uuid,\n nullif(p_row ->> 'service_id', '')::uuid,\n nullif(p_row ->> 'order_item_id', '')::uuid,\n nullif(p_row ->> 'quote_item_id', '')::uuid,\n nullif(p_row ->> 'deal_id', '')::uuid,\n greatest(coalesce(nullif(p_row ->> 'revision_number', '')::integer, 1), 1),\n nullif(p_row ->> 'parent_session_id', '')::uuid,\n nullif(p_row ->> 'source_evaluation_id', '')::uuid,\n nullif(p_row ->> 'scheduled_for', '')::date,\n nullif(p_row ->> 'started_at', '')::timestamptz,\n nullif(p_row ->> 'ended_at', '')::timestamptz,\n coalesce((p_row ->> 'products_initialized')::boolean, false),\n p_row ->> 'notes', v_meta,\n coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),\n coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.plg_clinical_sessions SET\n unit_id = coalesce(v_unit, unit_id),\n status = v_status,\n booking_id = coalesce(nullif(p_row ->> 'booking_id', '')::uuid, booking_id),\n booking_item_id = coalesce(nullif(p_row ->> 'booking_item_id', '')::uuid, booking_item_id),\n party_id = coalesce(nullif(p_row ->> 'party_id', '')::uuid, party_id),\n assignee_id = coalesce(nullif(p_row ->> 'assignee_id', '')::uuid, assignee_id),\n performed_by_id = coalesce(nullif(p_row ->> 'performed_by_id', '')::uuid, performed_by_id),\n service_id = coalesce(nullif(p_row ->> 'service_id', '')::uuid, service_id),\n order_item_id = coalesce(nullif(p_row ->> 'order_item_id', '')::uuid, order_item_id),\n quote_item_id = coalesce(nullif(p_row ->> 'quote_item_id', '')::uuid, quote_item_id),\n deal_id = coalesce(nullif(p_row ->> 'deal_id', '')::uuid, deal_id),\n parent_session_id = coalesce(nullif(p_row ->> 'parent_session_id', '')::uuid, parent_session_id),\n source_evaluation_id = coalesce(nullif(p_row ->> 'source_evaluation_id', '')::uuid, source_evaluation_id),\n scheduled_for = nullif(p_row ->> 'scheduled_for', '')::date,\n started_at = nullif(p_row ->> 'started_at', '')::timestamptz,\n ended_at = nullif(p_row ->> 'ended_at', '')::timestamptz,\n products_initialized = coalesce((p_row ->> 'products_initialized')::boolean, products_initialized),\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\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_clinical_sessions', p_id, v_unit);\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.plg_clinical_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-- 4. O writer do produto consumido\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE OR REPLACE FUNCTION migration.upsert_clinical_session_product(\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_session uuid;\n v_product uuid;\n v_unit 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 session product needs an id');\n END IF;\n\n BEGIN v_session := (p_row ->> 'session_id')::uuid; EXCEPTION WHEN OTHERS THEN v_session := NULL; END;\n BEGIN v_product := (p_row ->> 'product_id')::uuid; EXCEPTION WHEN OTHERS THEN v_product := NULL; END;\n -- Consumo sem sessão é consumo que nenhuma tela alcança; sem produto é uma\n -- quantidade de nada. Os dois são recusa de FK, e a 2ª passada do motor os\n -- reapresenta depois que a sessão migrar.\n IF v_session IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'a session product needs the session it was used in');\n END IF;\n IF v_product IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'a session product needs the product that was used');\n END IF;\n\n SELECT s.unit_id INTO v_unit FROM public.plg_clinical_sessions s\n WHERE s.id = v_session AND s.tenant_id = p_tenant;\n\n SELECT tenant_id INTO v_existing FROM public.plg_clinical_session_products 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('session product %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.plg_clinical_session_products\n (id, tenant_id, unit_id, session_id, product_id, order_item_id, assignee_id,\n default_quantity, used_quantity, used_unit_id, executed_on,\n is_audited, audited_at, audit_notes, metadata, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_unit, v_session, v_product,\n nullif(p_row ->> 'order_item_id', '')::uuid,\n nullif(p_row ->> 'assignee_id', '')::uuid,\n nullif(p_row ->> 'default_quantity', '')::numeric,\n nullif(p_row ->> 'used_quantity', '')::numeric,\n nullif(p_row ->> 'used_unit_id', '')::uuid,\n nullif(p_row ->> 'executed_on', '')::date,\n coalesce((p_row ->> 'is_audited')::boolean, false),\n nullif(p_row ->> 'audited_at', '')::timestamptz,\n p_row ->> 'audit_notes', v_meta,\n coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),\n coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.plg_clinical_session_products SET\n unit_id = coalesce(v_unit, unit_id),\n session_id = v_session,\n product_id = v_product,\n order_item_id = coalesce(nullif(p_row ->> 'order_item_id', '')::uuid, order_item_id),\n assignee_id = coalesce(nullif(p_row ->> 'assignee_id', '')::uuid, assignee_id),\n default_quantity = nullif(p_row ->> 'default_quantity', '')::numeric,\n used_quantity = nullif(p_row ->> 'used_quantity', '')::numeric,\n used_unit_id = coalesce(nullif(p_row ->> 'used_unit_id', '')::uuid, used_unit_id),\n executed_on = nullif(p_row ->> 'executed_on', '')::date,\n is_audited = coalesce((p_row ->> 'is_audited')::boolean, is_audited),\n audited_at = nullif(p_row ->> 'audited_at', '')::timestamptz,\n audit_notes = CASE WHEN p_row ? 'audit_notes' THEN p_row ->> 'audit_notes' ELSE audit_notes END,\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 IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.plg_clinical_session_products', 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-- 5. O writer da exigência\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE OR REPLACE FUNCTION migration.upsert_clinical_requirement(\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 := nullif(p_row ->> 'kind', '');\n v_scope text := nullif(p_row ->> 'scope', '');\n v_status text := coalesce(nullif(p_row ->> 'status', ''), 'pending');\n v_session uuid;\n v_unit uuid;\n v_subj_t text := nullif(p_row ->> 'subject_type', '');\n v_cand_t text := nullif(p_row ->> 'candidate_type', '');\n v_subj_id uuid;\n v_cand_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 IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a requirement needs an id');\n END IF;\n IF v_kind IS NULL OR v_kind NOT IN ('form','evaluation','contract','session_term','products','invoice_item','revision_link') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('requirement kind %L is not one of the seven the V1 writes', v_kind));\n END IF;\n IF v_scope IS NULL OR v_scope NOT IN ('service','attendance') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('requirement scope %L is neither service nor attendance', v_scope));\n END IF;\n IF v_status NOT IN ('pending','resolved','not_applicable') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('requirement status %L is not a status the V1 writes', v_status));\n END IF;\n\n BEGIN v_session := (p_row ->> 'session_id')::uuid; EXCEPTION WHEN OTHERS THEN v_session := NULL; END;\n IF v_session IS NOT NULL THEN\n SELECT s.unit_id INTO v_unit FROM public.plg_clinical_sessions s\n WHERE s.id = v_session AND s.tenant_id = p_tenant;\n END IF;\n\n -- A referência polimórfica: o de-para não alcança, o ledger sim. Quando a\n -- tabela apontada ainda não atravessou o resultado é NULO — e o número cru\n -- fica no metadata para a costura de depois.\n v_subj_id := migration._clinical_resolve(p_tenant, v_subj_t, p_row ->> 'v1_resolved_ref');\n v_cand_id := migration._clinical_resolve(p_tenant, v_cand_t, p_row ->> 'v1_candidate_ref');\n\n SELECT tenant_id INTO v_existing FROM public.plg_clinical_requirements 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('requirement %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.plg_clinical_requirements\n (id, tenant_id, unit_id, session_id, booking_id, party_id, service_id,\n kind, scope, blocks_start, blocks_finish, status,\n subject_type, subject_id, candidate_type, candidate_id,\n resolved_via, resolved_at, valid_until, frozen_at, rule_sources,\n metadata, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_unit, v_session,\n nullif(p_row ->> 'booking_id', '')::uuid,\n nullif(p_row ->> 'party_id', '')::uuid,\n nullif(p_row ->> 'service_id', '')::uuid,\n v_kind, v_scope,\n coalesce((p_row ->> 'blocks_start')::boolean, false),\n coalesce((p_row ->> 'blocks_finish')::boolean, false),\n v_status, v_subj_t, v_subj_id, v_cand_t, v_cand_id,\n nullif(p_row ->> 'resolved_via', ''),\n nullif(p_row ->> 'resolved_at', '')::timestamptz,\n nullif(p_row ->> 'valid_until', '')::timestamptz,\n nullif(p_row ->> 'frozen_at', '')::timestamptz,\n coalesce(p_row -> 'rule_sources', '[]'::jsonb),\n v_meta,\n coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),\n coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.plg_clinical_requirements SET\n unit_id = coalesce(v_unit, unit_id),\n session_id = coalesce(v_session, session_id),\n booking_id = coalesce(nullif(p_row ->> 'booking_id', '')::uuid, booking_id),\n party_id = coalesce(nullif(p_row ->> 'party_id', '')::uuid, party_id),\n service_id = coalesce(nullif(p_row ->> 'service_id', '')::uuid, service_id),\n kind = v_kind,\n scope = v_scope,\n blocks_start = coalesce((p_row ->> 'blocks_start')::boolean, blocks_start),\n blocks_finish = coalesce((p_row ->> 'blocks_finish')::boolean, blocks_finish),\n status = v_status,\n subject_type = v_subj_t,\n -- \\`coalesce\\` e não substituição: a segunda onda resolve o que a primeira\n -- não alcançou, e nunca apaga o que já estava resolvido.\n subject_id = coalesce(v_subj_id, subject_id),\n candidate_type = v_cand_t,\n candidate_id = coalesce(v_cand_id, candidate_id),\n resolved_via = nullif(p_row ->> 'resolved_via', ''),\n resolved_at = nullif(p_row ->> 'resolved_at', '')::timestamptz,\n valid_until = nullif(p_row ->> 'valid_until', '')::timestamptz,\n frozen_at = nullif(p_row ->> 'frozen_at', '')::timestamptz,\n rule_sources = coalesce(p_row -> 'rule_sources', rule_sources),\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 IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.plg_clinical_requirements', 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-- 6. A revisão do serviço, e os formulários dela\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE OR REPLACE FUNCTION migration.upsert_clinical_service_revision(\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_service uuid;\n v_number integer;\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 service revision needs an id');\n END IF;\n\n BEGIN v_service := (p_row ->> 'service_id')::uuid; EXCEPTION WHEN OTHERS THEN v_service := NULL; END;\n IF v_service IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'a service revision needs the service it freezes');\n END IF;\n v_number := greatest(coalesce(nullif(p_row ->> 'revision_number', '')::integer, 1), 1);\n\n SELECT tenant_id INTO v_existing FROM public.plg_clinical_service_revisions 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('service revision %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.plg_clinical_service_revisions\n (id, tenant_id, service_id, revision_number, price, duration_minutes, deadline_days,\n metadata, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_service, v_number,\n nullif(p_row ->> 'price', '')::numeric,\n nullif(p_row ->> 'duration_minutes', '')::integer,\n nullif(p_row ->> 'deadline_days', '')::integer,\n v_meta,\n coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),\n coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.plg_clinical_service_revisions SET\n service_id = v_service,\n revision_number = v_number,\n price = nullif(p_row ->> 'price', '')::numeric,\n duration_minutes = nullif(p_row ->> 'duration_minutes', '')::integer,\n deadline_days = nullif(p_row ->> 'deadline_days', '')::integer,\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 IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.plg_clinical_service_revisions', 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-- O facet: acrescenta um formulário ao jsonb da revisão. A chave de dedupe é o\n-- id da linha do V1, e é isso que faz a onda rodar duas vezes sem duplicar.\nCREATE OR REPLACE FUNCTION migration.upsert_clinical_revision_form(\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_rev uuid;\n v_ref text := p_row ->> 'v1_ref';\n v_entry 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\n BEGIN v_rev := (p_row ->> 'revision_id')::uuid; EXCEPTION WHEN OTHERS THEN v_rev := NULL; END;\n IF v_rev IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'a revision form needs the revision it belongs to');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.plg_clinical_service_revisions r\n WHERE r.id = v_rev AND r.tenant_id = p_tenant) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'the revision this form belongs to has not arrived yet');\n END IF;\n\n v_entry := jsonb_build_object(\n 'v1_ref', v_ref,\n 'v1_form_template_ref', p_row ->> 'v1_form_template_ref');\n\n UPDATE public.plg_clinical_service_revisions\n SET form_template_refs = (\n SELECT coalesce(jsonb_agg(e), '[]'::jsonb)\n FROM (SELECT e FROM jsonb_array_elements(form_template_refs) e\n WHERE e ->> 'v1_ref' IS DISTINCT FROM v_ref\n UNION ALL SELECT v_entry) s(e)),\n updated_at = now()\n WHERE id = v_rev;\n\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_rev, 'reason', 'attached');\nEND $function$;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 7. O writer do desfecho\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE OR REPLACE FUNCTION migration.upsert_clinical_prepaid_outcome(\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_outcome text := nullif(p_row ->> 'outcome', '');\n v_booking uuid;\n v_unit 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', 'an outcome needs an id');\n END IF;\n IF v_outcome IS NULL OR v_outcome NOT IN ('contracted','declined','postponed') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('outcome %L is not one of the three the V1 writes', v_outcome));\n END IF;\n\n BEGIN v_booking := (p_row ->> 'booking_id')::uuid; EXCEPTION WHEN OTHERS THEN v_booking := NULL; END;\n IF v_booking IS NOT NULL THEN\n SELECT a.unit_id INTO v_unit FROM public.appointments a\n WHERE a.id = v_booking AND a.tenant_id = p_tenant;\n END IF;\n\n SELECT tenant_id INTO v_existing FROM public.plg_clinical_prepaid_outcomes 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('outcome %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.plg_clinical_prepaid_outcomes\n (id, tenant_id, unit_id, booking_id, order_item_id, outcome, reason, decided_at, metadata, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_unit, v_booking,\n nullif(p_row ->> 'order_item_id', '')::uuid,\n v_outcome, p_row ->> 'reason',\n coalesce(nullif(p_row ->> 'decided_at', '')::timestamptz, now()),\n v_meta, now(), now());\n v_created := true;\n ELSE\n UPDATE public.plg_clinical_prepaid_outcomes SET\n unit_id = coalesce(v_unit, unit_id),\n booking_id = coalesce(v_booking, booking_id),\n order_item_id = coalesce(nullif(p_row ->> 'order_item_id', '')::uuid, order_item_id),\n outcome = v_outcome,\n reason = CASE WHEN p_row ? 'reason' THEN p_row ->> 'reason' ELSE reason END,\n decided_at = coalesce(nullif(p_row ->> 'decided_at', '')::timestamptz, decided_at),\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\n PERFORM migration._audit(p_tenant, 'public.plg_clinical_prepaid_outcomes', 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-- 8. As seis linhas do de-para\n-- ═══════════════════════════════════════════════════════════════════════════\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'service_revisions', 'public.plg_clinical_service_revisions', 'clinical', 'clinical_service_revision',\n '{}'::jsonb, NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'service_id', 'service_id@services',\n 'revision_number', 'revision_number',\n 'price', 'price',\n 'duration_minutes', 'duration_minutes',\n 'deadline_days', 'deadline_days',\n 'created_at', 'created_at',\n 'tenant_id', '-',\n 'id', 'jsonb:clinical.v1_ref'),\n 'A versão congelada do serviço. Vem ANTES da sessão porque é ela que dá o '\n 'preço e o prazo que valiam no dia da contratação.'\n);\n\nSELECT migration._allow(\n 'service_revision_forms', 'public.plg_clinical_service_revisions', 'clinical', 'clinical_revision_form',\n '{}'::jsonb, NULL::text[],\n jsonb_build_object(\n 'service_revision_id', 'revision_id@service_revisions',\n 'form_template_id', 'v1_form_template_ref',\n 'id', 'v1_ref',\n 'uuid', '-',\n 'service_id', '-',\n 'created_at', '-',\n 'tenant_id', '-'),\n '87 linhas em 110 revisões. Não tem tabela própria: entra no jsonb '\n '\\`form_template_refs\\` da revisão, e o writer é um facet que dedupe pelo id do '\n 'V1 — rodar a onda duas vezes não duplica.'\n);\n\nSELECT migration._allow(\n 'journey_executions', 'public.plg_clinical_sessions', 'clinical', 'clinical_session',\n '{}'::jsonb, NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'journey_status', 'status',\n 'appointment_id', 'booking_id@appointments',\n 'appointment_service_id', 'booking_item_id@appointment_services',\n 'client_id', 'party_id@clients',\n 'professional_id', 'assignee_id@professionals',\n 'execution_professional_id', 'performed_by_id@professionals',\n 'service_id', 'service_id@services',\n 'invoice_item_id', 'order_item_id@invoice_items',\n 'quote_item_id', 'quote_item_id@quote_items',\n 'opportunity_id', 'deal_id@opportunities',\n 'parent_execution_id', 'parent_session_id@journey_executions',\n 'source_evaluation_execution_id', 'source_evaluation_id@journey_executions',\n 'unit_numeric_id', 'unit_id@companies',\n 'revision_number', 'revision_number',\n 'appointment_date', 'scheduled_for',\n 'execution_start_time', 'started_at',\n 'execution_end_time', 'ended_at',\n 'products_initialized', 'products_initialized',\n 'notes', 'notes',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'product_id', '-',\n 'status', 'jsonb:clinical.v1_status',\n 'execution_photos', 'jsonb:clinical.v1_execution_photos',\n 'id', 'jsonb:clinical.v1_ref'),\n 'A sessão de execução. \\`journey_status\\` é quem manda no estado — tem sete '\n 'palavras contra as quatro de \\`status\\`, e é a única que separa orçado de '\n 'contratado; a outra fica em metadata. \\`product_id\\` sai: 0 de 233.950 '\n 'preenchidas. As fotos ficam em metadata até virarem linhas de '\n 'public.documents, que é onde arquivo mora.'\n);\n\nSELECT migration._allow(\n 'execution_products', 'public.plg_clinical_session_products', 'clinical', 'clinical_session_product',\n '{}'::jsonb, NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'journey_execution_id', 'session_id@journey_executions',\n 'product_id', 'product_id@products',\n 'invoice_item_id', 'order_item_id@invoice_items',\n 'professional_id', 'assignee_id@professionals',\n 'used_unit_id', 'used_unit_id@measurement_units',\n 'unit_id', '-',\n 'default_quantity', 'default_quantity',\n 'used_quantity', 'used_quantity',\n 'execution_date', 'executed_on',\n 'is_audited', 'is_audited',\n 'audited_at', 'audited_at',\n 'audit_notes', 'audit_notes',\n 'audited_by_user_id', '-',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'stock_deducted', '-',\n 'stock_movement_id', '-',\n 'stock_position_id', '-',\n 'position_allocations', '-',\n 'unit_label_allocations', '-',\n 'consumption_photos', 'jsonb:clinical.v1_consumption_photos',\n 'id', 'jsonb:clinical.v1_ref'),\n 'O produto consumido. As cinco colunas de baixa de estoque saem porque estão '\n 'VAZIAS: 0 de 117.015 no tenant 1. O consumo do V1 nunca chegou ao razão — é '\n 'o que o Guia descreve na jornada auditoria-de-uso-e-estoque. A unidade vem '\n 'da sessão, no writer, e não da coluna \\`unit_id\\` da origem.'\n);\n\nSELECT migration._allow(\n 'execution_requirements', 'public.plg_clinical_requirements', 'clinical', 'clinical_requirement',\n '{}'::jsonb, NULL::text[],\n jsonb_build_object(\n 'journey_execution_id', 'session_id@journey_executions',\n 'appointment_id', 'booking_id@appointments',\n 'client_id', 'party_id@clients',\n 'service_id', 'service_id@services',\n 'kind', 'kind',\n 'scope', 'scope',\n 'status', 'status',\n 'blocks_start', 'blocks_start',\n 'blocks_finish', 'blocks_finish',\n 'resolved_table', 'subject_type',\n 'resolved_id', 'v1_resolved_ref',\n 'candidate_table', 'candidate_type',\n 'candidate_id', 'v1_candidate_ref',\n 'resolved_via', 'resolved_via',\n 'resolved_at', 'resolved_at',\n 'valid_until', 'valid_until',\n 'frozen_at', 'frozen_at',\n 'rule_sources', 'rule_sources',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'ref_id', 'jsonb:clinical.v1_ref_id',\n 'id', 'jsonb:clinical.v1_ref'),\n 'O que trava começar e terminar. Sem \\`uuid\\` na origem — o id do destino nasce '\n 'determinístico da chave numérica. \\`resolved_table\\`/\\`resolved_id\\` é referência '\n 'polimórfica e o \\`@tabela\\` do de-para é estático, então quem resolve é o '\n 'writer, lendo o ledger com o nome que chegou na linha.'\n);\n\nSELECT migration._allow(\n 'prepaid_item_outcomes', 'public.plg_clinical_prepaid_outcomes', 'clinical', 'clinical_prepaid_outcome',\n '{}'::jsonb, NULL::text[],\n jsonb_build_object(\n 'appointment_id', 'booking_id@appointments',\n 'invoice_item_id', 'order_item_id@invoice_items',\n 'outcome', 'outcome',\n 'reason', 'reason',\n 'decided_at', 'decided_at',\n 'decided_by', '-',\n 'tenant_id', '-',\n 'id', 'jsonb:clinical.v1_ref'),\n 'O desfecho do serviço contratado e não executado no dia. \\`decided_by\\` é '\n 'usuário do auth do V1, que não atravessa — fica de fora em vez de virar uma '\n 'FK que aponta para nada.'\n);\n\nRESET migration.allowlist_unlock;\n\nREVOKE ALL ON FUNCTION migration._clinical_resolve(uuid, text, text) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration._clinical_resolve(uuid, text, text) TO service_role;\nDO $$\nDECLARE f text;\nBEGIN\n FOREACH f IN ARRAY ARRAY['upsert_clinical_session', 'upsert_clinical_session_product',\n 'upsert_clinical_requirement', 'upsert_clinical_service_revision',\n 'upsert_clinical_revision_form', 'upsert_clinical_prepaid_outcome'] LOOP\n EXECUTE format('REVOKE ALL ON FUNCTION migration.%I(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated', f);\n EXECUTE format('GRANT EXECUTE ON FUNCTION migration.%I(uuid, uuid, jsonb, jsonb) TO service_role', f);\n END LOOP;\nEND $$;\n`\n\nexport const MIGRATION_003_A_PERMISSAO_SE_PERGUNTA_POR_UNIDADE_NAO_POR_LINHA = `-- A permissão se pergunta por unidade, não por linha.\n--\n-- A tela \\`Estoque → Auditoria de uso\\` estourava o \\`statement_timeout\\`. Medido\n-- no cluster, como \\`authenticated\\`, tenant Espaço Facial:\n--\n-- select * from v_inventory_usage_audit order by used_at desc limit 50\n-- → 97.143 ms\n--\n-- A vista não era a culpada. As duas tabelas que ela lê já eram lentas sozinhas:\n--\n-- select count(*) from plg_clinical_session_products 19.000 ms\n-- select count(*) from plg_clinical_sessions 22.500 ms\n--\n-- ── A linha que custava ─────────────────────────────────────────────────────\n--\n-- A política que a 001 escreveu chama \\`app.has_permission('clinical.read',\n-- unit_id)\\` — e \\`unit_id\\` é COLUNA, então a chamada é por LINHA. São 116.911\n-- produtos consumidos e 233.556 sessões; o Espaço Facial tem 67 unidades.\n--\n-- É a mesma forma do defeito que o spine 121 corrigiu em \\`people\\`, e a mesma\n-- correção: a pergunta não muda dentro da unidade, então ela se faz uma vez\n-- POR UNIDADE. \\`unit_id IN (subconsulta não correlacionada)\\` é resolvido pelo\n-- planejador como hash montado uma vez; a subconsulta percorre \\`app.units\\` do\n-- inquilino e aplica ali os dois predicados que antes rodavam por linha.\n--\n-- A reescrita é EQUIVALENTE por construção, não por argumento: são os mesmos\n-- dois predicados (\\`app.has_unit\\` e \\`app.has_permission\\`) sobre os mesmos\n-- valores de \\`unit_id\\`, só que avaliados uma vez por valor distinto em vez de\n-- uma vez por linha. A linha sem unidade continua decidida pela permissão de\n-- inquilino, como antes.\n--\n-- Medido depois, com ROLLBACK, antes de escrever este arquivo:\n--\n-- plg_clinical_session_products 116.911 linhas 450 ms (era 19.000)\n-- plg_clinical_sessions 233.556 linhas 1.070 ms (era 22.500)\n-- auditoria de uso, 50 linhas 50 linhas 1.330 ms (era 97.143)\n--\n-- As contagens são IDÊNTICAS às de antes — é o teste que importa numa mudança\n-- de política, porque uma que fica rápida devolvendo menos linhas não ficou\n-- rápida, ficou errada.\n--\n-- Idempotente.\n\nDO $$\nDECLARE\n t text;\n v_unidade text := $q$unit_id IN (SELECT u.id FROM app.units u\n WHERE u.tenant_id = (SELECT app.current_tenant_id())\n AND app.has_unit(u.id)\n AND app.has_permission('clinical.read', u.id))$q$;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_clinical_sessions', 'plg_clinical_session_products',\n 'plg_clinical_requirements', 'plg_clinical_prepaid_outcomes'] LOOP\n IF to_regclass('public.' || t) IS NULL THEN CONTINUE; END IF;\n EXECUTE format($f$ALTER POLICY %I ON public.%I USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (CASE WHEN unit_id IS NULL\n THEN (SELECT app.has_permission('clinical.read', NULL::uuid))\n ELSE %s END))$f$, t || '_authz_select', t, v_unidade);\n END LOOP;\nEND $$;\n\n-- \\`plg_clinical_service_revisions\\` fica como está: ela não tem unidade — a\n-- revisão é do CATÁLOGO, e o preço congelado de um serviço vale na rede\n-- inteira. A política dela já decide uma vez por consulta.\n\n-- A auditoria de uso ordena por data e corta em 50. Sem índice, isso é ordenar\n-- 116.911 linhas para jogar 116.861 fora.\nCREATE INDEX IF NOT EXISTS plg_clinical_session_products_executed_idx\n ON public.plg_clinical_session_products USING btree (tenant_id, executed_on DESC);\n`\n\nexport const MIGRATION_004_A_REGUA_VOLTA_A_SER_APURADA = `-- ============================================================================\n-- A régua do atendimento volta a ser APURADA\n--\n-- SINTOMA: 6.236 linhas de exigência atravessaram do V1 e estão congeladas.\n-- Nada no V2 as recalcula: um atendimento novo nasce sem régua, e um antigo\n-- nunca responde de novo \"o que este serviço exigia\". A régua migrada é foto,\n-- não motor.\n--\n-- O V1 resolve isso em duas funções (\\`materialize_execution_requirements\\` e\n-- \\`refresh_execution_requirements\\`, migration 20260927130000). Esta migration é\n-- o espelho delas sobre o modelo do V2, com três diferenças declaradas:\n--\n-- 1. CONTRATO E TERMO DE SESSÃO FICAM DE FORA. Os blocos d, e, f do V1\n-- dependem de \\`contract_templates\\` e \\`generated_contracts\\`, que o V2 não\n-- modela. Emitir uma bola de contrato que nada pode resolver deixaria todo\n-- atendimento eternamente pendente — indicador errado é pior que nenhum.\n-- 2. O QUE A EXIGÊNCIA PEDE ganha colunas. O V1 tem \\`ref_id\\` (o modelo de\n-- formulário exigido); o ETL despejou isso em \\`metadata.clinical.v1_ref_id\\`\n-- e a tabela ficou sem discriminador — por isso há 1.378 grupos\n-- (session, kind) repetidos, que são formulários diferentes indistinguíveis.\n-- \\`ref_type\\`/\\`ref_id\\` devolvem a pergunta; a chave única cobre só as linhas\n-- que este motor escreve, porque as migradas não têm como ser desempatadas\n-- enquanto os modelos de formulário não chegarem.\n-- 3. DUAS BANDEIRAS DE SERVIÇO não vieram do V1 (\\`requires_evaluation\\` e\n-- \\`always_ask_used_products\\`). Sem elas os blocos g e i ficariam mudos,\n-- então entram em \\`products\\` — onde o serviço mora no V2.\n--\n-- Modo: \\`off\\` (nada acontece) · \\`shadow\\` (grava e não muda tela) · \\`on\\`.\n-- Lê-se de \\`tenants.settings -> 'execution_requirements' ->> 'mode'\\`.\n-- ============================================================================\n\n-- ─── 0. O que faltava para a pergunta caber ─────────────────────────────────\n\nALTER TABLE public.products\n ADD COLUMN IF NOT EXISTS requires_evaluation boolean NOT NULL DEFAULT false,\n ADD COLUMN IF NOT EXISTS always_ask_used_products boolean NOT NULL DEFAULT false;\n\nCOMMENT ON COLUMN public.products.requires_evaluation IS\n 'Serviço que só se executa depois de uma avaliação vinculada (bloco g da régua).';\nCOMMENT ON COLUMN public.products.always_ask_used_products IS\n 'Serviço que sempre pergunta os produtos usados, mesmo sem ficha técnica (bloco i).';\n\nALTER TABLE public.plg_clinical_requirements\n ADD COLUMN IF NOT EXISTS ref_type text,\n ADD COLUMN IF NOT EXISTS ref_id uuid;\n\nCOMMENT ON COLUMN public.plg_clinical_requirements.ref_type IS\n 'O que a exigência PEDE (form_template). Não confundir com subject_*, que é o que a RESOLVEU.';\n\n-- Chave única só das linhas deste motor: as migradas perderam o discriminador\n-- (ver cabeçalho) e colidiriam entre si.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_clinical_requirements_engine_key\n ON public.plg_clinical_requirements (session_id, kind, (coalesce(ref_id, '00000000-0000-0000-0000-000000000000'::uuid)))\n WHERE metadata -> 'migration' IS NULL;\n\nCREATE INDEX IF NOT EXISTS plg_clinical_requirements_booking_status\n ON public.plg_clinical_requirements (booking_id, status);\n\n-- ─── 1. O modo ──────────────────────────────────────────────────────────────\n\nCREATE OR REPLACE FUNCTION public.clinical_requirements_mode(p_tenant uuid)\nRETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'\nAS $fn$\n SELECT CASE lower(coalesce(t.settings -> 'execution_requirements' ->> 'mode', 'off'))\n WHEN 'shadow' THEN 'shadow'\n WHEN 'on' THEN 'on'\n ELSE 'off'\n END\n FROM public.tenants t WHERE t.id = p_tenant;\n$fn$;\n\n-- ─── 2. Uma linha, idempotente (o OR dos bloqueios é do V1) ─────────────────\n\nCREATE OR REPLACE FUNCTION public.clinical_requirement_upsert(\n p_tenant uuid, p_unit uuid, p_booking uuid, p_session uuid, p_party uuid, p_service uuid,\n p_kind text, p_scope text, p_ref_type text, p_ref_id uuid, p_rule text,\n p_blocks_start boolean, p_blocks_finish boolean)\nRETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'\nAS $fn$\nDECLARE v_id uuid;\nBEGIN\n SELECT r.id INTO v_id FROM public.plg_clinical_requirements r\n WHERE r.session_id = p_session AND r.kind = p_kind\n AND coalesce(r.ref_id, '00000000-0000-0000-0000-000000000000'::uuid)\n = coalesce(p_ref_id, '00000000-0000-0000-0000-000000000000'::uuid)\n AND r.metadata -> 'migration' IS NULL;\n\n IF v_id IS NULL THEN\n INSERT INTO public.plg_clinical_requirements (\n tenant_id, unit_id, booking_id, session_id, party_id, service_id,\n kind, scope, ref_type, ref_id, rule_sources, blocks_start, blocks_finish, status)\n VALUES (p_tenant, p_unit, p_booking, p_session, p_party, p_service,\n p_kind, p_scope, p_ref_type, p_ref_id, jsonb_build_array(p_rule),\n coalesce(p_blocks_start, false), coalesce(p_blocks_finish, false), 'pending');\n RETURN 1;\n END IF;\n\n -- Já existe: soma a origem da regra e faz o OR dos bloqueios. Uma linha\n -- resolvida ou congelada não volta a pendente por aqui.\n UPDATE public.plg_clinical_requirements r\n SET rule_sources = CASE WHEN r.rule_sources @> to_jsonb(p_rule) THEN r.rule_sources\n ELSE r.rule_sources || to_jsonb(p_rule) END,\n blocks_start = r.blocks_start OR coalesce(p_blocks_start, false),\n blocks_finish = r.blocks_finish OR coalesce(p_blocks_finish, false),\n status = CASE WHEN r.status = 'not_applicable' THEN 'pending' ELSE r.status END,\n updated_at = now()\n WHERE r.id = v_id AND r.frozen_at IS NULL;\n RETURN 0;\nEND $fn$;\n\n-- ─── 3. Materializar as linhas em branco de UMA sessão ──────────────────────\n\nCREATE OR REPLACE FUNCTION public.clinical_requirements_materialize(p_session uuid)\nRETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'\nAS $fn$\nDECLARE\n s record; sv record; r record; cfg jsonb;\n v_mode text; v_n int := 0; v_booking_status text; v_rule text;\n v_is_revision boolean; v_has_parent boolean; v_rev_num int; v_kind text;\nBEGIN\n SELECT * INTO s FROM public.plg_clinical_sessions WHERE id = p_session;\n IF NOT FOUND OR s.booking_id IS NULL THEN RETURN 0; END IF;\n\n v_mode := public.clinical_requirements_mode(s.tenant_id);\n IF v_mode = 'off' THEN RETURN 0; END IF;\n\n SELECT a.status INTO v_booking_status FROM public.appointments a WHERE a.id = s.booking_id;\n -- Histórico não ganha linha: sessão já executada em atendimento que não está\n -- em curso é fato consumado (mesma regra do V1).\n IF s.status IN ('executed', 'invoiced') AND coalesce(v_booking_status, '') <> 'in_progress' THEN\n RETURN 0;\n END IF;\n\n BEGIN\n SELECT p.service_kind, p.requires_evaluation, p.always_ask_used_products, p.category_id\n INTO sv FROM public.products p WHERE p.id = s.service_id;\n IF NOT FOUND THEN RETURN 0; END IF;\n v_kind := coalesce(sv.service_kind, 'standard');\n\n -- QUEM É REVISÃO. O V1 tem um booleano (\\`is_revision\\`) ao lado do número.\n -- No V2 sobrou só o número — e o ETL gravou \\`revision_number = 1\\` em TODA\n -- sessão, então o número não distingue nada: lido como \"é revisão\", ele\n -- transformou 54 de 54 sessões em revisão, tirou o item de fatura de 21\n -- delas e inventou vínculo de revisão em 43. O que de fato separa as duas\n -- coisas no V2 é o PAI: revisão aponta para a sessão original. Conferido\n -- contra as 288 exigências migradas destes mesmos atendimentos: o V1\n -- gravou \\`invoice_item\\` exatamente nas 21 sessões \\`standard\\` sem pai, e\n -- \\`revision_link\\` exatamente nas 8 de \\`return\\` mais as 3 \\`standard\\` com pai.\n v_rev_num := coalesce(s.revision_number, 0);\n v_has_parent := s.parent_session_id IS NOT NULL;\n v_is_revision := v_has_parent OR v_kind = 'return';\n\n cfg := coalesce((SELECT t.settings -> 'evaluation_flow' FROM public.tenants t WHERE t.id = s.tenant_id), '{}'::jsonb);\n IF jsonb_typeof(cfg) <> 'object' THEN cfg := '{}'::jsonb; END IF;\n\n -- a) Formulários universais do tenant — escopo atendimento.\n FOR r IN SELECT ft.id FROM public.plg_forms_templates ft\n WHERE ft.tenant_id = s.tenant_id AND ft.is_active AND coalesce(ft.is_deleted, false) = false\n AND coalesce(ft.required_on_attendance, false)\n LOOP\n v_n := v_n + public.clinical_requirement_upsert(s.tenant_id, s.unit_id, s.booking_id, s.id,\n s.party_id, s.service_id, 'form', 'attendance', 'form_template', r.id,\n 'forms_templates.required_on_attendance', false, false);\n END LOOP;\n\n IF NOT v_has_parent THEN\n -- b) Formulários do serviço.\n FOR r IN SELECT sd.template_id, coalesce(sd.requirement_scope, 'attendance') AS scope\n FROM public.plg_forms_service_defaults sd\n JOIN public.plg_forms_templates ft ON ft.id = sd.template_id AND ft.is_active\n WHERE sd.product_id = s.service_id AND sd.tenant_id = s.tenant_id\n LOOP\n v_n := v_n + public.clinical_requirement_upsert(s.tenant_id, s.unit_id, s.booking_id, s.id,\n s.party_id, s.service_id, 'form',\n CASE WHEN r.scope = 'service' THEN 'service' ELSE 'attendance' END,\n 'form_template', r.template_id, 'forms_service_defaults', false, false);\n END LOOP;\n ELSE\n -- c) Formulários da revisão, pelo número dela. No V2 os modelos da revisão\n -- moram numa lista dentro da própria revisão.\n FOR r IN SELECT (e.value #>> '{}')::uuid AS template_id\n FROM public.plg_clinical_service_revisions rev,\n LATERAL jsonb_array_elements(coalesce(rev.form_template_refs, '[]'::jsonb)) e\n WHERE rev.tenant_id = s.tenant_id AND rev.service_id = s.service_id\n AND rev.revision_number = v_rev_num\n AND (e.value #>> '{}') ~ '^[0-9a-f-]{36}$'\n LOOP\n v_n := v_n + public.clinical_requirement_upsert(s.tenant_id, s.unit_id, s.booking_id, s.id,\n s.party_id, s.service_id, 'form', 'service', 'form_template', r.template_id,\n 'clinical_service_revisions.form_template_refs', false, false);\n END LOOP;\n END IF;\n\n -- d, e, f) CONTRATO e TERMO DE SESSÃO: fora, por falta de modelo no V2.\n -- Ver o cabeçalho desta migration.\n\n -- g) Avaliação vinculada.\n IF v_kind = 'standard' THEN\n IF coalesce(sv.requires_evaluation, false) THEN\n v_n := v_n + public.clinical_requirement_upsert(s.tenant_id, s.unit_id, s.booking_id, s.id,\n s.party_id, s.service_id, 'evaluation', 'service', NULL, NULL,\n 'products.requires_evaluation', false, false);\n ELSIF coalesce((cfg ->> 'require_evaluation_for_kind_service')::boolean, false) THEN\n v_n := v_n + public.clinical_requirement_upsert(s.tenant_id, s.unit_id, s.booking_id, s.id,\n s.party_id, s.service_id, 'evaluation', 'service', NULL, NULL,\n 'evaluation_flow.require_evaluation_for_kind_service', false, false);\n END IF;\n END IF;\n\n -- h) Revisão vinculada ao serviço original: trava começar E terminar.\n IF v_is_revision AND coalesce((cfg ->> 'revision_link_required')::boolean, false) THEN\n v_n := v_n + public.clinical_requirement_upsert(s.tenant_id, s.unit_id, s.booking_id, s.id,\n s.party_id, s.service_id, 'revision_link', 'service', NULL, NULL,\n 'evaluation_flow.revision_link_required', true, true);\n END IF;\n\n -- i) Produtos utilizados: ficha técnica, \"sempre perguntar\", ou já lançado.\n v_rule := NULL;\n IF EXISTS (SELECT 1 FROM public.plg_inventory_recipes rc\n WHERE rc.product_id = s.service_id AND rc.tenant_id = s.tenant_id\n AND coalesce(rc.is_active, true)) THEN\n v_rule := 'inventory_recipes';\n ELSIF coalesce(sv.always_ask_used_products, false) THEN\n v_rule := 'products.always_ask_used_products';\n ELSIF EXISTS (SELECT 1 FROM public.plg_clinical_session_products sp\n WHERE sp.session_id = s.id\n OR (s.order_item_id IS NOT NULL AND sp.order_item_id = s.order_item_id)) THEN\n v_rule := 'clinical_session_products';\n END IF;\n IF v_rule IS NOT NULL THEN\n v_n := v_n + public.clinical_requirement_upsert(s.tenant_id, s.unit_id, s.booking_id, s.id,\n s.party_id, s.service_id, 'products', 'service', NULL, NULL, v_rule, false, false);\n END IF;\n\n -- j) Item de fatura: serviço comum, não-revisão.\n IF v_kind = 'standard' AND NOT v_has_parent THEN\n v_n := v_n + public.clinical_requirement_upsert(s.tenant_id, s.unit_id, s.booking_id, s.id,\n s.party_id, s.service_id, 'invoice_item', 'service', NULL, NULL, 'billing', false, false);\n END IF;\n EXCEPTION WHEN others THEN\n -- Nunca derruba quem chama: a fonte (a sessão) não pode falhar por causa da régua.\n RAISE WARNING 'clinical_requirements_materialize(%): %', p_session, sqlerrm;\n RETURN 0;\n END;\n RETURN v_n;\nEND $fn$;\n\n-- ─── 4. Reapurar as linhas de UM atendimento ────────────────────────────────\n\nCREATE OR REPLACE FUNCTION public.clinical_requirements_refresh(p_booking uuid, p_final boolean DEFAULT false)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'\nAS $fn$\nDECLARE\n a record; v_mode text; v_now timestamptz := now(); v_eval jsonb;\n n_mat int := 0; n_reopen int := 0;\n -- \\`v_sessao\\`, não \\`r\\`: plpgsql resolve o nome da VARIÁVEL antes do apelido da\n -- tabela, então um \\`record r\\` declarado aqui sequestra todo \\`r.coluna\\` dos\n -- UPDATEs abaixo — e o erro que sai é \"record r has no field booking_id\".\n v_sessao record;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.party_id, ap.status INTO a\n FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n v_mode := public.clinical_requirements_mode(a.tenant_id);\n IF v_mode = 'off' THEN RETURN jsonb_build_object('ok', true, 'mode', v_mode, 'skipped', true); END IF;\n\n p_final := coalesce(p_final, false) AND a.status = 'completed';\n\n BEGIN\n PERFORM pg_advisory_xact_lock(hashtextextended('clinical_req:' || p_booking::text, 0));\n IF a.status = 'completed' AND NOT p_final THEN\n RETURN jsonb_build_object('ok', true, 'mode', v_mode, 'frozen', true);\n END IF;\n\n -- 1) Materializa o que falta (sessão nova, ficha técnica criada depois).\n FOR v_sessao IN SELECT cs.id FROM public.plg_clinical_sessions cs WHERE cs.booking_id = p_booking\n LOOP\n n_mat := n_mat + public.clinical_requirements_materialize(v_sessao.id);\n END LOOP;\n\n v_eval := coalesce((SELECT t.settings -> 'evaluation_flow' FROM public.tenants t WHERE t.id = a.tenant_id), '{}'::jsonb);\n IF jsonb_typeof(v_eval) <> 'object' THEN v_eval := '{}'::jsonb; END IF;\n\n -- 1b) Regra que saiu da configuração para de bloquear (o upsert reabre se voltar).\n UPDATE public.plg_clinical_requirements r\n SET status = 'not_applicable', candidate_type = NULL, candidate_id = NULL, updated_at = v_now\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.frozen_at IS NULL\n AND r.metadata -> 'migration' IS NULL\n AND ((r.kind = 'revision_link' AND NOT coalesce((v_eval ->> 'revision_link_required')::boolean, false))\n OR (r.kind = 'evaluation'\n AND r.rule_sources = '[\"evaluation_flow.require_evaluation_for_kind_service\"]'::jsonb\n AND NOT coalesce((v_eval ->> 'require_evaluation_for_kind_service')::boolean, false)));\n\n -- 2) Reabre o que perdeu a fonte. Linha congelada não se mexe.\n UPDATE public.plg_clinical_requirements r\n SET status = 'pending', subject_type = NULL, subject_id = NULL, resolved_via = NULL,\n resolved_at = NULL, valid_until = NULL, updated_at = v_now\n WHERE r.booking_id = p_booking AND r.status = 'resolved' AND r.frozen_at IS NULL\n AND ((r.subject_type = 'documents' AND NOT EXISTS (SELECT 1 FROM public.documents d WHERE d.id = r.subject_id))\n OR (r.subject_type = 'clinical_sessions' AND NOT EXISTS (SELECT 1 FROM public.plg_clinical_sessions x WHERE x.id = r.subject_id))\n OR (r.subject_type = 'items' AND NOT EXISTS (SELECT 1 FROM public.items oi WHERE oi.id = r.subject_id)));\n GET DIAGNOSTICS n_reopen = ROW_COUNT;\n\n -- 3) Formulário universal: a última resposta do cliente, em qualquer\n -- atendimento, ainda dentro da validade do modelo.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'documents', subject_id = x.id,\n resolved_via = CASE WHEN x.subject_id = p_booking THEN 'attendance' ELSE 'auto_client' END,\n resolved_at = v_now,\n valid_until = CASE WHEN x.validity_days IS NULL THEN NULL\n ELSE x.eff + make_interval(days => x.validity_days) END,\n updated_at = v_now\n FROM (SELECT DISTINCT ON (fd.template_id) d.id, fd.template_id, d.subject_id,\n coalesce(d.updated_at, d.created_at) AS eff, ft.validity_days\n FROM public.plg_forms_documents fd\n JOIN public.documents d ON d.id = fd.document_id\n JOIN public.plg_forms_templates ft ON ft.id = fd.template_id\n WHERE d.person_id = a.party_id AND d.tenant_id = a.tenant_id\n ORDER BY fd.template_id, coalesce(d.updated_at, d.created_at) DESC) x\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'form'\n AND r.scope = 'attendance' AND r.ref_id = x.template_id\n AND r.rule_sources @> '[\"forms_templates.required_on_attendance\"]'::jsonb\n AND (x.validity_days IS NULL OR x.eff + make_interval(days => x.validity_days) > v_now);\n\n -- 4) Demais formulários: qualquer resposta DESTE atendimento com o modelo.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'documents', subject_id = x.id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM (SELECT DISTINCT ON (fd.template_id, coalesce(fd.product_id, '00000000-0000-0000-0000-000000000000'::uuid))\n d.id, fd.template_id, fd.product_id\n FROM public.plg_forms_documents fd\n JOIN public.documents d ON d.id = fd.document_id\n WHERE d.subject_type = 'appointment' AND d.subject_id = p_booking\n ORDER BY fd.template_id, coalesce(fd.product_id, '00000000-0000-0000-0000-000000000000'::uuid),\n coalesce(d.updated_at, d.created_at) DESC) x\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'form'\n AND r.ref_id = x.template_id\n AND NOT (r.rule_sources @> '[\"forms_templates.required_on_attendance\"]'::jsonb)\n AND (x.product_id IS NULL OR r.scope = 'attendance' OR x.product_id = r.service_id);\n\n -- 5) Avaliação vinculada: a sessão aponta a avaliação que a autorizou.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = x.eval_id,\n resolved_via = CASE WHEN x.same_booking THEN 'attendance' ELSE 'auto_client' END,\n resolved_at = v_now, updated_at = v_now\n FROM (SELECT r2.id AS req_id, e.id AS eval_id, (e.booking_id = p_booking) AS same_booking\n FROM public.plg_clinical_requirements r2\n JOIN public.plg_clinical_sessions cs ON cs.id = r2.session_id\n JOIN public.plg_clinical_sessions e ON e.id = cs.source_evaluation_id\n WHERE r2.booking_id = p_booking AND r2.kind = 'evaluation' AND r2.frozen_at IS NULL\n AND (r2.status = 'pending' OR (r2.status = 'resolved' AND r2.resolved_via = 'legacy_grace'))) x\n WHERE r.id = x.req_id;\n\n -- 5b) Graça legada: o cliente já tem atendimento concluído com serviço de\n -- avaliação. É a mesma concessão do V1, e vai marcada como tal.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'appointments', subject_id = x.booking_id,\n resolved_via = 'legacy_grace', resolved_at = v_now, updated_at = v_now\n FROM (SELECT ap.id AS booking_id\n FROM public.appointments ap\n JOIN public.appointment_items ai ON ai.booking_id = ap.id\n JOIN public.products p ON p.id = ai.service_id AND p.service_kind = 'evaluation'\n WHERE ap.party_id = a.party_id AND ap.tenant_id = a.tenant_id AND ap.status = 'completed'\n ORDER BY ap.starts_at DESC NULLS LAST LIMIT 1) x\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'evaluation';\n\n -- 6) Revisão vinculada ao serviço original.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = cs.parent_session_id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM public.plg_clinical_sessions cs\n WHERE cs.id = r.session_id AND r.booking_id = p_booking\n AND r.status = 'pending' AND r.kind = 'revision_link'\n AND cs.parent_session_id IS NOT NULL\n AND EXISTS (SELECT 1 FROM public.plg_clinical_sessions p WHERE p.id = cs.parent_session_id);\n\n -- 7) Produtos: a sessão declara que foram conferidos.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = cs.id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM public.plg_clinical_sessions cs\n WHERE cs.id = r.session_id AND r.booking_id = p_booking\n AND r.status = 'pending' AND r.kind = 'products'\n AND coalesce(cs.products_initialized, false);\n\n -- 8) Item de pedido vinculado. A linha do pedido no V2 é \\`public.items\\` —\n -- não existe \\`order_items\\`; o pedido é o documento e o item é dele.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'items', subject_id = oi.id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM public.plg_clinical_sessions cs\n JOIN public.items oi ON oi.id = cs.order_item_id\n WHERE cs.id = r.session_id AND r.booking_id = p_booking\n AND r.status = 'pending' AND r.kind = 'invoice_item';\n\n -- 9) Congelamento: só na última passada, com o atendimento concluído.\n IF p_final THEN\n UPDATE public.plg_clinical_requirements\n SET frozen_at = coalesce(frozen_at, v_now), updated_at = v_now\n WHERE booking_id = p_booking;\n END IF;\n\n RETURN jsonb_build_object(\n 'ok', true, 'mode', v_mode, 'materialized', n_mat, 'reopened', n_reopen,\n 'pending', (SELECT count(*) FROM public.plg_clinical_requirements WHERE booking_id = p_booking AND status = 'pending'),\n 'resolved', (SELECT count(*) FROM public.plg_clinical_requirements WHERE booking_id = p_booking AND status = 'resolved'),\n 'frozen', p_final);\n EXCEPTION WHEN others THEN\n RAISE WARNING 'clinical_requirements_refresh(%): %', p_booking, sqlerrm;\n RETURN jsonb_build_object('ok', false, 'error', sqlerrm);\n END;\nEND $fn$;\n\n-- ─── 5. Quem pode chamar ────────────────────────────────────────────────────\n-- Escrita só pelas funções (definer). O app chama o refresh; o resto é interno.\n\nREVOKE ALL ON FUNCTION public.clinical_requirement_upsert(uuid, uuid, uuid, uuid, uuid, uuid, text, text, text, uuid, text, boolean, boolean) FROM public, anon, authenticated;\nREVOKE ALL ON FUNCTION public.clinical_requirements_materialize(uuid) FROM public, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.clinical_requirements_mode(uuid) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.clinical_requirements_refresh(uuid, boolean) TO authenticated;\n`\n\nexport const MIGRATION_005_COMECAR_O_ATENDIMENTO_E_UM_GESTO_SO = `-- ============================================================================\n-- Começar o atendimento é UM gesto — e uma transação só\n--\n-- No V1, \"Iniciar Atendimento\" grava três coisas no mesmo instante: o\n-- agendamento vira \\`in_progress\\`, a execução de cada serviço nasce, e a régua é\n-- materializada (\\`rpc/refresh_execution_requirements\\`). Se qualquer uma falhar\n-- sozinha o atendimento fica meio aberto: status trocado sem régua, ou régua\n-- sem sessão — e a tela mostra uma bola que não pertence a nada.\n--\n-- Por isso é uma função e não três chamadas do app. A recepção e o profissional\n-- podem apertar o botão ao mesmo tempo, e o segundo não pode duplicar sessão.\n--\n-- QUEM PODE: o V1 só monta a régua para quem está VINCULADO A UM PROFISSIONAL,\n-- e só enxerga o atendimento DELE com aquele cliente. Aqui a regra é a mesma —\n-- e a recusa é FALADA (\\`reason\\`), não silenciosa: a tela precisa dizer por que\n-- o botão não serve, em vez de sumir com ele.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_attendance_start(p_booking uuid)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_created int := 0; v_item record; v_refresh jsonb;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.unit_id, ap.party_id, ap.assignee_id, ap.status, ap.starts_at\n INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'not_found');\n END IF;\n\n -- A pessoa por trás do login. Nulo = login sem ficha (o dono que criou o\n -- espaço, a licença de suporte): vê tudo, atende ninguém.\n v_person := public.current_person_id();\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional');\n END IF;\n IF a.assignee_id IS DISTINCT FROM v_person THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_seu_atendimento');\n END IF;\n -- Atender o que não é de hoje abriria um atendimento paralelo ao agendado —\n -- o mesmo motivo que fez o V1 trocar o botão conforme o dia.\n IF a.starts_at::date <> (now() AT TIME ZONE 'America/Sao_Paulo')::date THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_de_hoje');\n END IF;\n IF a.status IN ('completed', 'cancelled', 'no_show') THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'ja_encerrado', 'status', a.status);\n END IF;\n\n -- Uma sessão por serviço do agendamento. \\`booking_item_id\\` é a chave de\n -- idempotência: apertar duas vezes (ou duas pessoas ao mesmo tempo) não\n -- duplica nada.\n FOR v_item IN\n SELECT ai.id, ai.service_id, ai.assignee_id\n FROM public.appointment_items ai\n WHERE ai.booking_id = p_booking\n LOOP\n INSERT INTO public.plg_clinical_sessions (\n tenant_id, unit_id, status, booking_id, booking_item_id, party_id,\n assignee_id, performed_by_id, service_id, scheduled_for, started_at)\n SELECT a.tenant_id, a.unit_id, 'in_service', a.id, v_item.id, a.party_id,\n coalesce(v_item.assignee_id, a.assignee_id), v_person, v_item.service_id,\n a.starts_at, now()\n WHERE NOT EXISTS (\n SELECT 1 FROM public.plg_clinical_sessions cs\n WHERE cs.booking_id = p_booking AND cs.booking_item_id = v_item.id);\n IF FOUND THEN v_created := v_created + 1; END IF;\n END LOOP;\n\n UPDATE public.appointments\n SET status = 'in_progress', updated_at = now()\n WHERE id = p_booking AND status <> 'in_progress';\n\n v_refresh := public.clinical_requirements_refresh(p_booking);\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sessoes_criadas', v_created,\n 'sessoes', (SELECT count(*) FROM public.plg_clinical_sessions WHERE booking_id = p_booking),\n 'regua', v_refresh);\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_attendance_start(uuid) TO authenticated;\n`\n\nexport const MIGRATION_006_CONTRATAR_NAO_E_PAGAR = `-- ============================================================================\n-- CONTRATAR não é PAGAR\n--\n-- O passo mais delicado do turno do injetor, e a demonstração do V1 para nele\n-- para explicar: *\"Mas até então ele não pagou ainda? Não pagou — mas ele vai\n-- executar.\"* Contratar cria a DÍVIDA; receber é papel da recepção, em outra\n-- tela e outro momento.\n--\n-- No V1 isto grava \\`generated_contracts\\` + \\`invoices\\` + \\`invoice_items\\`. No V2\n-- o documento é o PEDIDO, e há uma porta só para abri-lo: \\`order_create\\` — a\n-- mesma que o PDV, o totem e a agenda usam. Nada aqui escreve uma linha de\n-- fatura à mão; a cobrança nasce depois, quando o atendimento fecha.\n--\n-- Por que uma função e não três chamadas do app: contratar é abrir o pedido,\n-- ligar cada sessão à sua linha e reapurar a régua. Pedido aberto com a sessão\n-- solta é um atendimento que cobra e não sabe o que executou.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_contract_services(p_booking uuid, p_lines jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_order uuid; v_line jsonb; v_item uuid; v_n int := 0;\n v_rpc_lines jsonb := '[]'::jsonb; v_idx int := 0;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.unit_id, ap.party_id, ap.assignee_id, ap.status, ap.order_id\n INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n v_person := public.current_person_id();\n IF v_person IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional'); END IF;\n IF a.assignee_id IS DISTINCT FROM v_person THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_seu_atendimento');\n END IF;\n IF a.status <> 'in_progress' THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'atendimento_nao_esta_em_curso');\n END IF;\n IF jsonb_typeof(p_lines) <> 'array' OR jsonb_array_length(p_lines) = 0 THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nada_marcado');\n END IF;\n\n -- As linhas, no formato que \\`order_create\\` já entende.\n FOR v_line IN SELECT * FROM jsonb_array_elements(p_lines)\n LOOP\n v_rpc_lines := v_rpc_lines || jsonb_build_object(\n 'product_id', v_line ->> 'service_id',\n 'name', v_line ->> 'name',\n 'quantity', coalesce((v_line ->> 'quantity')::numeric, 1),\n 'unit_price', coalesce((v_line ->> 'unit_price')::numeric, 0),\n 'sort_order', v_idx);\n v_idx := v_idx + 1;\n END LOOP;\n\n -- O pedido do atendimento. \\`idempotency_key\\` pelo agendamento: dois toques\n -- no \"Contratar\" não abrem duas contas para a mesma visita.\n IF a.order_id IS NULL THEN\n v_order := public.order_create(\n p_kind => 'appointment',\n p_lines => v_rpc_lines,\n p_party_id => a.party_id,\n p_unit_id => a.unit_id,\n p_status => 'scheduled',\n p_assignee_id => a.assignee_id,\n p_metadata => jsonb_build_object('agenda', jsonb_build_object('booking_id', p_booking), 'source', 'clinical:contratacao'),\n p_idempotency_key => 'clinical-contract:' || p_booking::text);\n UPDATE public.appointments SET order_id = v_order, updated_at = now() WHERE id = p_booking;\n ELSE\n -- Pedido já existe (a agenda abriu na marcação): acrescenta o que falta.\n v_order := a.order_id;\n v_idx := coalesce((SELECT max(i.sort_order) + 1 FROM public.items i WHERE i.order_id = v_order), 0);\n FOR v_line IN SELECT * FROM jsonb_array_elements(p_lines)\n LOOP\n INSERT INTO public.items (tenant_id, unit_id, order_id, product_id, name, quantity, unit_price, total, sort_order)\n VALUES (a.tenant_id, a.unit_id, v_order, (v_line ->> 'service_id')::uuid, v_line ->> 'name',\n coalesce((v_line ->> 'quantity')::numeric, 1),\n coalesce((v_line ->> 'unit_price')::numeric, 0),\n coalesce((v_line ->> 'quantity')::numeric, 1) * coalesce((v_line ->> 'unit_price')::numeric, 0),\n v_idx);\n v_idx := v_idx + 1;\n END LOOP;\n END IF;\n\n -- Cada sessão passa a apontar a SUA linha. Sem isto o pedido cobra e o\n -- atendimento não sabe o que executou — e a bola \"Conta do atendimento\"\n -- nunca fecha, porque é exatamente este vínculo que ela procura.\n UPDATE public.plg_clinical_sessions cs\n SET order_item_id = i.id,\n status = CASE WHEN cs.status IN ('quoted', 'pending', 'scheduled') THEN 'contracted' ELSE cs.status END,\n updated_at = now()\n FROM public.items i\n WHERE cs.booking_id = p_booking\n AND cs.order_item_id IS NULL\n AND i.order_id = v_order\n AND i.product_id = cs.service_id\n AND NOT EXISTS (SELECT 1 FROM public.plg_clinical_sessions o\n WHERE o.order_item_id = i.id AND o.id <> cs.id);\n GET DIAGNOSTICS v_n = ROW_COUNT;\n\n RETURN jsonb_build_object(\n 'ok', true, 'order_id', v_order, 'sessoes_ligadas', v_n,\n 'total', (SELECT coalesce(sum(i.total), 0) FROM public.items i WHERE i.order_id = v_order),\n 'regua', public.clinical_requirements_refresh(p_booking));\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_contract_services(uuid, jsonb) TO authenticated;\n`\n\nexport const MIGRATION_007_FINALIZAR_ACENDE_O_BALCAO = `-- ============================================================================\n-- FINALIZAR acende o balcão\n--\n-- O último gesto do profissional é o PRIMEIRO da recepção. No V1, o instante em\n-- que o atendimento vai para \\`completed\\` sem \\`checkout_confirmed_at\\` acende o\n-- rodapé do balcão — com contador, pulso e bipe.\n--\n-- Duas decisões do V1 que vêm junto, e que são o oposto uma da outra:\n--\n-- · o aviso de serviços não executados AVISA E DEIXA PASSAR. Travar criaria\n-- um atendimento sem saída pela interface; quem não executou cancela o item\n-- na conta do cliente. Por isso \\`p_force\\`: a primeira chamada devolve a\n-- lista, a segunda executa.\n-- · a exigência que bloqueia TERMINAR (\\`blocks_finish\\`) é outra coisa, e essa\n-- trava de verdade — é o termo de sessão do V1, que existe para não haver\n-- execução sem assinatura.\n--\n-- E o que NÃO se grava aqui: \\`checkout_confirmed_at\\`. Ele é da recepção, e é o\n-- que tira o cartão da fila — receber e conferir são duas coisas.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_attendance_finish(p_booking uuid, p_force boolean DEFAULT false)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_bloqueia jsonb; v_nao_executados jsonb;\n v_avisados int := 0; v_total numeric := 0; v_cliente text;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.unit_id, ap.party_id, ap.assignee_id, ap.status, ap.order_id\n INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n v_person := public.current_person_id();\n IF v_person IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional'); END IF;\n IF a.assignee_id IS DISTINCT FROM v_person THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_seu_atendimento');\n END IF;\n IF a.status <> 'in_progress' THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'atendimento_nao_esta_em_curso');\n END IF;\n\n -- A trava de verdade: exigência que impede TERMINAR.\n SELECT coalesce(jsonb_agg(jsonb_build_object('kind', r.kind, 'service_id', r.service_id)), '[]'::jsonb)\n INTO v_bloqueia\n FROM public.plg_clinical_requirements r\n WHERE r.booking_id = p_booking AND r.blocks_finish AND r.status = 'pending';\n IF jsonb_array_length(v_bloqueia) > 0 THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'exigencia_bloqueia', 'itens', v_bloqueia);\n END IF;\n\n -- O aviso que não trava: serviços contratados e ainda não executados.\n SELECT coalesce(jsonb_agg(p.name ORDER BY p.name), '[]'::jsonb) INTO v_nao_executados\n FROM public.plg_clinical_sessions cs\n JOIN public.products p ON p.id = cs.service_id\n WHERE cs.booking_id = p_booking AND cs.status <> 'executed';\n IF jsonb_array_length(v_nao_executados) > 0 AND NOT coalesce(p_force, false) THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'servicos_nao_executados', 'itens', v_nao_executados);\n END IF;\n\n -- \"Finalizar agora registra estes serviços como executados\" — texto do V1.\n UPDATE public.plg_clinical_sessions\n SET status = 'executed', ended_at = coalesce(ended_at, now()), updated_at = now()\n WHERE booking_id = p_booking AND status <> 'executed';\n\n UPDATE public.appointments\n SET status = 'completed', updated_at = now()\n WHERE id = p_booking;\n\n -- Congela a trilha: depois de encerrado, a régua não se reapura.\n PERFORM public.clinical_requirements_refresh(p_booking, true);\n\n SELECT coalesce(sum(i.total), 0) INTO v_total FROM public.items i WHERE i.order_id = a.order_id;\n SELECT pe.name INTO v_cliente FROM public.people pe WHERE pe.id = a.party_id;\n\n -- O BALCÃO ACENDE.\n --\n -- Quem recebe: a recepção da conta. Onde não houver ninguém com o papel de\n -- recepção — e é o caso do tenant migrado, com 0 membros nele —, cai para\n -- gerente e dono. Um aviso que não chega a ninguém deixaria a conta parada\n -- sem que nada na tela dissesse por quê.\n WITH alvo AS (\n SELECT m.user_id\n FROM app.memberships m\n JOIN app.roles r ON r.id = m.role_id\n WHERE m.tenant_id = a.tenant_id AND coalesce(m.active, true)\n AND r.key IN ('recepcao', 'reception')\n UNION ALL\n SELECT m.user_id\n FROM app.memberships m\n JOIN app.roles r ON r.id = m.role_id\n WHERE m.tenant_id = a.tenant_id AND coalesce(m.active, true)\n AND r.key IN ('owner', 'manager')\n AND NOT EXISTS (\n SELECT 1 FROM app.memberships m2 JOIN app.roles r2 ON r2.id = m2.role_id\n WHERE m2.tenant_id = a.tenant_id AND coalesce(m2.active, true)\n AND r2.key IN ('recepcao', 'reception'))\n )\n INSERT INTO public.notifications (tenant_id, user_id, title, body, type, link, subject_type, subject_id)\n SELECT a.tenant_id, alvo.user_id,\n 'Checkout pendente',\n coalesce(v_cliente, 'Cliente') || ' terminou o atendimento'\n -- O dinheiro escrito como o Brasil escreve. \\`to_char\\` usa o\n -- \\`lc_numeric\\` do BANCO para o grupo e a vírgula, e o cluster está\n -- em C: saía \"R$ 999.00\" no aviso de uma recepção brasileira. A\n -- troca é feita à mão para não depender de locale nenhum.\n || CASE WHEN v_total > 0 THEN ' · R$ ' ||\n replace(replace(replace(to_char(v_total, 'FM999G999G999D00'), ',', '§'), '.', ','), '§', '.')\n ELSE '' END,\n 'checkout_pendente', '#/checkin', 'appointment', p_booking\n FROM alvo\n ON CONFLICT (user_id, subject_type, subject_id, type) WHERE subject_id IS NOT NULL DO NOTHING;\n GET DIAGNOSTICS v_avisados = ROW_COUNT;\n\n RETURN jsonb_build_object(\n 'ok', true, 'avisados', v_avisados, 'total', v_total,\n 'executados', (SELECT count(*) FROM public.plg_clinical_sessions WHERE booking_id = p_booking AND status = 'executed'));\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_attendance_finish(uuid, boolean) TO authenticated;\n`\n\nexport const MIGRATION_008_CONFERIR_NAO_E_RECEBER = `-- ============================================================================\n-- CONFERIR não é RECEBER\n--\n-- O V1 é explícito: *\"Receber move dinheiro; concluir checkout é a recepção\n-- dizendo 'conferi a sessão'. O cartão fica na fila enquanto houver valor em\n-- aberto OU checkout por confirmar.\"* São duas coisas, e juntá-las numa só\n-- faria a fila esvaziar quando o dinheiro entra — mesmo que ninguém tenha\n-- olhado o que foi executado.\n--\n-- \\`checkout_confirmed_at\\` mora no \\`metadata->'agenda'\\` do agendamento, que é\n-- onde o ETL do V1 o deixou: 332 atendimentos migrados já o carregam ali.\n-- Escrever numa coluna nova faria o histórico e o presente responderem em\n-- lugares diferentes.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_checkout_confirm(p_booking uuid)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_aberto numeric := 0;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.status, ap.order_id, ap.metadata\n INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n -- Quem confere é quem está no balcão. Diferente de iniciar o atendimento,\n -- aqui NÃO se exige ser o profissional do agendamento: conferir é da\n -- recepção, e a recepção não atende ninguém.\n IF auth.uid() IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_sessao'); END IF;\n v_person := public.current_person_id();\n\n IF a.status <> 'completed' THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'atendimento_nao_terminou');\n END IF;\n IF a.metadata -> 'agenda' ->> 'checkout_confirmed_at' IS NOT NULL THEN\n RETURN jsonb_build_object('ok', true, 'ja_confirmado', true);\n END IF;\n\n -- Só o ramo \\`agenda\\` é reescrito, e ele é reescrito a partir do que já\n -- estava lá: o \\`||\\` sobre o objeto inteiro apagaria o que o ETL do V1\n -- deixou no metadata do agendamento.\n UPDATE public.appointments ap\n SET metadata = coalesce(ap.metadata, '{}'::jsonb)\n || jsonb_build_object('agenda',\n coalesce(ap.metadata -> 'agenda', '{}'::jsonb)\n || jsonb_build_object(\n 'checkout_confirmed_at', to_jsonb(now()),\n 'checkout_confirmed_by', to_jsonb(coalesce(v_person::text, auth.uid()::text)))),\n updated_at = now()\n WHERE ap.id = p_booking;\n\n -- O aviso cumpriu o papel dele. Deixá-lo não lido faria o sino cobrar uma\n -- conferência que já aconteceu.\n UPDATE public.notifications\n SET read = true\n WHERE subject_type = 'appointment' AND subject_id = p_booking AND NOT read;\n\n SELECT coalesce(sum(i.total), 0) INTO v_aberto FROM public.items i WHERE i.order_id = a.order_id;\n\n RETURN jsonb_build_object('ok', true, 'total', v_aberto);\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_checkout_confirm(uuid) TO authenticated;\n`\n\nexport const MIGRATION_009_O_BOTAO_DE_COBRAR_COBRA = `-- ============================================================================\n-- O botão de cobrar COBRA\n--\n-- SINTOMA, visto na tela: a conta do cliente mostra \"Este atendimento ainda não\n-- tem cobrança · **Gerar recebimento**\", e o clique não faz nada. Nenhum\n-- movimento nasce, nenhuma mensagem aparece. O botão existe porque o painel o\n-- oferece, e quem o liga é o APP hospedeiro — que neste app não liga.\n--\n-- Botão morto é pior que botão ausente: quem clica confia no botão.\n--\n-- O caminho do dinheiro no V2 é um só: o recebível é levantado quando o PEDIDO\n-- FECHA (\\`orders_raise_receivable_on_close\\`). Então cobrar é fechar o pedido do\n-- atendimento — e não escrever fatura à mão, que é o que a regra do SDK proíbe.\n--\n-- É aqui, e não na contratação, porque o V1 separa as duas coisas: contratar\n-- cria a dívida; cobrar é da recepção, depois de conferir o que foi executado.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_checkout_charge(p_booking uuid)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_linhas int; v_total numeric; v_mov int;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.status, ap.order_id INTO a\n FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n IF auth.uid() IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_sessao'); END IF;\n\n IF a.order_id IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nada_contratado');\n END IF;\n\n SELECT count(*), coalesce(sum(i.total), 0) INTO v_linhas, v_total\n FROM public.items i WHERE i.order_id = a.order_id;\n IF v_linhas = 0 THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nada_contratado');\n END IF;\n\n -- Idempotente: o pedido já fechado devolve o que existe, sem levantar um\n -- segundo recebível para a mesma visita.\n SELECT count(*) INTO v_mov FROM public.plg_financial_movements m WHERE m.invoice_id = a.order_id;\n IF v_mov > 0 THEN\n RETURN jsonb_build_object('ok', true, 'ja_cobrado', true, 'total', v_total);\n END IF;\n\n UPDATE public.orders SET status = 'completed', updated_at = now()\n WHERE id = a.order_id AND status <> 'completed';\n\n SELECT count(*) INTO v_mov FROM public.plg_financial_movements m WHERE m.invoice_id = a.order_id;\n RETURN jsonb_build_object('ok', true, 'total', v_total, 'movimentos', v_mov);\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_checkout_charge(uuid) TO authenticated;\n`\n\nexport const MIGRATION_010_A_INDICACAO_VAI_PARA_O_FUNIL = `-- ============================================================================\n-- A INDICAÇÃO vai para o funil\n--\n-- O V1 escreve, na própria tela da avaliação: *\"Indique procedimentos ou\n-- produtos para o cliente — eles alimentam as recomendações do CRM e a etapa de\n-- orçamento.\"* E a rede confirma: concluir a avaliação faz \\`POST\n-- opportunity_recommendations\\`.\n--\n-- **Indicação não é venda.** É o que o profissional recomenda; virar cobrança é\n-- outra decisão, do paciente, na contratação. As duas palavras existem\n-- separadas no V1 porque separam dois momentos e duas responsabilidades.\n--\n-- No V2 a cesta do interesse pende de um NEGÓCIO (\\`plg_crm_deal_interests.\n-- deal_id\\` é NOT NULL). Então a avaliação abre — ou reaproveita — um negócio do\n-- cliente e pendura ali o que foi indicado. É o mesmo desenho do V1, com o\n-- vocabulário do V2: a indicação nasce no atendimento e vive no funil.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_evaluation_complete(p_booking uuid, p_lines jsonb DEFAULT '[]'::jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_deal uuid; v_line jsonb; v_interest uuid;\n v_n int := 0; v_resolvidas int := 0; v_cliente text;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.unit_id, ap.party_id, ap.assignee_id, ap.status\n INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n v_person := public.current_person_id();\n IF v_person IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional'); END IF;\n IF a.assignee_id IS DISTINCT FROM v_person THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_seu_atendimento');\n END IF;\n IF a.status <> 'in_progress' THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'atendimento_nao_esta_em_curso');\n END IF;\n\n IF jsonb_typeof(p_lines) = 'array' AND jsonb_array_length(p_lines) > 0 THEN\n SELECT pe.name INTO v_cliente FROM public.people pe WHERE pe.id = a.party_id;\n\n -- O negócio do cliente. Reaproveita o que estiver aberto: duas avaliações\n -- no mesmo mês são a mesma conversa, e abrir um funil por atendimento faria\n -- o quadro do CRM virar histórico de agenda.\n SELECT d.id INTO v_deal FROM public.plg_crm_deals d\n WHERE d.tenant_id = a.tenant_id AND d.contact_id = a.party_id AND d.status = 'open'\n ORDER BY d.created_at DESC LIMIT 1;\n\n IF v_deal IS NULL THEN\n INSERT INTO public.plg_crm_deals (tenant_id, unit_id, title, contact_id, assigned_to_id, metadata)\n VALUES (a.tenant_id, a.unit_id,\n 'Avaliação — ' || coalesce(v_cliente, 'cliente'), a.party_id, a.assignee_id,\n jsonb_build_object('source', 'clinical:avaliacao', 'booking_id', p_booking))\n RETURNING id INTO v_deal;\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(p_lines)\n LOOP\n -- Idempotente por (negócio, produto): indicar duas vezes o mesmo\n -- procedimento é corrigir a quantidade, não criar uma segunda indicação.\n SELECT i.id INTO v_interest FROM public.plg_crm_deal_interests i\n WHERE i.deal_id = v_deal AND i.product_id = (v_line ->> 'service_id')::uuid;\n\n IF v_interest IS NULL THEN\n INSERT INTO public.plg_crm_deal_interests\n (tenant_id, unit_id, deal_id, product_id, quantity, origin, status, notes, metadata)\n VALUES (a.tenant_id, a.unit_id, v_deal, (v_line ->> 'service_id')::uuid,\n greatest(coalesce((v_line ->> 'quantity')::numeric, 1), 1),\n 'indication', 'open', nullif(v_line ->> 'notes', ''),\n jsonb_build_object('source', 'clinical:avaliacao', 'booking_id', p_booking))\n RETURNING id INTO v_interest;\n v_n := v_n + 1;\n ELSE\n UPDATE public.plg_crm_deal_interests\n SET quantity = greatest(coalesce((v_line ->> 'quantity')::numeric, 1), 1), updated_at = now()\n WHERE id = v_interest;\n END IF;\n\n INSERT INTO public.plg_crm_deal_interest_appointments (tenant_id, unit_id, appointment_id, interest_id)\n SELECT a.tenant_id, a.unit_id, p_booking, v_interest\n WHERE NOT EXISTS (SELECT 1 FROM public.plg_crm_deal_interest_appointments x\n WHERE x.appointment_id = p_booking AND x.interest_id = v_interest);\n END LOOP;\n END IF;\n\n -- A bola da avaliação fecha apontando a SESSÃO em que ela aconteceu. O V1\n -- resolve pelo vínculo com a execução da avaliação; aqui a avaliação é este\n -- atendimento, e o que ela deixa registrado é o que ele indicou.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = r.session_id,\n resolved_via = 'attendance', resolved_at = now(), updated_at = now()\n WHERE r.booking_id = p_booking AND r.kind = 'evaluation' AND r.status = 'pending'\n AND r.frozen_at IS NULL AND r.session_id IS NOT NULL;\n GET DIAGNOSTICS v_resolvidas = ROW_COUNT;\n\n RETURN jsonb_build_object('ok', true, 'deal_id', v_deal, 'indicacoes', v_n, 'bolas_fechadas', v_resolvidas);\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_evaluation_complete(uuid, jsonb) TO authenticated;\n`\n\nexport const MIGRATION_011_INTERESSE_E_INDICACAO_NA_MESMA_LINHA = `-- ============================================================================\n-- INTERESSE e INDICAÇÃO na MESMA LINHA\n--\n-- O desfecho do desenho do V1, e a captura que o documenta mostra a linha\n-- inteira:\n--\n-- Ácido Hialurônico — R$ 999,00\n-- Interesse do cliente · Indicado pelo profissional\n--\n-- São três palavras e três momentos: **interesse** é o que o cliente pediu ao\n-- marcar; **indicação** é o que o profissional recomendou na avaliação;\n-- **contratação** é o que virou cobrança. O mesmo serviço pode carregar as duas\n-- primeiras — e aí ele aparece UMA vez, com as duas origens somadas no rótulo.\n--\n-- Duas linhas para o mesmo procedimento fariam a recepção cobrar duas vezes; e\n-- esconder a origem faria o paciente ouvir \"o senhor pediu\" sobre algo que foi\n-- recomendado. Por isso a fusão é do BANCO, e não de cada tela que precisar\n-- dela.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_contract_candidates(p_booking uuid)\nRETURNS TABLE (\n service_id uuid,\n name text,\n price numeric,\n quantity numeric,\n from_interest boolean,\n from_indication boolean,\n already_contracted boolean\n) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\n WITH alvo AS (\n SELECT ap.id, ap.tenant_id, ap.party_id, ap.order_id FROM public.appointments ap WHERE ap.id = p_booking\n ),\n -- O que o cliente pediu ao marcar.\n interesse AS (\n SELECT ai.service_id, max(ai.name) AS name, max(ai.price) AS price, count(*)::numeric AS qtd\n FROM public.appointment_items ai, alvo\n WHERE ai.booking_id = alvo.id AND ai.service_id IS NOT NULL\n GROUP BY ai.service_id\n ),\n -- O que o profissional recomendou — deste atendimento ou do negócio aberto\n -- do cliente, que é onde a avaliação pendura o que indicou.\n indicacao AS (\n SELECT i.product_id AS service_id, max(i.quantity) AS qtd\n FROM public.plg_crm_deal_interests i\n JOIN public.plg_crm_deals d ON d.id = i.deal_id, alvo\n WHERE i.tenant_id = alvo.tenant_id AND i.status = 'open'\n AND (d.contact_id = alvo.party_id\n OR EXISTS (SELECT 1 FROM public.plg_crm_deal_interest_appointments x\n WHERE x.interest_id = i.id AND x.appointment_id = alvo.id))\n GROUP BY i.product_id\n ),\n -- O que já virou cobrança nesta visita: aparece marcado, não some — sumir\n -- faria a lista mentir sobre o que foi combinado.\n contratado AS (\n SELECT it.product_id AS service_id FROM public.items it, alvo\n WHERE alvo.order_id IS NOT NULL AND it.order_id = alvo.order_id AND it.product_id IS NOT NULL\n )\n SELECT\n coalesce(i.service_id, n.service_id) AS service_id,\n coalesce(i.name, p.name) AS name,\n coalesce(nullif(i.price, 0), p.price, 0) AS price,\n greatest(coalesce(i.qtd, 0), coalesce(n.qtd, 0), 1) AS quantity,\n i.service_id IS NOT NULL AS from_interest,\n n.service_id IS NOT NULL AS from_indication,\n EXISTS (SELECT 1 FROM contratado c WHERE c.service_id = coalesce(i.service_id, n.service_id)) AS already_contracted\n FROM interesse i\n FULL OUTER JOIN indicacao n ON n.service_id = i.service_id\n LEFT JOIN public.products p ON p.id = coalesce(i.service_id, n.service_id)\n ORDER BY 2;\n$fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_contract_candidates(uuid) TO authenticated;\n`\n\nexport const MIGRATION_012_O_QUE_FOI_USADO_ESPERA_APROVACAO = `-- ============================================================================\n-- O QUE FOI USADO espera aprovação\n--\n-- A bola de produtos do V1, e a frase que a define: *\"Ele vem tudo pra cá\n-- primeiro. Vem pra aprovação de dispensa.\"* **Nada baixa aqui.** O consumo\n-- declarado pelo profissional cai na fila da Auditoria de Uso, e é lá que\n-- alguém confirma — ou corrige a quantidade lançada errada.\n--\n-- Registrar consumo baixando estoque na hora seria transformar a memória de\n-- quem atende em movimento contábil sem conferência, que é exatamente o que a\n-- auditoria existe para evitar.\n--\n-- \\`is_audited = false\\` é o estado inicial, e é o que põe a linha na fila.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_products_record(p_booking uuid, p_lines jsonb DEFAULT '[]'::jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_line jsonb; v_session uuid; v_n int := 0; v_marcadas int := 0;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.unit_id, ap.assignee_id, ap.status\n INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n v_person := public.current_person_id();\n IF v_person IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional'); END IF;\n IF a.assignee_id IS DISTINCT FROM v_person THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_seu_atendimento');\n END IF;\n IF a.status <> 'in_progress' THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'atendimento_nao_esta_em_curso');\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_lines, '[]'::jsonb))\n LOOP\n -- A sessão do SERVIÇO que consumiu. O V1 desenha um bloco por serviço\n -- justamente porque a mesma seringa gasta em procedimentos diferentes, e\n -- juntar tudo num monte perde de qual execução veio.\n SELECT cs.id INTO v_session FROM public.plg_clinical_sessions cs\n WHERE cs.booking_id = p_booking AND cs.service_id = (v_line ->> 'service_id')::uuid\n ORDER BY cs.created_at LIMIT 1;\n IF v_session IS NULL THEN CONTINUE; END IF;\n\n INSERT INTO public.plg_clinical_session_products\n (tenant_id, unit_id, session_id, product_id, assignee_id, used_quantity, used_unit_id, executed_on, is_audited)\n SELECT a.tenant_id, a.unit_id, v_session, (v_line ->> 'product_id')::uuid, v_person,\n greatest(coalesce((v_line ->> 'quantity')::numeric, 1), 0),\n nullif(v_line ->> 'unit_id', '')::uuid, current_date, false\n WHERE NOT EXISTS (\n SELECT 1 FROM public.plg_clinical_session_products sp\n WHERE sp.session_id = v_session AND sp.product_id = (v_line ->> 'product_id')::uuid);\n IF FOUND THEN\n v_n := v_n + 1;\n ELSE\n UPDATE public.plg_clinical_session_products\n SET used_quantity = greatest(coalesce((v_line ->> 'quantity')::numeric, 1), 0), updated_at = now()\n WHERE session_id = v_session AND product_id = (v_line ->> 'product_id')::uuid AND NOT is_audited;\n END IF;\n END LOOP;\n\n -- Conferido é conferido, mesmo que não tenha sido usado nada: \"não usei\n -- produto neste procedimento\" é uma resposta, e sem poder dá-la a bola ficaria\n -- pendente para sempre em quem não gasta insumo.\n UPDATE public.plg_clinical_sessions\n SET products_initialized = true, updated_at = now()\n WHERE booking_id = p_booking AND coalesce(products_initialized, false) = false;\n GET DIAGNOSTICS v_marcadas = ROW_COUNT;\n\n RETURN jsonb_build_object(\n 'ok', true, 'lancados', v_n, 'sessoes_conferidas', v_marcadas,\n 'regua', public.clinical_requirements_refresh(p_booking));\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_products_record(uuid, jsonb) TO authenticated;\n`\n\nexport const MIGRATION_013_O_QUE_COUBER_HOJE = `-- ============================================================================\n-- O QUE COUBER HOJE\n--\n-- *\"Marque os serviços que serão executados agora. Os demais ficam contratados\n-- para execução em outro atendimento.\"* É o diálogo que o V1 abre depois da\n-- contratação, e ele existe por causa de uma frase da demonstração: *\"o labial\n-- veio com uma festa amanhã, não quero ficar com o lábio roxo\"*.\n--\n-- É este passo que separa **contratado** de **executado** — e é dele que nasce\n-- o saldo *a executar* na conta do cliente. Sem ele, contratar três sessões\n-- significaria executar três sessões no mesmo dia, o que não é como uma clínica\n-- funciona.\n--\n-- O que NÃO se executa hoje não vira pendência do atendimento: fica contratado,\n-- pago ou a pagar, esperando outra data.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_execute_services(p_booking uuid, p_service_ids uuid[])\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_executadas int := 0; v_adiadas int := 0;\nBEGIN\n SELECT ap.id, ap.assignee_id, ap.status INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n v_person := public.current_person_id();\n IF v_person IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional'); END IF;\n IF a.assignee_id IS DISTINCT FROM v_person THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_seu_atendimento');\n END IF;\n IF a.status <> 'in_progress' THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'atendimento_nao_esta_em_curso');\n END IF;\n\n UPDATE public.plg_clinical_sessions cs\n SET status = 'executed', ended_at = coalesce(cs.ended_at, now()), updated_at = now()\n WHERE cs.booking_id = p_booking\n AND cs.service_id = ANY (coalesce(p_service_ids, ARRAY[]::uuid[]))\n AND cs.status <> 'executed';\n GET DIAGNOSTICS v_executadas = ROW_COUNT;\n\n -- O que ficou de fora volta a CONTRATADO — e não a pendente: foi combinado e\n -- continua combinado, só que para outra data. Desmarcar depois de ter\n -- executado por engano é o caso que esta linha resolve.\n UPDATE public.plg_clinical_sessions cs\n SET status = 'contracted', ended_at = NULL, updated_at = now()\n WHERE cs.booking_id = p_booking\n AND NOT (cs.service_id = ANY (coalesce(p_service_ids, ARRAY[]::uuid[])))\n AND cs.status = 'executed';\n GET DIAGNOSTICS v_adiadas = ROW_COUNT;\n\n RETURN jsonb_build_object('ok', true, 'executadas', v_executadas, 'adiadas', v_adiadas);\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_execute_services(uuid, uuid[]) TO authenticated;\n`\n\nexport const MIGRATION_014_O_PAGO_A_EXECUTAR_DEIXA_DE_SER_INVISIVEL = `-- ============================================================================\n-- O \"PAGO A EXECUTAR\" deixa de ser invisível\n--\n-- O tipo \\`ClientAccount\\` do financeiro traz este comentário, escrito quando o\n-- porte começou:\n--\n-- \"\\`paidToExecute\\` (o 'pago a executar' do V1) é o valor já recebido de\n-- serviço ainda não prestado: num salão isso é PASSIVO, não receita. Fica\n-- opcional porque o V2 ainda não tem o eixo de EXECUÇÃO — modela pagamento e\n-- não modela entrega —, e um zero fixo ali mentiria sobre uma dívida.\"\n--\n-- O eixo passou a existir: \\`plg_clinical_sessions.status\\` separa **contratado**\n-- de **executado**, e a etapa \"o que couber hoje\" é quem move uma para a outra.\n-- Então a conta pode responder — e a regra de honestidade se inverte: agora\n-- OMITIR o número esconderia a dívida.\n--\n-- A vista é do plugin clínico porque o dado é dele. O financeiro lê se ela\n-- existir; onde o prontuário não está instalado, a conta continua sem o cartão,\n-- que é a resposta certa para quem não tem execução para acompanhar.\n-- ============================================================================\n\nCREATE OR REPLACE VIEW public.v_clinical_to_execute AS\nWITH sessao AS (\n SELECT\n cs.tenant_id,\n cs.party_id,\n cs.order_item_id,\n cs.status,\n i.order_id,\n coalesce(i.total, 0) AS item_total\n FROM public.plg_clinical_sessions cs\n JOIN public.items i ON i.id = cs.order_item_id\n WHERE cs.party_id IS NOT NULL\n AND cs.status IN ('contracted', 'quoted', 'in_service', 'scheduled', 'pending')\n),\n-- Quanto do PEDIDO já foi recebido, em proporção: o pagamento é por parcela e\n-- não por item, então a fatia paga de cada item é a fatia paga do pedido.\npago AS (\n SELECT m.invoice_id AS order_id,\n sum(coalesce(m.paid_amount, 0)) AS pago,\n nullif(sum(coalesce(m.amount, 0)), 0) AS total\n FROM public.plg_financial_movements m\n WHERE m.direction = 'credit'\n GROUP BY m.invoice_id\n)\nSELECT\n s.tenant_id,\n s.party_id,\n sum(s.item_total * coalesce(p.pago / p.total, 0))::numeric(14,2) AS paid_to_execute,\n sum(s.item_total * (1 - coalesce(p.pago / p.total, 0)))::numeric(14,2) AS open_to_execute,\n count(*)::int AS sessions_to_execute\nFROM sessao s\nLEFT JOIN pago p ON p.order_id = s.order_id\nGROUP BY s.tenant_id, s.party_id;\n\nCOMMENT ON VIEW public.v_clinical_to_execute IS\n 'O que o cliente já contratou e ainda NÃO foi executado, separado em pago e em aberto. O \"pago a executar\" do V1 é passivo: dinheiro recebido de serviço que a casa ainda deve prestar.';\n\nGRANT SELECT ON public.v_clinical_to_execute TO authenticated;\n`\n\nexport const MIGRATION_015_A_REVISAO_APONTA_O_QUE_REVISA = `-- ============================================================================\n-- A REVISÃO aponta O QUE ela revisa\n--\n-- Revisão é o retorno de um procedimento que já foi feito — e ela só significa\n-- alguma coisa se disser DE QUAL. Sem o vínculo, o histórico do paciente fica\n-- com um \"Revisão\" solto, e ninguém consegue responder, meses depois, o que foi\n-- revisado nem se ainda estava no prazo.\n--\n-- Por isso a exigência \\`revision_link\\` do V1 é a única que trava as DUAS pontas:\n-- \\`blocks_start\\` e \\`blocks_finish\\`. Não se começa uma revisão sem saber do quê,\n-- e não se encerra sem ter dito.\n--\n-- O modal do V1 mudou em 21/09 (\\`2e1a119ef\\`): passou a listar **todo serviço\n-- executado**, com o **prazo à vista**, em vez de só os candidatos filtrados —\n-- porque quem atende sabe reconhecer o procedimento na lista, e um filtro\n-- silencioso esconde justamente o caso fora do padrão.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_revision_candidates(p_booking uuid)\nRETURNS TABLE (\n session_id uuid,\n service_id uuid,\n service_name text,\n executed_on timestamptz,\n deadline_days integer,\n days_since integer,\n within_deadline boolean\n) LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\n WITH alvo AS (\n SELECT ap.tenant_id, ap.party_id FROM public.appointments ap WHERE ap.id = p_booking\n )\n SELECT\n cs.id,\n cs.service_id,\n p.name,\n coalesce(cs.ended_at, cs.scheduled_for) AS executed_on,\n rev.deadline_days,\n (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date)::int AS days_since,\n -- Prazo ausente não vira \"fora do prazo\": vira SEM prazo, e a tela diz isso.\n CASE WHEN rev.deadline_days IS NULL THEN NULL\n ELSE (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date) <= rev.deadline_days\n END AS within_deadline\n FROM public.plg_clinical_sessions cs\n JOIN alvo a ON a.tenant_id = cs.tenant_id AND a.party_id = cs.party_id\n LEFT JOIN public.products p ON p.id = cs.service_id\n LEFT JOIN LATERAL (\n SELECT r.deadline_days FROM public.plg_clinical_service_revisions r\n WHERE r.tenant_id = cs.tenant_id AND r.service_id = cs.service_id\n ORDER BY r.revision_number LIMIT 1\n ) rev ON true\n WHERE cs.status = 'executed'\n AND cs.booking_id IS DISTINCT FROM p_booking\n ORDER BY coalesce(cs.ended_at, cs.scheduled_for) DESC NULLS LAST\n LIMIT 50;\n$fn$;\n\nCREATE OR REPLACE FUNCTION public.clinical_link_revision(p_session uuid, p_parent_session uuid)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n s record; v_person uuid; v_booking uuid;\nBEGIN\n SELECT cs.id, cs.booking_id, cs.tenant_id, cs.party_id INTO s\n FROM public.plg_clinical_sessions cs WHERE cs.id = p_session;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n v_person := public.current_person_id();\n IF v_person IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional'); END IF;\n\n -- O original tem de ser do MESMO paciente. Revisar o procedimento de outra\n -- pessoa não é engano de digitação: é prontuário trocado.\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_clinical_sessions p\n WHERE p.id = p_parent_session AND p.party_id = s.party_id AND p.tenant_id = s.tenant_id\n ) THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'original_de_outro_paciente');\n END IF;\n\n UPDATE public.plg_clinical_sessions\n SET parent_session_id = p_parent_session, updated_at = now()\n WHERE id = p_session;\n\n v_booking := s.booking_id;\n RETURN jsonb_build_object('ok', true, 'regua', public.clinical_requirements_refresh(v_booking));\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_revision_candidates(uuid) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.clinical_link_revision(uuid, uuid) TO authenticated;\n`\n\nexport const MIGRATION_016_A_COMPRA_ANTECIPADA_NAO_TEM_AGENDAMENTO_ATRAS = `-- ============================================================================\n-- A COMPRA ANTECIPADA não tem agendamento atrás\n--\n-- O caso que a demonstração do V1 narra: *\"Tá na promoção, vou comprar esse\n-- Botox e deixar guardado.\"* A recepção abre **Contas a Receber → Inserir**,\n-- escolhe o serviço, aplica o desconto combinado e recebe — **sem agendamento,\n-- sem contrato e sem proposta**. O V1 é explícito: *\"Não precisa de contrato?\n-- Não. Não precisa de proposta? Não.\"*\n--\n-- O efeito aparece na CONTA DA CLIENTE: um item **a executar**, pago, esperando\n-- o dia. É por isso que ele é passivo — a casa recebeu e ainda deve prestar —,\n-- e é por isso que ele precisa nascer como SESSÃO: sem sessão, \"a executar\" não\n-- existe como estado, e o dinheiro entraria como receita de um serviço que\n-- ninguém fez.\n--\n-- Sessão sem \\`booking_id\\` é o que distingue esta compra de um atendimento: ela\n-- ainda não tem dia. Ganha um quando for agendada.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_prepay_services(p_party uuid, p_lines jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n v_tenant uuid; v_unit uuid; v_order uuid; v_line jsonb;\n v_rpc_lines jsonb := '[]'::jsonb; v_idx int := 0; v_sessoes int := 0; v_total numeric;\nBEGIN\n IF auth.uid() IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_sessao'); END IF;\n v_tenant := app.current_tenant_id();\n IF v_tenant IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_tenant'); END IF;\n IF NOT EXISTS (SELECT 1 FROM public.people p WHERE p.id = p_party AND p.tenant_id = v_tenant) THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'cliente_nao_encontrado');\n END IF;\n IF jsonb_typeof(p_lines) <> 'array' OR jsonb_array_length(p_lines) = 0 THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nada_escolhido');\n END IF;\n\n v_unit := app.requested_unit();\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(p_lines)\n LOOP\n v_rpc_lines := v_rpc_lines || jsonb_build_object(\n 'product_id', v_line ->> 'service_id',\n 'name', v_line ->> 'name',\n 'quantity', greatest(coalesce((v_line ->> 'quantity')::numeric, 1), 1),\n 'unit_price', greatest(coalesce((v_line ->> 'unit_price')::numeric, 0), 0),\n 'sort_order', v_idx);\n v_idx := v_idx + 1;\n END LOOP;\n\n v_order := public.order_create(\n p_kind => 'sale',\n p_lines => v_rpc_lines,\n p_party_id => p_party,\n p_unit_id => v_unit,\n -- Nasce CONFIRMADO e fecha logo abaixo, em dois tempos — e isso não é\n -- cerimônia: o gatilho que levanta o recebível é \\`AFTER UPDATE OF status\\`,\n -- então um pedido criado já \\`completed\\` **vende sem cobrar**. Medido: a\n -- primeira versão desta função criou a compra fechada e a conta ficou com\n -- \"a executar\" e ZERO cobrança.\n -- (\\`scheduled\\` nem é aceito para \\`sale\\`: o contrato de estados do pedido\n -- permite draft, confirmed, completed e cancelled.)\n p_status => 'confirmed',\n p_metadata => jsonb_build_object('source', 'clinical:compra-antecipada'),\n p_idempotency_key => NULL);\n\n -- Uma sessão por item, SEM dia. É ela que sustenta o \"a executar\" na conta —\n -- e o que permite, quando o dia chegar, amarrar a execução ao que já foi pago.\n INSERT INTO public.plg_clinical_sessions\n (tenant_id, unit_id, status, party_id, service_id, order_item_id)\n SELECT v_tenant, v_unit, 'contracted', p_party, i.product_id, i.id\n FROM public.items i\n WHERE i.order_id = v_order AND i.product_id IS NOT NULL;\n GET DIAGNOSTICS v_sessoes = ROW_COUNT;\n\n -- O fechamento, agora como UPDATE: é ele que dispara\n -- \\`orders_raise_receivable_on_close\\` e faz nascer a conta a receber.\n UPDATE public.orders SET status = 'completed', updated_at = now() WHERE id = v_order;\n\n SELECT coalesce(sum(i.total), 0) INTO v_total FROM public.items i WHERE i.order_id = v_order;\n\n RETURN jsonb_build_object('ok', true, 'order_id', v_order, 'sessoes', v_sessoes, 'total', v_total);\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_prepay_services(uuid, jsonb) TO authenticated;\n`\n\nexport const MIGRATION_017_O_QUE_JA_FOI_PAGO_NAO_SE_COBRA_DE_NOVO = `-- ============================================================================\n-- O QUE JÁ FOI PAGO não se cobra de novo\n--\n-- A compra antecipada deixa uma sessão **sem dia**: o Botox comprado na\n-- promoção, esperando. Quando o dia chega, o atendimento começava criando uma\n-- sessão NOVA para o mesmo serviço — e aí a cliente tinha duas: a que pagou e a\n-- que acabou de ser aberta. A primeira ficava \"a executar\" para sempre, e a\n-- segunda entrava na contratação como se nada tivesse sido pago.\n--\n-- É a mesma pessoa, o mesmo serviço e o mesmo dinheiro. Então o início do\n-- atendimento ADOTA a sessão que já existe: ela ganha o dia, o profissional e o\n-- item do agendamento, e deixa de pender na conta.\n--\n-- A escolha é a mais ANTIGA: quem comprou duas vezes executa primeiro o que\n-- comprou primeiro — e é isso que faz o saldo andar em vez de envelhecer.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_attendance_start(p_booking uuid)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_created int := 0; v_adotadas int := 0;\n v_item record; v_refresh jsonb; v_antiga uuid;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.unit_id, ap.party_id, ap.assignee_id, ap.status, ap.starts_at\n INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'not_found');\n END IF;\n\n v_person := public.current_person_id();\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional');\n END IF;\n IF a.assignee_id IS DISTINCT FROM v_person THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_seu_atendimento');\n END IF;\n IF a.starts_at::date <> (now() AT TIME ZONE 'America/Sao_Paulo')::date THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_de_hoje');\n END IF;\n IF a.status IN ('completed', 'cancelled', 'no_show') THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'ja_encerrado', 'status', a.status);\n END IF;\n\n FOR v_item IN\n SELECT ai.id, ai.service_id, ai.assignee_id\n FROM public.appointment_items ai\n WHERE ai.booking_id = p_booking\n LOOP\n -- Já existe sessão para este item? (dois toques, duas pessoas ao mesmo tempo)\n CONTINUE WHEN EXISTS (\n SELECT 1 FROM public.plg_clinical_sessions cs\n WHERE cs.booking_id = p_booking AND cs.booking_item_id = v_item.id);\n\n -- O QUE FOI COMPRADO ANTES E AINDA NÃO SAIU: adota em vez de criar.\n SELECT cs.id INTO v_antiga\n FROM public.plg_clinical_sessions cs\n WHERE cs.tenant_id = a.tenant_id\n AND cs.party_id = a.party_id\n AND cs.service_id = v_item.service_id\n AND cs.booking_id IS NULL\n AND cs.status IN ('contracted', 'quoted', 'pending')\n ORDER BY cs.created_at\n LIMIT 1;\n\n IF v_antiga IS NOT NULL THEN\n UPDATE public.plg_clinical_sessions\n SET booking_id = p_booking,\n booking_item_id = v_item.id,\n unit_id = coalesce(a.unit_id, unit_id),\n assignee_id = coalesce(v_item.assignee_id, a.assignee_id),\n performed_by_id = v_person,\n status = 'in_service',\n scheduled_for = a.starts_at,\n started_at = coalesce(started_at, now()),\n updated_at = now()\n WHERE id = v_antiga;\n v_adotadas := v_adotadas + 1;\n ELSE\n INSERT INTO public.plg_clinical_sessions (\n tenant_id, unit_id, status, booking_id, booking_item_id, party_id,\n assignee_id, performed_by_id, service_id, scheduled_for, started_at)\n VALUES (a.tenant_id, a.unit_id, 'in_service', a.id, v_item.id, a.party_id,\n coalesce(v_item.assignee_id, a.assignee_id), v_person, v_item.service_id,\n a.starts_at, now());\n v_created := v_created + 1;\n END IF;\n END LOOP;\n\n UPDATE public.appointments\n SET status = 'in_progress', updated_at = now()\n WHERE id = p_booking AND status <> 'in_progress';\n\n v_refresh := public.clinical_requirements_refresh(p_booking);\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sessoes_criadas', v_created,\n 'sessoes_adotadas', v_adotadas,\n 'sessoes', (SELECT count(*) FROM public.plg_clinical_sessions WHERE booking_id = p_booking),\n 'regua', v_refresh);\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_attendance_start(uuid) TO authenticated;\n`\n\nexport const MIGRATION_018_O_QUE_FOI_CONTRATADO_VIRA_SESSAO = `-- O que foi contratado vira SESSÃO, mesmo sem agendamento atrás dele.\n--\n-- A 006 só LIGAVA sessões já existentes às linhas do pedido. Serviço indicado\n-- na avaliação e contratado na hora não tem sessão nenhuma para ligar: a conta\n-- ficava com \"Botox Full Face · 2 · R$ 2.798,00\" e o prontuário não sabia que\n-- havia dois Botox a executar. O dinheiro entrava e a execução sumia — nem na\n-- régua, nem em \\`v_clinical_to_execute\\`, nem no diálogo de \"Executar\".\n--\n-- Uma sessão POR UNIDADE, porque é assim que a execução acontece: dois Botox\n-- contratados são duas idas à cadeira, e executar um hoje não pode fechar o\n-- outro. O \\`booking_id\\` é o do atendimento onde foi contratado — quem não for\n-- executado hoje continua \\`contracted\\`, que é o estado de \"a executar\".\n\nCREATE OR REPLACE FUNCTION public.clinical_sessions_for_order(p_booking uuid, p_order uuid)\nRETURNS integer\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $fn$\nDECLARE\n a public.appointments%ROWTYPE;\n v_item record;\n v_faltam integer;\n v_k integer;\n v_total integer := 0;\nBEGIN\n SELECT * INTO a FROM public.appointments WHERE id = p_booking;\n IF NOT FOUND THEN RETURN 0; END IF;\n\n FOR v_item IN\n SELECT i.id, i.product_id, i.quantity,\n (SELECT count(*) FROM public.plg_clinical_sessions s WHERE s.order_item_id = i.id) AS ja\n FROM public.items i\n WHERE i.order_id = p_order\n AND i.product_id IS NOT NULL\n LOOP\n -- Serviço, não produto: o que se executa numa cadeira.\n CONTINUE WHEN NOT EXISTS (\n SELECT 1 FROM public.products p\n WHERE p.id = v_item.product_id AND p.kind = 'service');\n\n v_faltam := greatest(0, ceil(coalesce(v_item.quantity, 1))::integer - v_item.ja::integer);\n FOR v_k IN 1 .. v_faltam LOOP\n INSERT INTO public.plg_clinical_sessions\n (tenant_id, unit_id, status, booking_id, party_id, assignee_id, service_id, order_item_id, metadata)\n VALUES\n (a.tenant_id, a.unit_id, 'contracted', p_booking, a.party_id, a.assignee_id,\n v_item.product_id, v_item.id,\n jsonb_build_object('source', 'clinical_contract_services'));\n v_total := v_total + 1;\n END LOOP;\n END LOOP;\n\n RETURN v_total;\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_sessions_for_order(uuid, uuid) TO authenticated;\n\n\nCREATE OR REPLACE FUNCTION public.clinical_contract_services(p_booking uuid, p_lines jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_order uuid; v_line jsonb; v_item uuid; v_n int := 0;\n v_rpc_lines jsonb := '[]'::jsonb; v_idx int := 0;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.unit_id, ap.party_id, ap.assignee_id, ap.status, ap.order_id\n INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n v_person := public.current_person_id();\n IF v_person IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional'); END IF;\n IF a.assignee_id IS DISTINCT FROM v_person THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_seu_atendimento');\n END IF;\n IF a.status <> 'in_progress' THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'atendimento_nao_esta_em_curso');\n END IF;\n IF jsonb_typeof(p_lines) <> 'array' OR jsonb_array_length(p_lines) = 0 THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nada_marcado');\n END IF;\n\n -- As linhas, no formato que \\`order_create\\` já entende.\n FOR v_line IN SELECT * FROM jsonb_array_elements(p_lines)\n LOOP\n v_rpc_lines := v_rpc_lines || jsonb_build_object(\n 'product_id', v_line ->> 'service_id',\n 'name', v_line ->> 'name',\n 'quantity', coalesce((v_line ->> 'quantity')::numeric, 1),\n 'unit_price', coalesce((v_line ->> 'unit_price')::numeric, 0),\n 'sort_order', v_idx);\n v_idx := v_idx + 1;\n END LOOP;\n\n -- O pedido do atendimento. \\`idempotency_key\\` pelo agendamento: dois toques\n -- no \"Contratar\" não abrem duas contas para a mesma visita.\n IF a.order_id IS NULL THEN\n v_order := public.order_create(\n p_kind => 'appointment',\n p_lines => v_rpc_lines,\n p_party_id => a.party_id,\n p_unit_id => a.unit_id,\n p_status => 'scheduled',\n p_assignee_id => a.assignee_id,\n p_metadata => jsonb_build_object('agenda', jsonb_build_object('booking_id', p_booking), 'source', 'clinical:contratacao'),\n p_idempotency_key => 'clinical-contract:' || p_booking::text);\n UPDATE public.appointments SET order_id = v_order, updated_at = now() WHERE id = p_booking;\n ELSE\n -- Pedido já existe (a agenda abriu na marcação): acrescenta o que falta.\n v_order := a.order_id;\n v_idx := coalesce((SELECT max(i.sort_order) + 1 FROM public.items i WHERE i.order_id = v_order), 0);\n FOR v_line IN SELECT * FROM jsonb_array_elements(p_lines)\n LOOP\n INSERT INTO public.items (tenant_id, unit_id, order_id, product_id, name, quantity, unit_price, total, sort_order)\n VALUES (a.tenant_id, a.unit_id, v_order, (v_line ->> 'service_id')::uuid, v_line ->> 'name',\n coalesce((v_line ->> 'quantity')::numeric, 1),\n coalesce((v_line ->> 'unit_price')::numeric, 0),\n coalesce((v_line ->> 'quantity')::numeric, 1) * coalesce((v_line ->> 'unit_price')::numeric, 0),\n v_idx);\n v_idx := v_idx + 1;\n END LOOP;\n END IF;\n\n -- Cada sessão passa a apontar a SUA linha. Sem isto o pedido cobra e o\n -- atendimento não sabe o que executou — e a bola \"Conta do atendimento\"\n -- nunca fecha, porque é exatamente este vínculo que ela procura.\n UPDATE public.plg_clinical_sessions cs\n SET order_item_id = i.id,\n status = CASE WHEN cs.status IN ('quoted', 'pending', 'scheduled') THEN 'contracted' ELSE cs.status END,\n updated_at = now()\n FROM public.items i\n WHERE cs.booking_id = p_booking\n AND cs.order_item_id IS NULL\n AND i.order_id = v_order\n AND i.product_id = cs.service_id\n AND NOT EXISTS (SELECT 1 FROM public.plg_clinical_sessions o\n WHERE o.order_item_id = i.id AND o.id <> cs.id);\n GET DIAGNOSTICS v_n = ROW_COUNT;\n\n -- O que foi contratado e não tinha sessão ganha a sua (018).\n PERFORM public.clinical_sessions_for_order(p_booking, v_order);\n\n RETURN jsonb_build_object(\n 'ok', true, 'order_id', v_order, 'sessoes_ligadas', v_n,\n 'total', (SELECT coalesce(sum(i.total), 0) FROM public.items i WHERE i.order_id = v_order),\n 'regua', public.clinical_requirements_refresh(p_booking));\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_contract_services(uuid, jsonb) TO authenticated;\n`\n\nexport const MIGRATION_019_ADIAR_E_UMA_ESCOLHA = `-- Adiar é uma escolha, e encerrar não pode desfazê-la.\n--\n-- O \"Executar neste atendimento\" existe para dizer o que sai HOJE; o resto\n-- fica contratado para outro dia. Mas o \\`Finalizar\\` marcava como executada\n-- TODA sessão do agendamento — inclusive a que acabara de ser adiada. O\n-- serviço sumia do \"a executar\" da conta e a cliente ficava com um\n-- procedimento pago que ninguém fez, e que o sistema jurava ter feito.\n--\n-- O aviso de encerramento tinha o mesmo vício: cobrava por serviços que a\n-- pessoa tinha acabado de dizer que ficariam para depois.\n--\n-- Agora as duas coisas olham para \\`in_service\\` — o que entrou na sala. O que\n-- está em \\`contracted\\` é trabalho futuro e atravessa o encerramento intacto.\n\nCREATE OR REPLACE FUNCTION public.clinical_attendance_finish(p_booking uuid, p_force boolean DEFAULT false)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_bloqueia jsonb; v_nao_executados jsonb;\n v_avisados int := 0; v_total numeric := 0; v_cliente text;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.unit_id, ap.party_id, ap.assignee_id, ap.status, ap.order_id\n INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n v_person := public.current_person_id();\n IF v_person IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional'); END IF;\n IF a.assignee_id IS DISTINCT FROM v_person THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_seu_atendimento');\n END IF;\n IF a.status <> 'in_progress' THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'atendimento_nao_esta_em_curso');\n END IF;\n\n -- A trava de verdade: exigência que impede TERMINAR.\n SELECT coalesce(jsonb_agg(jsonb_build_object('kind', r.kind, 'service_id', r.service_id)), '[]'::jsonb)\n INTO v_bloqueia\n FROM public.plg_clinical_requirements r\n WHERE r.booking_id = p_booking AND r.blocks_finish AND r.status = 'pending';\n IF jsonb_array_length(v_bloqueia) > 0 THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'exigencia_bloqueia', 'itens', v_bloqueia);\n END IF;\n\n -- O aviso que não trava: o que ENTROU NA SALA e não foi confirmado.\n --\n -- \"Fica para outra data\" é uma escolha, não um esquecimento: sessão em\n -- \\`contracted\\` é trabalho futuro, já na conta, e não tem por que ser\n -- cobrada no aviso nem executada à força ao encerrar.\n SELECT coalesce(jsonb_agg(p.name ORDER BY p.name), '[]'::jsonb) INTO v_nao_executados\n FROM public.plg_clinical_sessions cs\n JOIN public.products p ON p.id = cs.service_id\n WHERE cs.booking_id = p_booking AND cs.status = 'in_service';\n IF jsonb_array_length(v_nao_executados) > 0 AND NOT coalesce(p_force, false) THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'servicos_nao_executados', 'itens', v_nao_executados);\n END IF;\n\n -- \"Finalizar agora registra estes serviços como executados\" — texto do V1,\n -- e vale para os que estavam EM EXECUÇÃO. O que foi adiado continua\n -- contratado: marcá-lo executado sumiria com ele do \"a executar\" da conta\n -- sem que a cliente tivesse recebido nada.\n UPDATE public.plg_clinical_sessions\n SET status = 'executed', ended_at = coalesce(ended_at, now()), updated_at = now()\n WHERE booking_id = p_booking AND status = 'in_service';\n\n UPDATE public.appointments\n SET status = 'completed', updated_at = now()\n WHERE id = p_booking;\n\n -- Congela a trilha: depois de encerrado, a régua não se reapura.\n PERFORM public.clinical_requirements_refresh(p_booking, true);\n\n SELECT coalesce(sum(i.total), 0) INTO v_total FROM public.items i WHERE i.order_id = a.order_id;\n SELECT pe.name INTO v_cliente FROM public.people pe WHERE pe.id = a.party_id;\n\n -- O BALCÃO ACENDE.\n --\n -- Quem recebe: a recepção da conta. Onde não houver ninguém com o papel de\n -- recepção — e é o caso do tenant migrado, com 0 membros nele —, cai para\n -- gerente e dono. Um aviso que não chega a ninguém deixaria a conta parada\n -- sem que nada na tela dissesse por quê.\n WITH alvo AS (\n SELECT m.user_id\n FROM app.memberships m\n JOIN app.roles r ON r.id = m.role_id\n WHERE m.tenant_id = a.tenant_id AND coalesce(m.active, true)\n AND r.key IN ('recepcao', 'reception')\n UNION ALL\n SELECT m.user_id\n FROM app.memberships m\n JOIN app.roles r ON r.id = m.role_id\n WHERE m.tenant_id = a.tenant_id AND coalesce(m.active, true)\n AND r.key IN ('owner', 'manager')\n AND NOT EXISTS (\n SELECT 1 FROM app.memberships m2 JOIN app.roles r2 ON r2.id = m2.role_id\n WHERE m2.tenant_id = a.tenant_id AND coalesce(m2.active, true)\n AND r2.key IN ('recepcao', 'reception'))\n )\n INSERT INTO public.notifications (tenant_id, user_id, title, body, type, link, subject_type, subject_id)\n SELECT a.tenant_id, alvo.user_id,\n 'Checkout pendente',\n coalesce(v_cliente, 'Cliente') || ' terminou o atendimento'\n -- O dinheiro escrito como o Brasil escreve. \\`to_char\\` usa o\n -- \\`lc_numeric\\` do BANCO para o grupo e a vírgula, e o cluster está\n -- em C: saía \"R$ 999.00\" no aviso de uma recepção brasileira. A\n -- troca é feita à mão para não depender de locale nenhum.\n || CASE WHEN v_total > 0 THEN ' · R$ ' ||\n replace(replace(replace(to_char(v_total, 'FM999G999G999D00'), ',', '§'), '.', ','), '§', '.')\n ELSE '' END,\n 'checkout_pendente', '#/checkin', 'appointment', p_booking\n FROM alvo\n ON CONFLICT (user_id, subject_type, subject_id, type) WHERE subject_id IS NOT NULL DO NOTHING;\n GET DIAGNOSTICS v_avisados = ROW_COUNT;\n\n RETURN jsonb_build_object(\n 'ok', true, 'avisados', v_avisados, 'total', v_total,\n 'executados', (SELECT count(*) FROM public.plg_clinical_sessions WHERE booking_id = p_booking AND status = 'executed'));\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_attendance_finish(uuid, boolean) TO authenticated;\n`\n\nexport const MIGRATION_020_COBRAR_UM_AGENDAMENTO_SEM_PEDIDO_LEVANTA_O_PEDIDO = `-- ============================================================================\n-- Cobrar um agendamento SEM pedido levanta o pedido primeiro\n--\n-- SINTOMA, visto na tela (23-09, Juliane): a aba Financeiro acende o vermelho\n-- do V1 (\"atendimento de hoje sem cobrança levantada\"), mas a conta não\n-- oferece caminho nenhum — o agendamento veio da MIGRAÇÃO do V1 com\n-- \\`order_id\\` nulo, e a linha do serviço mora em \\`appointment_items\\`, não em\n-- \\`items\\`. O V1 nunca teve esse beco: \"Gerar Recebimento\" lá CRIA a fatura a\n-- partir dos serviços do agendamento.\n--\n-- A tradução para o V2 respeita o caminho único do dinheiro: a cobrança não\n-- escreve fatura — ela LEVANTA O PEDIDO pela porta do núcleo (\\`order_create\\`,\n-- a mesma que \\`agenda_booking_create\\` usa) com as linhas COM PREÇO dos\n-- \\`appointment_items\\`, amarra o pedido ao agendamento e segue o fechamento\n-- normal (\\`orders_raise_receivable_on_close\\`).\n--\n-- Linha sem preço não vira pedido: revisão e avaliação gratuitas continuam\n-- sem cobrança, e a recusa \\`nada_contratado\\` continua falando por elas.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_checkout_charge(p_booking uuid)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_linhas int; v_total numeric; v_mov int; v_lines jsonb; v_order uuid;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.kind, ap.status, ap.order_id,\n ap.party_id, ap.assignee_id, ap.unit_id\n INTO a\n FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n IF auth.uid() IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_sessao'); END IF;\n -- O agendamento de OUTRA conta não existe para esta sessão.\n IF a.tenant_id IS DISTINCT FROM app.current_tenant_id() THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'not_found');\n END IF;\n\n IF a.order_id IS NULL THEN\n -- As linhas com preço do agendamento migrado. Sem nenhuma, não há o que\n -- cobrar — e isso é um estado legítimo (revisão, avaliação gratuita).\n SELECT jsonb_agg(jsonb_build_object(\n 'service_id', ai.service_id,\n 'name', ai.name,\n 'quantity', 1,\n 'unit_price', ai.price,\n 'sort_order', ai.sort_order,\n 'duration_minutes', ai.duration_minutes,\n 'assignee_id', coalesce(ai.assignee_id, a.assignee_id)\n ) ORDER BY ai.sort_order)\n INTO v_lines\n FROM public.appointment_items ai\n WHERE ai.booking_id = a.id AND coalesce(ai.price, 0) > 0;\n\n IF v_lines IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nada_contratado');\n END IF;\n\n v_order := public.order_create(\n p_kind => coalesce(a.kind, 'appointment'),\n p_lines => v_lines,\n p_party_id => a.party_id,\n p_unit_id => a.unit_id,\n p_status => 'scheduled',\n p_assignee_id => a.assignee_id,\n p_metadata => jsonb_build_object('source', 'clinical_checkout_charge', 'booking_id', a.id));\n\n UPDATE public.appointments SET order_id = v_order, updated_at = now() WHERE id = a.id;\n a.order_id := v_order;\n END IF;\n\n SELECT count(*), coalesce(sum(i.total), 0) INTO v_linhas, v_total\n FROM public.items i WHERE i.order_id = a.order_id;\n IF v_linhas = 0 THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nada_contratado');\n END IF;\n\n -- Idempotente: o pedido já fechado devolve o que existe, sem levantar um\n -- segundo recebível para a mesma visita.\n SELECT count(*) INTO v_mov FROM public.plg_financial_movements m WHERE m.invoice_id = a.order_id;\n IF v_mov > 0 THEN\n RETURN jsonb_build_object('ok', true, 'ja_cobrado', true, 'total', v_total);\n END IF;\n\n UPDATE public.orders SET status = 'completed', updated_at = now()\n WHERE id = a.order_id AND status <> 'completed';\n\n SELECT count(*) INTO v_mov FROM public.plg_financial_movements m WHERE m.invoice_id = a.order_id;\n RETURN jsonb_build_object('ok', true, 'total', v_total, 'movimentos', v_mov);\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_checkout_charge(uuid) TO authenticated;\n`\n\nexport const MIGRATION_021_AGENDAMENTO_NASCIDO_NO_V2_TAMBEM_TEM_REGUA = `-- ============================================================================\n-- O AGENDAMENTO NASCIDO NO V2 TAMBÉM TEM RÉGUA\n--\n-- O Iniciar atendimento percorre \\`appointment_items\\` — a linha do agendamento\n-- do V1, onde as 314 mil linhas migradas moram. Só que o Novo Agendamento do\n-- V2 grava as linhas pelo caminho do PEDIDO (\\`agenda_booking_create\\` →\n-- \\`orders\\` + \\`items\\`), e nunca escreve \\`appointment_items\\`. Resultado, visto\n-- na tela em 23/09: o botão vira \"Em atendimento\", e a régua nasce VAZIA —\n-- zero sessões, zero exigências, bolas nenhuma. Todo agendamento criado pela\n-- própria UI do V2 caía nisso; só os migrados do V1 funcionavam.\n--\n-- O conserto MATERIALIZA a linha do agendamento na hora de começar: se o\n-- agendamento tem pedido e ainda não tem \\`appointment_items\\`, elas nascem das\n-- linhas do pedido — e o resto do fluxo (sessões, FK de \\`booking_item_id\\`,\n-- régua, produtos por serviço) continua lendo o modelo que sempre leu.\n-- Idempotente: só insere quando não há linha nenhuma para o agendamento.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_attendance_start(p_booking uuid)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_created int := 0; v_adotadas int := 0;\n v_item record; v_refresh jsonb; v_antiga uuid;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.unit_id, ap.party_id, ap.assignee_id, ap.status,\n ap.starts_at, ap.order_id\n INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'not_found');\n END IF;\n\n v_person := public.current_person_id();\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional');\n END IF;\n IF a.assignee_id IS DISTINCT FROM v_person THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_seu_atendimento');\n END IF;\n IF a.starts_at::date <> (now() AT TIME ZONE 'America/Sao_Paulo')::date THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_de_hoje');\n END IF;\n IF a.status IN ('completed', 'cancelled', 'no_show') THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'ja_encerrado', 'status', a.status);\n END IF;\n\n -- Agendamento nascido no V2: as linhas vivem no pedido. Materializa a linha\n -- do agendamento — uma vez só — para o resto do fluxo ter onde se pendurar.\n IF a.order_id IS NOT NULL\n AND NOT EXISTS (SELECT 1 FROM public.appointment_items ai WHERE ai.booking_id = a.id) THEN\n INSERT INTO public.appointment_items (\n tenant_id, unit_id, booking_id, service_id, name, price,\n duration_minutes, assignee_id, sort_order)\n -- \\`coalesce(product_id, service_id)\\`: o \\`order_create\\` grava o serviço em\n -- \\`service_id\\`; a edição pela agenda grava em \\`product_id\\` (spine 190).\n -- As duas gerações de linha existem no banco.\n SELECT a.tenant_id, a.unit_id, a.id, coalesce(it.product_id, it.service_id),\n it.name, it.unit_price,\n it.duration_minutes, coalesce(it.assignee_id, a.assignee_id),\n coalesce(it.sort_order, 0)\n FROM public.items it\n WHERE it.order_id = a.order_id;\n END IF;\n\n FOR v_item IN\n SELECT ai.id, ai.service_id, ai.assignee_id\n FROM public.appointment_items ai\n WHERE ai.booking_id = p_booking\n LOOP\n -- Já existe sessão para este item? (dois toques, duas pessoas ao mesmo tempo)\n CONTINUE WHEN EXISTS (\n SELECT 1 FROM public.plg_clinical_sessions cs\n WHERE cs.booking_id = p_booking AND cs.booking_item_id = v_item.id);\n\n -- O QUE FOI COMPRADO ANTES E AINDA NÃO SAIU: adota em vez de criar.\n SELECT cs.id INTO v_antiga\n FROM public.plg_clinical_sessions cs\n WHERE cs.tenant_id = a.tenant_id\n AND cs.party_id = a.party_id\n AND cs.service_id = v_item.service_id\n AND cs.booking_id IS NULL\n AND cs.status IN ('contracted', 'quoted', 'pending')\n ORDER BY cs.created_at\n LIMIT 1;\n\n IF v_antiga IS NOT NULL THEN\n UPDATE public.plg_clinical_sessions\n SET booking_id = p_booking,\n booking_item_id = v_item.id,\n unit_id = coalesce(a.unit_id, unit_id),\n assignee_id = coalesce(v_item.assignee_id, a.assignee_id),\n performed_by_id = v_person,\n status = 'in_service',\n scheduled_for = a.starts_at,\n started_at = coalesce(started_at, now()),\n updated_at = now()\n WHERE id = v_antiga;\n v_adotadas := v_adotadas + 1;\n ELSE\n INSERT INTO public.plg_clinical_sessions (\n tenant_id, unit_id, status, booking_id, booking_item_id, party_id,\n assignee_id, performed_by_id, service_id, scheduled_for, started_at)\n VALUES (a.tenant_id, a.unit_id, 'in_service', a.id, v_item.id, a.party_id,\n coalesce(v_item.assignee_id, a.assignee_id), v_person, v_item.service_id,\n a.starts_at, now());\n v_created := v_created + 1;\n END IF;\n END LOOP;\n\n UPDATE public.appointments\n SET status = 'in_progress', updated_at = now()\n WHERE id = p_booking AND status <> 'in_progress';\n\n v_refresh := public.clinical_requirements_refresh(p_booking);\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sessoes_criadas', v_created,\n 'sessoes_adotadas', v_adotadas,\n 'sessoes', (SELECT count(*) FROM public.plg_clinical_sessions WHERE booking_id = p_booking),\n 'regua', v_refresh);\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_attendance_start(uuid) TO authenticated;\n`\n\nexport const MIGRATION_022_A_VISTA_DO_QUE_EXECUTAR_RESPEITA_A_CERCA = `-- ============================================================================\n-- THE \"TO EXECUTE\" VIEW READS WITH THE CALLER'S PRIVILEGES\n--\n-- \\`v_clinical_to_execute\\` was created in 014 without \\`security_invoker\\`, so it\n-- ran with the privileges of the role that OWNS it, and a view owned by a\n-- privileged role does not apply the row policies of the tables beneath it.\n-- \\`plg_clinical_sessions\\`, \\`items\\` and \\`plg_financial_movements\\` are all\n-- tenant-scoped with RLS; read through this view, they were not.\n--\n-- Not theoretical: the view answers, per party, how much a patient has already\n-- PAID for procedures not yet performed. Without the fence that is every\n-- clinic's prepaid balance, to anyone holding any session.\n--\n-- ── why nobody saw it ───────────────────────────────────────────────────────\n--\n-- N16 of the posture suite grades exactly this (\"every public view over a\n-- tenant_id table is security_invoker\"), and it stayed green throughout:\n-- plugin-clinical was never listed in \\`packages/db/chain.json\\`, so its objects\n-- were never built on the bench and never graded. The plugin reached a live\n-- pool on 2026-09-22 and no board at all. Putting it in the chain is what made\n-- the suite fail, on the first run, in the expected place.\n--\n-- ── why a new file, and not an edit to 014 ──────────────────────────────────\n--\n-- 014 is in \\`public._migrations\\` on a live pool with checksum 6f04df24cdb2,\n-- and the ledger treats a changed checksum on an applied file as a HARD STOP\n-- (cli/src/lib/ledger.ts). Editing it in place would not fix the view — it\n-- would stop \\`fayz db apply\\` on every cluster that already ran it.\n--\n-- The body below is 014's, copied unchanged. \\`CREATE OR REPLACE VIEW\\` refuses\n-- a different column list, and the only thing that may differ here is who the\n-- row filter runs as.\n-- ============================================================================\n\nCREATE OR REPLACE VIEW public.v_clinical_to_execute WITH (security_invoker = true) AS\nWITH sessao AS (\n SELECT\n cs.tenant_id,\n cs.party_id,\n cs.order_item_id,\n cs.status,\n i.order_id,\n coalesce(i.total, 0) AS item_total\n FROM public.plg_clinical_sessions cs\n JOIN public.items i ON i.id = cs.order_item_id\n WHERE cs.party_id IS NOT NULL\n AND cs.status IN ('contracted', 'quoted', 'in_service', 'scheduled', 'pending')\n),\n-- Quanto do PEDIDO já foi recebido, em proporção: o pagamento é por parcela e\n-- não por item, então a fatia paga de cada item é a fatia paga do pedido.\npago AS (\n SELECT m.invoice_id AS order_id,\n sum(coalesce(m.paid_amount, 0)) AS pago,\n nullif(sum(coalesce(m.amount, 0)), 0) AS total\n FROM public.plg_financial_movements m\n WHERE m.direction = 'credit'\n GROUP BY m.invoice_id\n)\nSELECT\n s.tenant_id,\n s.party_id,\n sum(s.item_total * coalesce(p.pago / p.total, 0))::numeric(14,2) AS paid_to_execute,\n sum(s.item_total * (1 - coalesce(p.pago / p.total, 0)))::numeric(14,2) AS open_to_execute,\n count(*)::int AS sessions_to_execute\nFROM sessao s\nLEFT JOIN pago p ON p.order_id = s.order_id\nGROUP BY s.tenant_id, s.party_id;\n`\n\nexport const MIGRATION_023_A_REVISAO_SE_ESCOLHE_AO_AGENDAR = `-- ============================================================================\n-- 023 — The revision is chosen when it is booked, not only during the visit.\n-- ============================================================================\n-- V1 asks \"De qual serviço é esta revisão?\" the moment the reception picks the\n-- Revisão service in Novo Agendamento (FAY-1613). 015 answered it only from an\n-- existing booking. This lists the same candidates for a PERSON, with the ordinal\n-- the next revision of each will take (V1 shows \"1ª Revisão\").\n--\n-- Also closes a gap in 015: \\`clinical_revision_candidates(p_booking)\\` is a\n-- definer function that read any booking id it was given, so a member of one\n-- tenant could list another tenant's patient history by guessing an id. Both\n-- now answer only inside the caller's tenant.\n--\n-- Idempotent.\n\nCREATE OR REPLACE FUNCTION public.clinical_revision_candidates_for_person(p_person uuid)\nRETURNS TABLE(session_id uuid, service_id uuid, service_name text, executed_on timestamp with time zone,\n deadline_days integer, days_since integer, within_deadline boolean, next_revision_number integer)\nLANGUAGE sql\nSTABLE SECURITY DEFINER\nSET search_path TO 'public', 'app'\nAS $function$\n SELECT\n cs.id,\n cs.service_id,\n p.name,\n coalesce(cs.ended_at, cs.scheduled_for) AS executed_on,\n rev.deadline_days,\n (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date)::int AS days_since,\n CASE WHEN rev.deadline_days IS NULL THEN NULL\n ELSE (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date) <= rev.deadline_days\n END AS within_deadline,\n coalesce((SELECT max(r.revision_number) FROM public.plg_clinical_sessions r\n WHERE r.tenant_id = cs.tenant_id AND r.parent_session_id = cs.id), 0) + 1\n FROM public.plg_clinical_sessions cs\n LEFT JOIN public.products p ON p.id = cs.service_id\n LEFT JOIN LATERAL (\n SELECT r.deadline_days FROM public.plg_clinical_service_revisions r\n WHERE r.tenant_id = cs.tenant_id AND r.service_id = cs.service_id\n ORDER BY r.revision_number LIMIT 1\n ) rev ON true\n WHERE cs.tenant_id = app.current_tenant_id()\n AND cs.party_id = p_person\n AND cs.status = 'executed'\n AND cs.parent_session_id IS NULL\n ORDER BY coalesce(cs.ended_at, cs.scheduled_for) DESC NULLS LAST\n LIMIT 50;\n$function$;\n\nREVOKE ALL ON FUNCTION public.clinical_revision_candidates_for_person(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.clinical_revision_candidates_for_person(uuid) TO authenticated;\n\nCREATE OR REPLACE FUNCTION public.clinical_revision_candidates(p_booking uuid)\nRETURNS TABLE(session_id uuid, service_id uuid, service_name text, executed_on timestamp with time zone,\n deadline_days integer, days_since integer, within_deadline boolean)\nLANGUAGE sql\nSTABLE SECURITY DEFINER\nSET search_path TO 'public', 'app'\nAS $function$\n WITH alvo AS (\n SELECT ap.tenant_id, ap.party_id FROM public.appointments ap\n WHERE ap.id = p_booking AND ap.tenant_id = app.current_tenant_id()\n )\n SELECT\n cs.id,\n cs.service_id,\n p.name,\n coalesce(cs.ended_at, cs.scheduled_for) AS executed_on,\n rev.deadline_days,\n (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date)::int AS days_since,\n CASE WHEN rev.deadline_days IS NULL THEN NULL\n ELSE (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date) <= rev.deadline_days\n END AS within_deadline\n FROM public.plg_clinical_sessions cs\n JOIN alvo a ON a.tenant_id = cs.tenant_id AND a.party_id = cs.party_id\n LEFT JOIN public.products p ON p.id = cs.service_id\n LEFT JOIN LATERAL (\n SELECT r.deadline_days FROM public.plg_clinical_service_revisions r\n WHERE r.tenant_id = cs.tenant_id AND r.service_id = cs.service_id\n ORDER BY r.revision_number LIMIT 1\n ) rev ON true\n WHERE cs.status = 'executed'\n AND cs.booking_id IS DISTINCT FROM p_booking\n ORDER BY coalesce(cs.ended_at, cs.scheduled_for) DESC NULLS LAST\n LIMIT 50;\n$function$;\n`\n\nexport const MIGRATION_024_A_AVALIACAO_DESTA_VISITA_VALE_PARA_O_QUE_ELA_CONTRATOU = `-- The evaluation done in this attendance covers what was contracted in it.\n--\n-- FAY-1613 (Espaço Facial, etapa 3): the professional fills the \"Avaliação\" form, indicates, the\n-- client contracts — and every contracted session came out with its own \"Avaliação\" step pending,\n-- because nothing in V2 points a contracted session at the evaluation that authorised it\n-- (\\`source_evaluation_id\\` is only set by the V1 import). V1 shows \"Avaliação concluída — o\n-- atendimento segue para a execução\" at that point.\n--\n-- Same body as live, plus step 5c. Idempotent (CREATE OR REPLACE).\n\nCREATE OR REPLACE FUNCTION public.clinical_requirements_refresh(p_booking uuid, p_final boolean DEFAULT false)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO 'public'\nAS $function$\nDECLARE\n a record; v_mode text; v_now timestamptz := now(); v_eval jsonb;\n n_mat int := 0; n_reopen int := 0;\n -- \\`v_sessao\\`, não \\`r\\`: plpgsql resolve o nome da VARIÁVEL antes do apelido da\n -- tabela, então um \\`record r\\` declarado aqui sequestra todo \\`r.coluna\\` dos\n -- UPDATEs abaixo — e o erro que sai é \"record r has no field booking_id\".\n v_sessao record;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.party_id, ap.status INTO a\n FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n v_mode := public.clinical_requirements_mode(a.tenant_id);\n IF v_mode = 'off' THEN RETURN jsonb_build_object('ok', true, 'mode', v_mode, 'skipped', true); END IF;\n\n p_final := coalesce(p_final, false) AND a.status = 'completed';\n\n BEGIN\n PERFORM pg_advisory_xact_lock(hashtextextended('clinical_req:' || p_booking::text, 0));\n IF a.status = 'completed' AND NOT p_final THEN\n RETURN jsonb_build_object('ok', true, 'mode', v_mode, 'frozen', true);\n END IF;\n\n -- 1) Materializa o que falta (sessão nova, ficha técnica criada depois).\n FOR v_sessao IN SELECT cs.id FROM public.plg_clinical_sessions cs WHERE cs.booking_id = p_booking\n LOOP\n n_mat := n_mat + public.clinical_requirements_materialize(v_sessao.id);\n END LOOP;\n\n v_eval := coalesce((SELECT t.settings -> 'evaluation_flow' FROM public.tenants t WHERE t.id = a.tenant_id), '{}'::jsonb);\n IF jsonb_typeof(v_eval) <> 'object' THEN v_eval := '{}'::jsonb; END IF;\n\n -- 1b) Regra que saiu da configuração para de bloquear (o upsert reabre se voltar).\n UPDATE public.plg_clinical_requirements r\n SET status = 'not_applicable', candidate_type = NULL, candidate_id = NULL, updated_at = v_now\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.frozen_at IS NULL\n AND r.metadata -> 'migration' IS NULL\n AND ((r.kind = 'revision_link' AND NOT coalesce((v_eval ->> 'revision_link_required')::boolean, false))\n OR (r.kind = 'evaluation'\n AND r.rule_sources = '[\"evaluation_flow.require_evaluation_for_kind_service\"]'::jsonb\n AND NOT coalesce((v_eval ->> 'require_evaluation_for_kind_service')::boolean, false)));\n\n -- 2) Reabre o que perdeu a fonte. Linha congelada não se mexe.\n UPDATE public.plg_clinical_requirements r\n SET status = 'pending', subject_type = NULL, subject_id = NULL, resolved_via = NULL,\n resolved_at = NULL, valid_until = NULL, updated_at = v_now\n WHERE r.booking_id = p_booking AND r.status = 'resolved' AND r.frozen_at IS NULL\n AND ((r.subject_type = 'documents' AND NOT EXISTS (SELECT 1 FROM public.documents d WHERE d.id = r.subject_id))\n OR (r.subject_type = 'clinical_sessions' AND NOT EXISTS (SELECT 1 FROM public.plg_clinical_sessions x WHERE x.id = r.subject_id))\n OR (r.subject_type = 'items' AND NOT EXISTS (SELECT 1 FROM public.items oi WHERE oi.id = r.subject_id)));\n GET DIAGNOSTICS n_reopen = ROW_COUNT;\n\n -- 3) Formulário universal: a última resposta do cliente, em qualquer\n -- atendimento, ainda dentro da validade do modelo.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'documents', subject_id = x.id,\n resolved_via = CASE WHEN x.subject_id = p_booking THEN 'attendance' ELSE 'auto_client' END,\n resolved_at = v_now,\n valid_until = CASE WHEN x.validity_days IS NULL THEN NULL\n ELSE x.eff + make_interval(days => x.validity_days) END,\n updated_at = v_now\n FROM (SELECT DISTINCT ON (fd.template_id) d.id, fd.template_id, d.subject_id,\n coalesce(d.updated_at, d.created_at) AS eff, ft.validity_days\n FROM public.plg_forms_documents fd\n JOIN public.documents d ON d.id = fd.document_id\n JOIN public.plg_forms_templates ft ON ft.id = fd.template_id\n WHERE d.person_id = a.party_id AND d.tenant_id = a.tenant_id\n ORDER BY fd.template_id, coalesce(d.updated_at, d.created_at) DESC) x\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'form'\n AND r.scope = 'attendance' AND r.ref_id = x.template_id\n AND r.rule_sources @> '[\"forms_templates.required_on_attendance\"]'::jsonb\n AND (x.validity_days IS NULL OR x.eff + make_interval(days => x.validity_days) > v_now);\n\n -- 4) Demais formulários: qualquer resposta DESTE atendimento com o modelo.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'documents', subject_id = x.id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM (SELECT DISTINCT ON (fd.template_id, coalesce(fd.product_id, '00000000-0000-0000-0000-000000000000'::uuid))\n d.id, fd.template_id, fd.product_id\n FROM public.plg_forms_documents fd\n JOIN public.documents d ON d.id = fd.document_id\n WHERE d.subject_type = 'appointment' AND d.subject_id = p_booking\n ORDER BY fd.template_id, coalesce(fd.product_id, '00000000-0000-0000-0000-000000000000'::uuid),\n coalesce(d.updated_at, d.created_at) DESC) x\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'form'\n AND r.ref_id = x.template_id\n AND NOT (r.rule_sources @> '[\"forms_templates.required_on_attendance\"]'::jsonb)\n AND (x.product_id IS NULL OR r.scope = 'attendance' OR x.product_id = r.service_id);\n\n -- 5) Avaliação vinculada: a sessão aponta a avaliação que a autorizou.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = x.eval_id,\n resolved_via = CASE WHEN x.same_booking THEN 'attendance' ELSE 'auto_client' END,\n resolved_at = v_now, updated_at = v_now\n FROM (SELECT r2.id AS req_id, e.id AS eval_id, (e.booking_id = p_booking) AS same_booking\n FROM public.plg_clinical_requirements r2\n JOIN public.plg_clinical_sessions cs ON cs.id = r2.session_id\n JOIN public.plg_clinical_sessions e ON e.id = cs.source_evaluation_id\n WHERE r2.booking_id = p_booking AND r2.kind = 'evaluation' AND r2.frozen_at IS NULL\n AND (r2.status = 'pending' OR (r2.status = 'resolved' AND r2.resolved_via = 'legacy_grace'))) x\n WHERE r.id = x.req_id;\n\n -- 5b) Graça legada: o cliente já tem atendimento concluído com serviço de\n -- avaliação. É a mesma concessão do V1, e vai marcada como tal.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'appointments', subject_id = x.booking_id,\n resolved_via = 'legacy_grace', resolved_at = v_now, updated_at = v_now\n FROM (SELECT ap.id AS booking_id\n FROM public.appointments ap\n JOIN public.appointment_items ai ON ai.booking_id = ap.id\n JOIN public.products p ON p.id = ai.service_id AND p.service_kind = 'evaluation'\n WHERE ap.party_id = a.party_id AND ap.tenant_id = a.tenant_id AND ap.status = 'completed'\n ORDER BY ap.starts_at DESC NULLS LAST LIMIT 1) x\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'evaluation';\n\n -- 5c) Avaliação feita NESTE atendimento. Contratar depois de avaliar cria sessões novas, e cada\n -- uma nascia com a própria bola \"Avaliação\" pendente — o profissional acabou de avaliar.\n -- Indicação registrada aqui (o formulário de avaliação) ou sessão de avaliação deste\n -- atendimento resolvem as sessões contratadas na mesma visita.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'appointments', subject_id = p_booking,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'evaluation'\n AND r.frozen_at IS NULL\n AND (EXISTS (SELECT 1 FROM public.plg_crm_deal_interest_appointments x WHERE x.appointment_id = p_booking)\n OR EXISTS (SELECT 1 FROM public.plg_clinical_sessions e\n JOIN public.products p ON p.id = e.service_id AND p.service_kind = 'evaluation'\n WHERE e.booking_id = p_booking));\n\n -- 6) Revisão vinculada ao serviço original.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = cs.parent_session_id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM public.plg_clinical_sessions cs\n WHERE cs.id = r.session_id AND r.booking_id = p_booking\n AND r.status = 'pending' AND r.kind = 'revision_link'\n AND cs.parent_session_id IS NOT NULL\n AND EXISTS (SELECT 1 FROM public.plg_clinical_sessions p WHERE p.id = cs.parent_session_id);\n\n -- 7) Produtos: a sessão declara que foram conferidos.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = cs.id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM public.plg_clinical_sessions cs\n WHERE cs.id = r.session_id AND r.booking_id = p_booking\n AND r.status = 'pending' AND r.kind = 'products'\n AND coalesce(cs.products_initialized, false);\n\n -- 8) Item de pedido vinculado. A linha do pedido no V2 é \\`public.items\\` —\n -- não existe \\`order_items\\`; o pedido é o documento e o item é dele.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'items', subject_id = oi.id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM public.plg_clinical_sessions cs\n JOIN public.items oi ON oi.id = cs.order_item_id\n WHERE cs.id = r.session_id AND r.booking_id = p_booking\n AND r.status = 'pending' AND r.kind = 'invoice_item';\n\n -- 9) Congelamento: só na última passada, com o atendimento concluído.\n IF p_final THEN\n UPDATE public.plg_clinical_requirements\n SET frozen_at = coalesce(frozen_at, v_now), updated_at = v_now\n WHERE booking_id = p_booking;\n END IF;\n\n RETURN jsonb_build_object(\n 'ok', true, 'mode', v_mode, 'materialized', n_mat, 'reopened', n_reopen,\n 'pending', (SELECT count(*) FROM public.plg_clinical_requirements WHERE booking_id = p_booking AND status = 'pending'),\n 'resolved', (SELECT count(*) FROM public.plg_clinical_requirements WHERE booking_id = p_booking AND status = 'resolved'),\n 'frozen', p_final);\n EXCEPTION WHEN others THEN\n RAISE WARNING 'clinical_requirements_refresh(%): %', p_booking, sqlerrm;\n RETURN jsonb_build_object('ok', false, 'error', sqlerrm);\n END;\nEND $function$;\n`\n\nexport const MIGRATION_025_AS_REGRAS_DO_ATENDIMENTO_SAO_CONFIGURACAO = `-- The attendance rules 004 already reads, registered as setting keys so the\n-- settings screen can write them through tenant_setting_set (typed, permission\n-- checked). Same paths the V1 import wrote: tenants.settings.execution_requirements.mode\n-- and tenants.settings.evaluation_flow.*. Nested writes keep the sibling keys.\n--\n-- Idempotent.\nSELECT app.seed_setting_keys($$[\n { \"key\": \"execution_requirements.mode\", \"type\": \"string\", \"default\": \"off\", \"scope\": \"tenant\", \"reader\": \"plugin:clinical\",\n \"description\": \"Execution requirements: off, shadow (warn only) or on (block). V1: general_settings.execution_requirements.mode.\" },\n { \"key\": \"evaluation_flow.require_evaluation_for_kind_service\", \"type\": \"boolean\", \"default\": false, \"scope\": \"tenant\", \"reader\": \"plugin:clinical\",\n \"description\": \"A paid service needs a linked evaluation. V1: general_settings.evaluation_flow.\" },\n { \"key\": \"evaluation_flow.revision_link_required\", \"type\": \"boolean\", \"default\": false, \"scope\": \"tenant\", \"reader\": \"plugin:clinical\",\n \"description\": \"A revision must be linked to the original service; blocks start and finish. V1: general_settings.evaluation_flow.\" }\n]$$::jsonb);\n`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"001_o_atendimento_clinico_tem_onde_morar\", sql: MIGRATION_001_O_ATENDIMENTO_CLINICO_TEM_ONDE_MORAR },\n { id: \"002_o_prontuario_do_v1_atravessa\", sql: MIGRATION_002_O_PRONTUARIO_DO_V1_ATRAVESSA },\n { id: \"003_a_permissao_se_pergunta_por_unidade_nao_por_linha\", sql: MIGRATION_003_A_PERMISSAO_SE_PERGUNTA_POR_UNIDADE_NAO_POR_LINHA },\n { id: \"004_a_regua_volta_a_ser_apurada\", sql: MIGRATION_004_A_REGUA_VOLTA_A_SER_APURADA },\n { id: \"005_comecar_o_atendimento_e_um_gesto_so\", sql: MIGRATION_005_COMECAR_O_ATENDIMENTO_E_UM_GESTO_SO },\n { id: \"006_contratar_nao_e_pagar\", sql: MIGRATION_006_CONTRATAR_NAO_E_PAGAR },\n { id: \"007_finalizar_acende_o_balcao\", sql: MIGRATION_007_FINALIZAR_ACENDE_O_BALCAO },\n { id: \"008_conferir_nao_e_receber\", sql: MIGRATION_008_CONFERIR_NAO_E_RECEBER },\n { id: \"009_o_botao_de_cobrar_cobra\", sql: MIGRATION_009_O_BOTAO_DE_COBRAR_COBRA },\n { id: \"010_a_indicacao_vai_para_o_funil\", sql: MIGRATION_010_A_INDICACAO_VAI_PARA_O_FUNIL },\n { id: \"011_interesse_e_indicacao_na_mesma_linha\", sql: MIGRATION_011_INTERESSE_E_INDICACAO_NA_MESMA_LINHA },\n { id: \"012_o_que_foi_usado_espera_aprovacao\", sql: MIGRATION_012_O_QUE_FOI_USADO_ESPERA_APROVACAO },\n { id: \"013_o_que_couber_hoje\", sql: MIGRATION_013_O_QUE_COUBER_HOJE },\n { id: \"014_o_pago_a_executar_deixa_de_ser_invisivel\", sql: MIGRATION_014_O_PAGO_A_EXECUTAR_DEIXA_DE_SER_INVISIVEL },\n { id: \"015_a_revisao_aponta_o_que_revisa\", sql: MIGRATION_015_A_REVISAO_APONTA_O_QUE_REVISA },\n { id: \"016_a_compra_antecipada_nao_tem_agendamento_atras\", sql: MIGRATION_016_A_COMPRA_ANTECIPADA_NAO_TEM_AGENDAMENTO_ATRAS },\n { id: \"017_o_que_ja_foi_pago_nao_se_cobra_de_novo\", sql: MIGRATION_017_O_QUE_JA_FOI_PAGO_NAO_SE_COBRA_DE_NOVO },\n { id: \"018_o_que_foi_contratado_vira_sessao\", sql: MIGRATION_018_O_QUE_FOI_CONTRATADO_VIRA_SESSAO },\n { id: \"019_adiar_e_uma_escolha\", sql: MIGRATION_019_ADIAR_E_UMA_ESCOLHA },\n { id: \"020_cobrar_um_agendamento_sem_pedido_levanta_o_pedido\", sql: MIGRATION_020_COBRAR_UM_AGENDAMENTO_SEM_PEDIDO_LEVANTA_O_PEDIDO },\n { id: \"021_agendamento_nascido_no_v2_tambem_tem_regua\", sql: MIGRATION_021_AGENDAMENTO_NASCIDO_NO_V2_TAMBEM_TEM_REGUA },\n { id: \"022_a_vista_do_que_executar_respeita_a_cerca\", sql: MIGRATION_022_A_VISTA_DO_QUE_EXECUTAR_RESPEITA_A_CERCA },\n { id: \"023_a_revisao_se_escolhe_ao_agendar\", sql: MIGRATION_023_A_REVISAO_SE_ESCOLHE_AO_AGENDAR },\n { id: \"024_a_avaliacao_desta_visita_vale_para_o_que_ela_contratou\", sql: MIGRATION_024_A_AVALIACAO_DESTA_VISITA_VALE_PARA_O_QUE_ELA_CONTRATOU },\n { id: \"025_as_regras_do_atendimento_sao_configuracao\", sql: MIGRATION_025_AS_REGRAS_DO_ATENDIMENTO_SAO_CONFIGURACAO },\n]\n","import { attendanceSettingsParent, defineSettingsTab } from '@fayz-ai/admin'\nimport { tl } from '@fayz-ai/core'\n\n// V1 \"Ordem das etapas\" / \"Modelos de contratos\": the rules the clinical\n// requirement engine (migration 004) already reads.\nexport const attendanceSettingsTab = defineSettingsTab({\n id: 'clinical-attendance-rules',\n label: tl('Rules', 'Regras'),\n icon: 'ClipboardCheck',\n section: 'operations',\n parent: attendanceSettingsParent(),\n order: 2,\n keywords: [tl('Attendance rules', 'Regras do atendimento'), tl('Evaluation', 'Avaliação')],\n defaults: {\n 'execution_requirements.mode': 'off',\n 'evaluation_flow.require_evaluation_for_kind_service': false,\n 'evaluation_flow.revision_link_required': false,\n },\n sections: [{\n description: tl('What must be done before an attendance starts or finishes.', 'O que precisa estar feito antes de começar ou terminar um atendimento.'),\n fields: [\n { key: 'execution_requirements.mode', type: 'select', label: tl('Execution requirements', 'Exigências de execução'), options: [\n { value: 'off', label: tl('Off', 'Desligado') },\n { value: 'shadow', label: tl('Warn only', 'Só avisar') },\n { value: 'on', label: tl('Enforce', 'Exigir') },\n ] },\n { key: 'evaluation_flow.require_evaluation_for_kind_service', type: 'boolean', label: tl('A paid service needs an evaluation', 'Serviço pago precisa de avaliação vinculada') },\n { key: 'evaluation_flow.revision_link_required', type: 'boolean', label: tl('A revision must point to the original service', 'Revisão precisa estar ligada ao serviço original') },\n ],\n }],\n})\n","import { definePlugin, type PluginManifest } from '@fayz-ai/core'\nimport { MIGRATIONS } from './migrations'\nimport { attendanceSettingsTab } from './settings/attendance-settings'\n\nexport type {\n ClinicalPrepaidOutcome,\n ClinicalRequirementKind,\n ClinicalRequirementScope,\n ClinicalRequirementStatus,\n ClinicalSessionStatus,\n} from './types'\n\nexport interface ClinicalPluginOptions {\n /** Onde o item entra no menu. Ausente = o plugin não desenha navegação. */\n navPosition?: number\n}\n\n/**\n * O atendimento clínico.\n *\n * A agenda responde QUEM, QUANDO e ONDE. Este plugin responde O QUE FOI FEITO:\n * anamnese com validade, avaliação com foto, indicação, contratação, execução\n * por sessão e consumo de produto contra a ficha técnica.\n *\n * É vertical (estética, saúde), não núcleo — por isso `scope: 'addon'` e\n * `optional` no chain.json.\n *\n * Nesta versão o plugin é só BANCO: as tabelas existem, a RLS existe e o porte\n * do V1 tem onde cair. A tela vem depois, e vem do Guia do V1\n * (`jornadas/avaliacao-e-contratacao`), não da imaginação.\n */\nexport function createClinicalPlugin(_options: ClinicalPluginOptions = {}): PluginManifest {\n return definePlugin({\n id: 'clinical',\n name: 'Prontuário',\n icon: 'Stethoscope',\n version: '0.1.0',\n apiVersion: 1,\n scope: 'addon',\n defaultEnabled: false,\n dependencies: ['agenda'],\n navigation: [],\n routes: [],\n settings: [attendanceSettingsTab],\n migrations: MIGRATIONS.map((entry) => ({\n id: `clinical-${entry.id}`,\n version: '1.0.0',\n sql: entry.sql,\n description: 'A sessão clínica, o consumo de produto, as exigências de execução e a revisão do serviço.',\n })),\n })\n}\n\nexport default createClinicalPlugin\n"]}
1
+ {"version":3,"sources":["../src/settings/attendance-settings.ts","../src/index.ts"],"names":[],"mappings":";;;;;AAKO,IAAM,wBAAwB,iBAAA,CAAkB;AAAA,EACrD,EAAA,EAAI,2BAAA;AAAA,EACJ,KAAA,EAAO,EAAA,CAAG,OAAA,EAAS,QAAQ,CAAA;AAAA,EAC3B,IAAA,EAAM,gBAAA;AAAA,EACN,OAAA,EAAS,YAAA;AAAA,EACT,QAAQ,wBAAA,EAAyB;AAAA,EACjC,KAAA,EAAO,CAAA;AAAA,EACP,QAAA,EAAU,CAAC,EAAA,CAAG,kBAAA,EAAoB,uBAAuB,CAAA,EAAG,EAAA,CAAG,YAAA,EAAc,iBAAW,CAAC,CAAA;AAAA,EACzF,QAAA,EAAU;AAAA,IACR,6BAAA,EAA+B,KAAA;AAAA,IAC/B,qDAAA,EAAuD,KAAA;AAAA,IACvD,wCAAA,EAA0C;AAAA,GAC5C;AAAA,EACA,UAAU,CAAC;AAAA,IACT,WAAA,EAAa,EAAA,CAAG,4DAAA,EAA8D,2EAAwE,CAAA;AAAA,IACtJ,MAAA,EAAQ;AAAA,MACN,EAAE,GAAA,EAAK,6BAAA,EAA+B,IAAA,EAAM,QAAA,EAAU,OAAO,EAAA,CAAG,wBAAA,EAA0B,iCAAwB,CAAA,EAAG,OAAA,EAAS;AAAA,QAC5H,EAAE,KAAA,EAAO,KAAA,EAAO,OAAO,EAAA,CAAG,KAAA,EAAO,WAAW,CAAA,EAAE;AAAA,QAC9C,EAAE,KAAA,EAAO,QAAA,EAAU,OAAO,EAAA,CAAG,WAAA,EAAa,cAAW,CAAA,EAAE;AAAA,QACvD,EAAE,KAAA,EAAO,IAAA,EAAM,OAAO,EAAA,CAAG,SAAA,EAAW,QAAQ,CAAA;AAAE,OAChD,EAAE;AAAA,MACF,EAAE,KAAK,qDAAA,EAAuD,IAAA,EAAM,WAAW,KAAA,EAAO,EAAA,CAAG,oCAAA,EAAsC,sDAA6C,CAAA,EAAE;AAAA,MAC9K,EAAE,KAAK,wCAAA,EAA0C,IAAA,EAAM,WAAW,KAAA,EAAO,EAAA,CAAG,+CAAA,EAAiD,wDAAkD,CAAA;AAAE;AACnL,GACD;AACH,CAAC,CAAA;;;ACCM,SAAS,oBAAA,CAAqB,QAAA,GAAkC,EAAC,EAAmB;AACzF,EAAA,OAAO,YAAA,CAAa;AAAA,IAClB,EAAA,EAAI,UAAA;AAAA,IACJ,IAAA,EAAM,eAAA;AAAA,IACN,IAAA,EAAM,aAAA;AAAA,IACN,OAAA,EAAS,OAAA;AAAA,IACT,UAAA,EAAY,CAAA;AAAA,IACZ,KAAA,EAAO,OAAA;AAAA,IACP,cAAA,EAAgB,KAAA;AAAA,IAChB,YAAA,EAAc,CAAC,QAAQ,CAAA;AAAA,IACvB,YAAY,EAAC;AAAA,IACb,QAAQ,EAAC;AAAA,IACT,QAAA,EAAU,CAAC,qBAAqB,CAAA;AAAA,IAChC,UAAA,EAAY,UAAA,CAAW,GAAA,CAAI,CAAC,KAAA,MAAW;AAAA,MACrC,EAAA,EAAI,CAAA,SAAA,EAAY,KAAA,CAAM,EAAE,CAAA,CAAA;AAAA,MACxB,OAAA,EAAS,OAAA;AAAA,MACT,KAAK,KAAA,CAAM,GAAA;AAAA,MACX,WAAA,EAAa;AAAA,KACf,CAAE;AAAA,GACH,CAAA;AACH;AAEA,IAAO,WAAA,GAAQ","file":"index.js","sourcesContent":["import { attendanceSettingsParent, defineSettingsTab } from '@fayz-ai/admin'\nimport { tl } from '@fayz-ai/core'\n\n// V1 \"Ordem das etapas\" / \"Modelos de contratos\": the rules the clinical\n// requirement engine (migration 004) already reads.\nexport const attendanceSettingsTab = defineSettingsTab({\n id: 'clinical-attendance-rules',\n label: tl('Rules', 'Regras'),\n icon: 'ClipboardCheck',\n section: 'operations',\n parent: attendanceSettingsParent(),\n order: 2,\n keywords: [tl('Attendance rules', 'Regras do atendimento'), tl('Evaluation', 'Avaliação')],\n defaults: {\n 'execution_requirements.mode': 'off',\n 'evaluation_flow.require_evaluation_for_kind_service': false,\n 'evaluation_flow.revision_link_required': false,\n },\n sections: [{\n description: tl('What must be done before an attendance starts or finishes.', 'O que precisa estar feito antes de começar ou terminar um atendimento.'),\n fields: [\n { key: 'execution_requirements.mode', type: 'select', label: tl('Execution requirements', 'Exigências de execução'), options: [\n { value: 'off', label: tl('Off', 'Desligado') },\n { value: 'shadow', label: tl('Warn only', 'Só avisar') },\n { value: 'on', label: tl('Enforce', 'Exigir') },\n ] },\n { key: 'evaluation_flow.require_evaluation_for_kind_service', type: 'boolean', label: tl('A paid service needs an evaluation', 'Serviço pago precisa de avaliação vinculada') },\n { key: 'evaluation_flow.revision_link_required', type: 'boolean', label: tl('A revision must point to the original service', 'Revisão precisa estar ligada ao serviço original') },\n ],\n }],\n})\n","import { definePlugin, type PluginManifest } from '@fayz-ai/core'\nimport { MIGRATIONS } from './migrations'\nimport { attendanceSettingsTab } from './settings/attendance-settings'\n\nexport type {\n ClinicalPrepaidOutcome,\n ClinicalRequirementKind,\n ClinicalRequirementScope,\n ClinicalRequirementStatus,\n ClinicalSessionStatus,\n} from './types'\n\nexport interface ClinicalPluginOptions {\n /** Onde o item entra no menu. Ausente = o plugin não desenha navegação. */\n navPosition?: number\n}\n\n/**\n * O atendimento clínico.\n *\n * A agenda responde QUEM, QUANDO e ONDE. Este plugin responde O QUE FOI FEITO:\n * anamnese com validade, avaliação com foto, indicação, contratação, execução\n * por sessão e consumo de produto contra a ficha técnica.\n *\n * É vertical (estética, saúde), não núcleo — por isso `scope: 'addon'` e\n * `optional` no chain.json.\n *\n * Nesta versão o plugin é só BANCO: as tabelas existem, a RLS existe e o porte\n * do V1 tem onde cair. A tela vem depois, e vem do Guia do V1\n * (`jornadas/avaliacao-e-contratacao`), não da imaginação.\n */\nexport function createClinicalPlugin(_options: ClinicalPluginOptions = {}): PluginManifest {\n return definePlugin({\n id: 'clinical',\n name: 'Prontuário',\n icon: 'Stethoscope',\n version: '0.1.0',\n apiVersion: 1,\n scope: 'addon',\n defaultEnabled: false,\n dependencies: ['agenda'],\n navigation: [],\n routes: [],\n settings: [attendanceSettingsTab],\n migrations: MIGRATIONS.map((entry) => ({\n id: `clinical-${entry.id}`,\n version: '1.0.0',\n sql: entry.sql,\n description: 'A sessão clínica, o consumo de produto, as exigências de execução e a revisão do serviço.',\n })),\n })\n}\n\nexport default createClinicalPlugin\n"]}