@baruk/core 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.baruk-provenance.json +4 -4
- package/CHANGELOG.md +42 -0
- package/dist/audit/repository.d.ts +33 -1
- package/dist/audit/repository.d.ts.map +1 -1
- package/dist/audit/repository.js +41 -7
- package/dist/audit/repository.js.map +1 -1
- package/dist/auth/service.d.ts +21 -0
- package/dist/auth/service.d.ts.map +1 -1
- package/dist/auth/service.js +46 -1
- package/dist/auth/service.js.map +1 -1
- package/dist/billing/ar-repository.d.ts +41 -1
- package/dist/billing/ar-repository.d.ts.map +1 -1
- package/dist/billing/ar-repository.js +82 -6
- package/dist/billing/ar-repository.js.map +1 -1
- package/dist/billing/provider-account.d.ts +38 -0
- package/dist/billing/provider-account.d.ts.map +1 -1
- package/dist/billing/provider-account.js +58 -2
- package/dist/billing/provider-account.js.map +1 -1
- package/dist/billing/repository.d.ts +44 -0
- package/dist/billing/repository.d.ts.map +1 -1
- package/dist/billing/repository.js +49 -5
- package/dist/billing/repository.js.map +1 -1
- package/dist/cjs/audit/repository.js +41 -7
- package/dist/cjs/audit/repository.js.map +1 -1
- package/dist/cjs/auth/service.js +46 -1
- package/dist/cjs/auth/service.js.map +1 -1
- package/dist/cjs/billing/ar-repository.js +82 -6
- package/dist/cjs/billing/ar-repository.js.map +1 -1
- package/dist/cjs/billing/provider-account.js +60 -1
- package/dist/cjs/billing/provider-account.js.map +1 -1
- package/dist/cjs/billing/repository.js +51 -5
- package/dist/cjs/billing/repository.js.map +1 -1
- package/dist/cjs/clients/ibge.js +103 -0
- package/dist/cjs/clients/ibge.js.map +1 -0
- package/dist/cjs/clients/repository.js +186 -9
- package/dist/cjs/clients/repository.js.map +1 -1
- package/dist/cjs/collection/branding.js +190 -0
- package/dist/cjs/collection/branding.js.map +1 -1
- package/dist/cjs/collection/clearance-store.js +74 -0
- package/dist/cjs/collection/clearance-store.js.map +1 -0
- package/dist/cjs/collection/communication-runtime.js +65 -2
- package/dist/cjs/collection/communication-runtime.js.map +1 -1
- package/dist/cjs/collection/engine.js +20 -0
- package/dist/cjs/collection/engine.js.map +1 -1
- package/dist/cjs/collection/policy.js +29 -1
- package/dist/cjs/collection/policy.js.map +1 -1
- package/dist/cjs/communication/clearance.js +18 -0
- package/dist/cjs/communication/clearance.js.map +1 -0
- package/dist/cjs/communication/dry-run-scenario.js +5 -0
- package/dist/cjs/communication/dry-run-scenario.js.map +1 -1
- package/dist/cjs/communication/engine.js +12 -0
- package/dist/cjs/communication/engine.js.map +1 -1
- package/dist/cjs/communication/policy.js.map +1 -1
- package/dist/cjs/competence/business-days.js +155 -0
- package/dist/cjs/competence/business-days.js.map +1 -0
- package/dist/cjs/competence/catalog-validation.js +174 -0
- package/dist/cjs/competence/catalog-validation.js.map +1 -0
- package/dist/cjs/competence/obligation-preview.js +190 -0
- package/dist/cjs/competence/obligation-preview.js.map +1 -0
- package/dist/cjs/competence/repository.js +330 -17
- package/dist/cjs/competence/repository.js.map +1 -1
- package/dist/cjs/dashboard/pilot-panel.js +90 -0
- package/dist/cjs/dashboard/pilot-panel.js.map +1 -0
- package/dist/cjs/dashboard/repository.js +11 -1
- package/dist/cjs/dashboard/repository.js.map +1 -1
- package/dist/cjs/db/client.js +17 -11
- package/dist/cjs/db/client.js.map +1 -1
- package/dist/cjs/db/provision-e2e-identity.js +111 -0
- package/dist/cjs/db/provision-e2e-identity.js.map +1 -0
- package/dist/cjs/fiscal/applicability.js +164 -0
- package/dist/cjs/fiscal/applicability.js.map +1 -0
- package/dist/cjs/fiscal/conditions.js +198 -0
- package/dist/cjs/fiscal/conditions.js.map +1 -0
- package/dist/cjs/fiscal/contracts/nfse-adn-contract.js +390 -0
- package/dist/cjs/fiscal/contracts/nfse-adn-contract.js.map +1 -0
- package/dist/cjs/fiscal/contracts/nfse-adn-endpoint.js +108 -0
- package/dist/cjs/fiscal/contracts/nfse-adn-endpoint.js.map +1 -0
- package/dist/cjs/fiscal/contracts/nfse-adn-envelope.js +220 -0
- package/dist/cjs/fiscal/contracts/nfse-adn-envelope.js.map +1 -0
- package/dist/cjs/fiscal/coverage/model.js +127 -0
- package/dist/cjs/fiscal/coverage/model.js.map +1 -0
- package/dist/cjs/fiscal/coverage/requests.js +99 -0
- package/dist/cjs/fiscal/coverage/requests.js.map +1 -0
- package/dist/cjs/fiscal/credentials/material.js +168 -0
- package/dist/cjs/fiscal/credentials/material.js.map +1 -0
- package/dist/cjs/fiscal/credentials/readiness.js +163 -0
- package/dist/cjs/fiscal/credentials/readiness.js.map +1 -0
- package/dist/cjs/fiscal/eligibility.js +199 -0
- package/dist/cjs/fiscal/eligibility.js.map +1 -0
- package/dist/cjs/fiscal/ingestion/__fixtures__/nfse.js +128 -0
- package/dist/cjs/fiscal/ingestion/__fixtures__/nfse.js.map +1 -0
- package/dist/cjs/fiscal/ingestion/adn-transport.js +229 -0
- package/dist/cjs/fiscal/ingestion/adn-transport.js.map +1 -0
- package/dist/cjs/fiscal/ingestion/direction.js +36 -0
- package/dist/cjs/fiscal/ingestion/direction.js.map +1 -0
- package/dist/cjs/fiscal/ingestion/nfse-adn.js +228 -0
- package/dist/cjs/fiscal/ingestion/nfse-adn.js.map +1 -0
- package/dist/cjs/fiscal/ingestion/pipeline.js +704 -0
- package/dist/cjs/fiscal/ingestion/pipeline.js.map +1 -0
- package/dist/cjs/fiscal/ingestion/provider.js +51 -0
- package/dist/cjs/fiscal/ingestion/provider.js.map +1 -0
- package/dist/cjs/fiscal/ingestion/repository.js +132 -0
- package/dist/cjs/fiscal/ingestion/repository.js.map +1 -0
- package/dist/cjs/fiscal/ingestion/synthetic-provider.js +103 -0
- package/dist/cjs/fiscal/ingestion/synthetic-provider.js.map +1 -0
- package/dist/cjs/fiscal/ingestion/synthetic-transport-types.js +13 -0
- package/dist/cjs/fiscal/ingestion/synthetic-transport-types.js.map +1 -0
- package/dist/cjs/fiscal/ingestion/synthetic-transport.js +61 -0
- package/dist/cjs/fiscal/ingestion/synthetic-transport.js.map +1 -0
- package/dist/cjs/fiscal/ingestion/xml-reader.js +121 -0
- package/dist/cjs/fiscal/ingestion/xml-reader.js.map +1 -0
- package/dist/cjs/fiscal/repository.js +470 -0
- package/dist/cjs/fiscal/repository.js.map +1 -0
- package/dist/cjs/fiscal/sync/circuit.js +124 -0
- package/dist/cjs/fiscal/sync/circuit.js.map +1 -0
- package/dist/cjs/fiscal/sync/intervention.js +140 -0
- package/dist/cjs/fiscal/sync/intervention.js.map +1 -0
- package/dist/cjs/fiscal/sync/lease.js +146 -0
- package/dist/cjs/fiscal/sync/lease.js.map +1 -0
- package/dist/cjs/fiscal/sync/observability.js +244 -0
- package/dist/cjs/fiscal/sync/observability.js.map +1 -0
- package/dist/cjs/fiscal/sync/orchestrator.js +490 -0
- package/dist/cjs/fiscal/sync/orchestrator.js.map +1 -0
- package/dist/cjs/fiscal/sync/policy.js +143 -0
- package/dist/cjs/fiscal/sync/policy.js.map +1 -0
- package/dist/cjs/fiscal/sync/reprocess.js +241 -0
- package/dist/cjs/fiscal/sync/reprocess.js.map +1 -0
- package/dist/cjs/fiscal/sync/retry.js +183 -0
- package/dist/cjs/fiscal/sync/retry.js.map +1 -0
- package/dist/cjs/fiscal/sync/secret-boundary.js +123 -0
- package/dist/cjs/fiscal/sync/secret-boundary.js.map +1 -0
- package/dist/cjs/fiscal/transport/https.js +309 -0
- package/dist/cjs/fiscal/transport/https.js.map +1 -0
- package/dist/cjs/fiscal/transport/synthetic-tls.js +328 -0
- package/dist/cjs/fiscal/transport/synthetic-tls.js.map +1 -0
- package/dist/cjs/fiscal/transport/types.js +93 -0
- package/dist/cjs/fiscal/transport/types.js.map +1 -0
- package/dist/cjs/import/br-documents.js +85 -0
- package/dist/cjs/import/br-documents.js.map +1 -0
- package/dist/cjs/import/clients-import.js +347 -40
- package/dist/cjs/import/clients-import.js.map +1 -1
- package/dist/cjs/index.js +316 -11
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/legal/documents.js +100 -15
- package/dist/cjs/legal/documents.js.map +1 -1
- package/dist/cjs/legal/index.js +3 -1
- package/dist/cjs/legal/index.js.map +1 -1
- package/dist/cjs/legal/types.js.map +1 -1
- package/dist/cjs/onboarding/persistence.js +4 -4
- package/dist/cjs/onboarding/persistence.js.map +1 -1
- package/dist/cjs/pilot/go-live-checklist.js +212 -0
- package/dist/cjs/pilot/go-live-checklist.js.map +1 -0
- package/dist/cjs/pilot/milestone.js +79 -0
- package/dist/cjs/pilot/milestone.js.map +1 -0
- package/dist/cjs/platform/capabilities.js +144 -0
- package/dist/cjs/platform/capabilities.js.map +1 -0
- package/dist/cjs/platform/job-contract.js +411 -0
- package/dist/cjs/platform/job-contract.js.map +1 -0
- package/dist/cjs/rbac/catalog.js +34 -0
- package/dist/cjs/rbac/catalog.js.map +1 -1
- package/dist/cjs/subscription-billing/index.js +2 -1
- package/dist/cjs/subscription-billing/index.js.map +1 -1
- package/dist/cjs/subscription-billing/service.js +153 -77
- package/dist/cjs/subscription-billing/service.js.map +1 -1
- package/dist/cjs/tenancy/features.js +131 -0
- package/dist/cjs/tenancy/features.js.map +1 -0
- package/dist/cjs/tenancy/settings.js +224 -0
- package/dist/cjs/tenancy/settings.js.map +1 -0
- package/dist/cjs/users/repository.js +24 -1
- package/dist/cjs/users/repository.js.map +1 -1
- package/dist/clients/ibge.d.ts +17 -0
- package/dist/clients/ibge.d.ts.map +1 -0
- package/dist/clients/ibge.js +98 -0
- package/dist/clients/ibge.js.map +1 -0
- package/dist/clients/repository.d.ts +154 -0
- package/dist/clients/repository.d.ts.map +1 -1
- package/dist/clients/repository.js +182 -9
- package/dist/clients/repository.js.map +1 -1
- package/dist/collection/branding.d.ts +65 -0
- package/dist/collection/branding.d.ts.map +1 -1
- package/dist/collection/branding.js +186 -0
- package/dist/collection/branding.js.map +1 -1
- package/dist/collection/clearance-store.d.ts +39 -0
- package/dist/collection/clearance-store.d.ts.map +1 -0
- package/dist/collection/clearance-store.js +69 -0
- package/dist/collection/clearance-store.js.map +1 -0
- package/dist/collection/communication-runtime.d.ts +37 -0
- package/dist/collection/communication-runtime.d.ts.map +1 -1
- package/dist/collection/communication-runtime.js +63 -1
- package/dist/collection/communication-runtime.js.map +1 -1
- package/dist/collection/engine.d.ts +23 -0
- package/dist/collection/engine.d.ts.map +1 -1
- package/dist/collection/engine.js +20 -0
- package/dist/collection/engine.js.map +1 -1
- package/dist/collection/policy.d.ts +25 -0
- package/dist/collection/policy.d.ts.map +1 -1
- package/dist/collection/policy.js +27 -0
- package/dist/collection/policy.js.map +1 -1
- package/dist/communication/clearance.d.ts +53 -0
- package/dist/communication/clearance.d.ts.map +1 -0
- package/dist/communication/clearance.js +15 -0
- package/dist/communication/clearance.js.map +1 -0
- package/dist/communication/dry-run-scenario.d.ts.map +1 -1
- package/dist/communication/dry-run-scenario.js +5 -0
- package/dist/communication/dry-run-scenario.js.map +1 -1
- package/dist/communication/engine.d.ts +10 -0
- package/dist/communication/engine.d.ts.map +1 -1
- package/dist/communication/engine.js +12 -0
- package/dist/communication/engine.js.map +1 -1
- package/dist/communication/policy.d.ts +16 -0
- package/dist/communication/policy.d.ts.map +1 -1
- package/dist/communication/policy.js.map +1 -1
- package/dist/competence/business-days.d.ts +51 -0
- package/dist/competence/business-days.d.ts.map +1 -0
- package/dist/competence/business-days.js +145 -0
- package/dist/competence/business-days.js.map +1 -0
- package/dist/competence/catalog-validation.d.ts +86 -0
- package/dist/competence/catalog-validation.d.ts.map +1 -0
- package/dist/competence/catalog-validation.js +168 -0
- package/dist/competence/catalog-validation.js.map +1 -0
- package/dist/competence/obligation-preview.d.ts +103 -0
- package/dist/competence/obligation-preview.d.ts.map +1 -0
- package/dist/competence/obligation-preview.js +184 -0
- package/dist/competence/obligation-preview.js.map +1 -0
- package/dist/competence/repository.d.ts +139 -1
- package/dist/competence/repository.d.ts.map +1 -1
- package/dist/competence/repository.js +325 -17
- package/dist/competence/repository.js.map +1 -1
- package/dist/dashboard/pilot-panel.d.ts +100 -0
- package/dist/dashboard/pilot-panel.d.ts.map +1 -0
- package/dist/dashboard/pilot-panel.js +87 -0
- package/dist/dashboard/pilot-panel.js.map +1 -0
- package/dist/dashboard/repository.d.ts.map +1 -1
- package/dist/dashboard/repository.js +11 -1
- package/dist/dashboard/repository.js.map +1 -1
- package/dist/db/client.d.ts +45 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +16 -11
- package/dist/db/client.js.map +1 -1
- package/dist/db/migrate.d.ts +36 -1
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +199 -32
- package/dist/db/migrate.js.map +1 -1
- package/dist/db/provision-e2e-identity.d.ts +2 -0
- package/dist/db/provision-e2e-identity.d.ts.map +1 -0
- package/dist/db/provision-e2e-identity.js +109 -0
- package/dist/db/provision-e2e-identity.js.map +1 -0
- package/dist/fiscal/applicability.d.ts +73 -0
- package/dist/fiscal/applicability.d.ts.map +1 -0
- package/dist/fiscal/applicability.js +160 -0
- package/dist/fiscal/applicability.js.map +1 -0
- package/dist/fiscal/conditions.d.ts +99 -0
- package/dist/fiscal/conditions.d.ts.map +1 -0
- package/dist/fiscal/conditions.js +190 -0
- package/dist/fiscal/conditions.js.map +1 -0
- package/dist/fiscal/contracts/nfse-adn-contract.d.ts +315 -0
- package/dist/fiscal/contracts/nfse-adn-contract.d.ts.map +1 -0
- package/dist/fiscal/contracts/nfse-adn-contract.js +386 -0
- package/dist/fiscal/contracts/nfse-adn-contract.js.map +1 -0
- package/dist/fiscal/contracts/nfse-adn-endpoint.d.ts +13 -0
- package/dist/fiscal/contracts/nfse-adn-endpoint.d.ts.map +1 -0
- package/dist/fiscal/contracts/nfse-adn-endpoint.js +103 -0
- package/dist/fiscal/contracts/nfse-adn-endpoint.js.map +1 -0
- package/dist/fiscal/contracts/nfse-adn-envelope.d.ts +77 -0
- package/dist/fiscal/contracts/nfse-adn-envelope.d.ts.map +1 -0
- package/dist/fiscal/contracts/nfse-adn-envelope.js +215 -0
- package/dist/fiscal/contracts/nfse-adn-envelope.js.map +1 -0
- package/dist/fiscal/coverage/model.d.ts +86 -0
- package/dist/fiscal/coverage/model.d.ts.map +1 -0
- package/dist/fiscal/coverage/model.js +120 -0
- package/dist/fiscal/coverage/model.js.map +1 -0
- package/dist/fiscal/coverage/requests.d.ts +70 -0
- package/dist/fiscal/coverage/requests.d.ts.map +1 -0
- package/dist/fiscal/coverage/requests.js +95 -0
- package/dist/fiscal/coverage/requests.js.map +1 -0
- package/dist/fiscal/credentials/material.d.ts +81 -0
- package/dist/fiscal/credentials/material.d.ts.map +1 -0
- package/dist/fiscal/credentials/material.js +164 -0
- package/dist/fiscal/credentials/material.js.map +1 -0
- package/dist/fiscal/credentials/readiness.d.ts +92 -0
- package/dist/fiscal/credentials/readiness.d.ts.map +1 -0
- package/dist/fiscal/credentials/readiness.js +159 -0
- package/dist/fiscal/credentials/readiness.js.map +1 -0
- package/dist/fiscal/eligibility.d.ts +106 -0
- package/dist/fiscal/eligibility.d.ts.map +1 -0
- package/dist/fiscal/eligibility.js +194 -0
- package/dist/fiscal/eligibility.js.map +1 -0
- package/dist/fiscal/ingestion/__fixtures__/nfse.d.ts +57 -0
- package/dist/fiscal/ingestion/__fixtures__/nfse.d.ts.map +1 -0
- package/dist/fiscal/ingestion/__fixtures__/nfse.js +121 -0
- package/dist/fiscal/ingestion/__fixtures__/nfse.js.map +1 -0
- package/dist/fiscal/ingestion/adn-transport.d.ts +123 -0
- package/dist/fiscal/ingestion/adn-transport.d.ts.map +1 -0
- package/dist/fiscal/ingestion/adn-transport.js +220 -0
- package/dist/fiscal/ingestion/adn-transport.js.map +1 -0
- package/dist/fiscal/ingestion/direction.d.ts +60 -0
- package/dist/fiscal/ingestion/direction.d.ts.map +1 -0
- package/dist/fiscal/ingestion/direction.js +32 -0
- package/dist/fiscal/ingestion/direction.js.map +1 -0
- package/dist/fiscal/ingestion/nfse-adn.d.ts +91 -0
- package/dist/fiscal/ingestion/nfse-adn.d.ts.map +1 -0
- package/dist/fiscal/ingestion/nfse-adn.js +222 -0
- package/dist/fiscal/ingestion/nfse-adn.js.map +1 -0
- package/dist/fiscal/ingestion/pipeline.d.ts +168 -0
- package/dist/fiscal/ingestion/pipeline.d.ts.map +1 -0
- package/dist/fiscal/ingestion/pipeline.js +699 -0
- package/dist/fiscal/ingestion/pipeline.js.map +1 -0
- package/dist/fiscal/ingestion/provider.d.ts +206 -0
- package/dist/fiscal/ingestion/provider.d.ts.map +1 -0
- package/dist/fiscal/ingestion/provider.js +47 -0
- package/dist/fiscal/ingestion/provider.js.map +1 -0
- package/dist/fiscal/ingestion/repository.d.ts +82 -0
- package/dist/fiscal/ingestion/repository.d.ts.map +1 -0
- package/dist/fiscal/ingestion/repository.js +127 -0
- package/dist/fiscal/ingestion/repository.js.map +1 -0
- package/dist/fiscal/ingestion/synthetic-provider.d.ts +92 -0
- package/dist/fiscal/ingestion/synthetic-provider.d.ts.map +1 -0
- package/dist/fiscal/ingestion/synthetic-provider.js +99 -0
- package/dist/fiscal/ingestion/synthetic-provider.js.map +1 -0
- package/dist/fiscal/ingestion/synthetic-transport-types.d.ts +19 -0
- package/dist/fiscal/ingestion/synthetic-transport-types.d.ts.map +1 -0
- package/dist/fiscal/ingestion/synthetic-transport-types.js +9 -0
- package/dist/fiscal/ingestion/synthetic-transport-types.js.map +1 -0
- package/dist/fiscal/ingestion/synthetic-transport.d.ts +55 -0
- package/dist/fiscal/ingestion/synthetic-transport.d.ts.map +1 -0
- package/dist/fiscal/ingestion/synthetic-transport.js +57 -0
- package/dist/fiscal/ingestion/synthetic-transport.js.map +1 -0
- package/dist/fiscal/ingestion/xml-reader.d.ts +55 -0
- package/dist/fiscal/ingestion/xml-reader.d.ts.map +1 -0
- package/dist/fiscal/ingestion/xml-reader.js +112 -0
- package/dist/fiscal/ingestion/xml-reader.js.map +1 -0
- package/dist/fiscal/repository.d.ts +220 -0
- package/dist/fiscal/repository.d.ts.map +1 -0
- package/dist/fiscal/repository.js +449 -0
- package/dist/fiscal/repository.js.map +1 -0
- package/dist/fiscal/sync/circuit.d.ts +86 -0
- package/dist/fiscal/sync/circuit.d.ts.map +1 -0
- package/dist/fiscal/sync/circuit.js +118 -0
- package/dist/fiscal/sync/circuit.js.map +1 -0
- package/dist/fiscal/sync/intervention.d.ts +81 -0
- package/dist/fiscal/sync/intervention.d.ts.map +1 -0
- package/dist/fiscal/sync/intervention.js +135 -0
- package/dist/fiscal/sync/intervention.js.map +1 -0
- package/dist/fiscal/sync/lease.d.ts +118 -0
- package/dist/fiscal/sync/lease.d.ts.map +1 -0
- package/dist/fiscal/sync/lease.js +139 -0
- package/dist/fiscal/sync/lease.js.map +1 -0
- package/dist/fiscal/sync/observability.d.ts +148 -0
- package/dist/fiscal/sync/observability.d.ts.map +1 -0
- package/dist/fiscal/sync/observability.js +239 -0
- package/dist/fiscal/sync/observability.js.map +1 -0
- package/dist/fiscal/sync/orchestrator.d.ts +132 -0
- package/dist/fiscal/sync/orchestrator.d.ts.map +1 -0
- package/dist/fiscal/sync/orchestrator.js +487 -0
- package/dist/fiscal/sync/orchestrator.js.map +1 -0
- package/dist/fiscal/sync/policy.d.ts +87 -0
- package/dist/fiscal/sync/policy.d.ts.map +1 -0
- package/dist/fiscal/sync/policy.js +136 -0
- package/dist/fiscal/sync/policy.js.map +1 -0
- package/dist/fiscal/sync/reprocess.d.ts +76 -0
- package/dist/fiscal/sync/reprocess.d.ts.map +1 -0
- package/dist/fiscal/sync/reprocess.js +237 -0
- package/dist/fiscal/sync/reprocess.js.map +1 -0
- package/dist/fiscal/sync/retry.d.ts +82 -0
- package/dist/fiscal/sync/retry.d.ts.map +1 -0
- package/dist/fiscal/sync/retry.js +176 -0
- package/dist/fiscal/sync/retry.js.map +1 -0
- package/dist/fiscal/sync/secret-boundary.d.ts +169 -0
- package/dist/fiscal/sync/secret-boundary.d.ts.map +1 -0
- package/dist/fiscal/sync/secret-boundary.js +116 -0
- package/dist/fiscal/sync/secret-boundary.js.map +1 -0
- package/dist/fiscal/transport/https.d.ts +86 -0
- package/dist/fiscal/transport/https.d.ts.map +1 -0
- package/dist/fiscal/transport/https.js +303 -0
- package/dist/fiscal/transport/https.js.map +1 -0
- package/dist/fiscal/transport/synthetic-tls.d.ts +80 -0
- package/dist/fiscal/transport/synthetic-tls.d.ts.map +1 -0
- package/dist/fiscal/transport/synthetic-tls.js +323 -0
- package/dist/fiscal/transport/synthetic-tls.js.map +1 -0
- package/dist/fiscal/transport/types.d.ts +134 -0
- package/dist/fiscal/transport/types.d.ts.map +1 -0
- package/dist/fiscal/transport/types.js +88 -0
- package/dist/fiscal/transport/types.js.map +1 -0
- package/dist/import/br-documents.d.ts +26 -0
- package/dist/import/br-documents.d.ts.map +1 -0
- package/dist/import/br-documents.js +78 -0
- package/dist/import/br-documents.js.map +1 -0
- package/dist/import/clients-import.d.ts +122 -4
- package/dist/import/clients-import.d.ts.map +1 -1
- package/dist/import/clients-import.js +341 -41
- package/dist/import/clients-import.js.map +1 -1
- package/dist/index.d.ts +115 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -7
- package/dist/index.js.map +1 -1
- package/dist/legal/documents.d.ts +12 -1
- package/dist/legal/documents.d.ts.map +1 -1
- package/dist/legal/documents.js +98 -14
- package/dist/legal/documents.js.map +1 -1
- package/dist/legal/index.d.ts +1 -1
- package/dist/legal/index.d.ts.map +1 -1
- package/dist/legal/index.js +1 -1
- package/dist/legal/index.js.map +1 -1
- package/dist/legal/types.d.ts +37 -2
- package/dist/legal/types.d.ts.map +1 -1
- package/dist/legal/types.js.map +1 -1
- package/dist/onboarding/persistence.d.ts +10 -1
- package/dist/onboarding/persistence.d.ts.map +1 -1
- package/dist/onboarding/persistence.js +4 -4
- package/dist/onboarding/persistence.js.map +1 -1
- package/dist/pilot/go-live-checklist.d.ts +41 -0
- package/dist/pilot/go-live-checklist.d.ts.map +1 -0
- package/dist/pilot/go-live-checklist.js +209 -0
- package/dist/pilot/go-live-checklist.js.map +1 -0
- package/dist/pilot/milestone.d.ts +56 -0
- package/dist/pilot/milestone.d.ts.map +1 -0
- package/dist/pilot/milestone.js +74 -0
- package/dist/pilot/milestone.js.map +1 -0
- package/dist/platform/capabilities.d.ts +88 -0
- package/dist/platform/capabilities.d.ts.map +1 -0
- package/dist/platform/capabilities.js +140 -0
- package/dist/platform/capabilities.js.map +1 -0
- package/dist/platform/job-contract.d.ts +232 -0
- package/dist/platform/job-contract.d.ts.map +1 -0
- package/dist/platform/job-contract.js +403 -0
- package/dist/platform/job-contract.js.map +1 -0
- package/dist/rbac/catalog.d.ts.map +1 -1
- package/dist/rbac/catalog.js +34 -0
- package/dist/rbac/catalog.js.map +1 -1
- package/dist/subscription-billing/index.d.ts +6 -5
- package/dist/subscription-billing/index.d.ts.map +1 -1
- package/dist/subscription-billing/index.js +3 -3
- package/dist/subscription-billing/index.js.map +1 -1
- package/dist/subscription-billing/service.d.ts +21 -0
- package/dist/subscription-billing/service.d.ts.map +1 -1
- package/dist/subscription-billing/service.js +90 -15
- package/dist/subscription-billing/service.js.map +1 -1
- package/dist/tenancy/features.d.ts +69 -0
- package/dist/tenancy/features.d.ts.map +1 -0
- package/dist/tenancy/features.js +123 -0
- package/dist/tenancy/features.js.map +1 -0
- package/dist/tenancy/settings.d.ts +63 -0
- package/dist/tenancy/settings.d.ts.map +1 -0
- package/dist/tenancy/settings.js +218 -0
- package/dist/tenancy/settings.js.map +1 -0
- package/dist/users/repository.d.ts +16 -1
- package/dist/users/repository.d.ts.map +1 -1
- package/dist/users/repository.js +24 -1
- package/dist/users/repository.js.map +1 -1
- package/package.json +9 -2
- package/src/db/migrations/0045_role_write_scope.sql +122 -0
- package/src/db/migrations/0046_pilot_activation_model.sql +184 -0
- package/src/db/migrations/0047_recipient_clearance.sql +89 -0
- package/src/db/migrations/0048_item_type_note.sql +16 -0
- package/src/db/migrations/0049_national_fiscal_engine.sql +279 -0
- package/src/db/migrations/0050_fiscal_vocabulary_readonly.sql +29 -0
- package/src/db/migrations/0051_seed_rules_from_catalog.sql +72 -0
- package/src/db/migrations/0052_fiscal_rule_delete_scope.sql +57 -0
- package/src/db/migrations/0053_rule_validity_lower_bound.sql +44 -0
- package/src/db/migrations/0054_tenant_profile_and_rule_source.sql +89 -0
- package/src/db/migrations/0055_job_capability_contract.sql +300 -0
- package/src/db/migrations/0056_fiscal_document_ingestion.sql +337 -0
- package/src/db/migrations/0057_fiscal_pending_event_inbox.sql +228 -0
- package/src/db/migrations/0058_fiscal_operational_engine.sql +516 -0
- package/src/db/migrations/0059_fiscal_contract_version.sql +59 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import { uuidv7 } from '@baruk/shared';
|
|
2
|
+
import { sha256Hex } from '../../storage/checksum.js';
|
|
3
|
+
import { buildStorageKey } from '../../storage/key.js';
|
|
4
|
+
import { appendAudit } from '../../audit/audit.js';
|
|
5
|
+
import { evaluateFiscalEligibility } from '../eligibility.js';
|
|
6
|
+
import { classifyFiscalDirection } from './direction.js';
|
|
7
|
+
import { FiscalProviderError, } from './provider.js';
|
|
8
|
+
/**
|
|
9
|
+
* `synthetic` no motor de elegibilidade e no ambiente do adapter são o mesmo
|
|
10
|
+
* eixo, então a conversão é identidade — declarada explicitamente para que uma
|
|
11
|
+
* divergência futura entre os dois vocabulários quebre a compilação em vez de
|
|
12
|
+
* abrir um fallback silencioso.
|
|
13
|
+
*/
|
|
14
|
+
const ENVIRONMENT_MAP = {
|
|
15
|
+
synthetic: 'synthetic',
|
|
16
|
+
homologation: 'homologation',
|
|
17
|
+
production: 'production',
|
|
18
|
+
};
|
|
19
|
+
export async function ingestFiscalDocuments(tx, deps, request) {
|
|
20
|
+
const now = request.now ?? new Date();
|
|
21
|
+
const env = ENVIRONMENT_MAP[request.environment];
|
|
22
|
+
const { adapter } = deps;
|
|
23
|
+
const blocked = (outcome, detail, errorClass = null, cursorBefore = null) => ({
|
|
24
|
+
outcome,
|
|
25
|
+
documentsFetched: 0,
|
|
26
|
+
documentsNew: 0,
|
|
27
|
+
documentsDuplicate: 0,
|
|
28
|
+
documentsFailed: 0,
|
|
29
|
+
eventsPending: 0,
|
|
30
|
+
eventsReconciled: 0,
|
|
31
|
+
eventsApplied: 0,
|
|
32
|
+
cursorBefore,
|
|
33
|
+
cursorAfter: cursorBefore,
|
|
34
|
+
allItemsPreserved: true,
|
|
35
|
+
errorClass,
|
|
36
|
+
detail,
|
|
37
|
+
documentIds: [],
|
|
38
|
+
});
|
|
39
|
+
// ── 1 · O adapter sabe operar neste ambiente? ────────────────────────────
|
|
40
|
+
if (!adapter.environments.includes(env)) {
|
|
41
|
+
// Não é erro de configuração do escritório: é um adapter sendo usado onde
|
|
42
|
+
// não se declarou capaz. Fechar aqui impede que um adapter sintético seja
|
|
43
|
+
// apontado para produção por engano de fiação.
|
|
44
|
+
return blocked('blocked_contract', `adapter "${adapter.source}" não opera o ambiente "${env}"`);
|
|
45
|
+
}
|
|
46
|
+
// ── 2 · Elegibilidade e credencial, nesta ordem ──────────────────────────
|
|
47
|
+
const eligibility = await evaluateFiscalEligibility(tx, {
|
|
48
|
+
clientId: request.clientId,
|
|
49
|
+
documentType: request.documentType,
|
|
50
|
+
environment: request.environment,
|
|
51
|
+
});
|
|
52
|
+
if (!eligibility.eligible) {
|
|
53
|
+
// Credencial é motivo SEPARADO de elegibilidade, mesmo sendo uma das
|
|
54
|
+
// camadas dela: "falta o certificado" e "este escritório não ligou a
|
|
55
|
+
// automação" pedem ações de pessoas diferentes.
|
|
56
|
+
const onlyCredential = eligibility.reasons.length === 1 && eligibility.reasons[0] === 'credential_not_ready';
|
|
57
|
+
return blocked(onlyCredential ? 'blocked_credential' : 'blocked_eligibility', eligibility.detail);
|
|
58
|
+
}
|
|
59
|
+
// ── 3 · Cursor e espera obrigatória ──────────────────────────────────────
|
|
60
|
+
const cursor = await loadCursor(tx, request, adapter.source, env);
|
|
61
|
+
const cursorBefore = cursor?.last_nsu ?? null;
|
|
62
|
+
if (cursor?.next_attempt_after && cursor.next_attempt_after > now) {
|
|
63
|
+
// A espera é do provedor, não nossa. Insistir aqui é o caminho direto para
|
|
64
|
+
// o 656 — e o bloqueio recai sobre o CNPJ do cliente.
|
|
65
|
+
return blocked('throttled', `espera obrigatória do provedor até ${cursor.next_attempt_after.toISOString()}`, 'throttled', cursorBefore);
|
|
66
|
+
}
|
|
67
|
+
// ── 4 · Busca ────────────────────────────────────────────────────────────
|
|
68
|
+
let fetched;
|
|
69
|
+
try {
|
|
70
|
+
fetched = await adapter.fetchBatch({
|
|
71
|
+
actorDocument: request.actorDocument,
|
|
72
|
+
fromNsu: cursorBefore,
|
|
73
|
+
environment: env,
|
|
74
|
+
batchLimit: adapter.batchLimit,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const provider = err instanceof FiscalProviderError
|
|
79
|
+
? err
|
|
80
|
+
: new FiscalProviderError('unknown', 'falha não classificada do provedor');
|
|
81
|
+
await recordFailure(tx, request, adapter.source, env, provider, now, cursorBefore, adapter.contractVersion ?? null);
|
|
82
|
+
return {
|
|
83
|
+
...blocked('provider_error', describeProviderError(provider), provider.errorClass, cursorBefore),
|
|
84
|
+
outcome: 'provider_error',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// ── 5 · Persistência, item a item ────────────────────────────────────────
|
|
88
|
+
//
|
|
89
|
+
// Um documento ilegível no meio do lote NÃO derruba o lote — mas também não
|
|
90
|
+
// é descartado: ele vai para a caixa de entrada com os bytes originais, e é
|
|
91
|
+
// ISSO que autoriza o cursor a passar por cima dele. Travar a carteira
|
|
92
|
+
// inteira num único XML malformado seria pior; perdê-lo seria pior ainda.
|
|
93
|
+
let novos = 0;
|
|
94
|
+
let duplicados = 0;
|
|
95
|
+
let falhos = 0;
|
|
96
|
+
let pendentes = 0;
|
|
97
|
+
let aplicados = 0;
|
|
98
|
+
const ids = [];
|
|
99
|
+
let ultimoOk = cursorBefore;
|
|
100
|
+
let todosPreservados = true;
|
|
101
|
+
let primeiraFalhaDeGuarda = null;
|
|
102
|
+
for (const doc of fetched.documents) {
|
|
103
|
+
const disposition = await handleItem(tx, deps, request, adapter, env, doc, now);
|
|
104
|
+
switch (disposition.kind) {
|
|
105
|
+
case 'document_created':
|
|
106
|
+
novos += 1;
|
|
107
|
+
ids.push(disposition.id);
|
|
108
|
+
break;
|
|
109
|
+
case 'document_duplicate':
|
|
110
|
+
duplicados += 1;
|
|
111
|
+
break;
|
|
112
|
+
case 'event_applied':
|
|
113
|
+
aplicados += 1;
|
|
114
|
+
ids.push(disposition.id);
|
|
115
|
+
break;
|
|
116
|
+
case 'event_pending':
|
|
117
|
+
pendentes += 1;
|
|
118
|
+
break;
|
|
119
|
+
case 'event_duplicate':
|
|
120
|
+
duplicados += 1;
|
|
121
|
+
break;
|
|
122
|
+
case 'invalid_preserved':
|
|
123
|
+
falhos += 1;
|
|
124
|
+
break;
|
|
125
|
+
case 'unpreserved':
|
|
126
|
+
falhos += 1;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (disposition.kind === 'unpreserved') {
|
|
130
|
+
// O cursor é marca d'água: nada depois deste item pode ser commitado,
|
|
131
|
+
// mesmo que grave com sucesso. Parar aqui é o que impede o buraco.
|
|
132
|
+
todosPreservados = false;
|
|
133
|
+
primeiraFalhaDeGuarda = disposition.reason;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
ultimoOk = doc.nsu;
|
|
137
|
+
}
|
|
138
|
+
// ── 6 · Reconciliação: quem estava esperando já tem alvo? ────────────────
|
|
139
|
+
//
|
|
140
|
+
// Depois do lote, e não durante: um evento pode chegar no NSU 100 e o
|
|
141
|
+
// documento no 101, dentro do MESMO lote. Varrer no fim cobre esse caso e
|
|
142
|
+
// também o evento guardado por uma execução anterior — inclusive uma que
|
|
143
|
+
// morreu antes de reconciliar.
|
|
144
|
+
const reconciliados = await reconcileFiscalPendingEvents(tx, deps, request);
|
|
145
|
+
// ── 7 · COMMIT do cursor, por último ─────────────────────────────────────
|
|
146
|
+
//
|
|
147
|
+
// A posição do servidor só é adotada quando TUDO ficou preservado. Adotá-la
|
|
148
|
+
// com um buraco no meio do lote é exatamente como o NSU de um evento sumia.
|
|
149
|
+
const cursorAfter = todosPreservados ? (fetched.lastNsu ?? ultimoOk) : ultimoOk;
|
|
150
|
+
const exhausted = todosPreservados &&
|
|
151
|
+
(fetched.exhausted || (cursorAfter !== null && cursorAfter === fetched.maxNsu));
|
|
152
|
+
const nextAttempt = exhausted
|
|
153
|
+
? new Date(now.getTime() + adapter.exhaustedBackoffSeconds * 1000)
|
|
154
|
+
: todosPreservados
|
|
155
|
+
? null
|
|
156
|
+
: // Item não preservável é determinístico: repetir de imediato seria o
|
|
157
|
+
// laço que o cStat 656 pune. Recua como falha transitória.
|
|
158
|
+
new Date(now.getTime() + 300_000);
|
|
159
|
+
await saveCursor(tx, request, adapter.source, env, {
|
|
160
|
+
lastNsu: cursorAfter,
|
|
161
|
+
maxNsu: fetched.maxNsu,
|
|
162
|
+
lastSuccessAt: now,
|
|
163
|
+
nextAttemptAfter: nextAttempt,
|
|
164
|
+
preserved: todosPreservados,
|
|
165
|
+
});
|
|
166
|
+
const outcome = !todosPreservados
|
|
167
|
+
? 'failed'
|
|
168
|
+
: fetched.documents.length === 0
|
|
169
|
+
? 'no_documents'
|
|
170
|
+
: 'completed';
|
|
171
|
+
const report = {
|
|
172
|
+
outcome,
|
|
173
|
+
documentsFetched: fetched.documents.length,
|
|
174
|
+
documentsNew: novos,
|
|
175
|
+
documentsDuplicate: duplicados,
|
|
176
|
+
documentsFailed: falhos,
|
|
177
|
+
eventsPending: pendentes,
|
|
178
|
+
eventsReconciled: reconciliados,
|
|
179
|
+
eventsApplied: aplicados,
|
|
180
|
+
cursorBefore,
|
|
181
|
+
cursorAfter,
|
|
182
|
+
allItemsPreserved: todosPreservados,
|
|
183
|
+
errorClass: todosPreservados ? null : 'transient',
|
|
184
|
+
detail: !todosPreservados
|
|
185
|
+
? `cursor retido: item não pôde ser preservado (${primeiraFalhaDeGuarda ?? 'motivo não classificado'})`
|
|
186
|
+
: exhausted
|
|
187
|
+
? 'lote concluído; provedor sem mais documentos'
|
|
188
|
+
: 'lote concluído',
|
|
189
|
+
documentIds: ids,
|
|
190
|
+
};
|
|
191
|
+
await recordRun(tx, request, adapter.source, env, report, now, adapter.contractVersion ?? null);
|
|
192
|
+
await appendAudit(tx, {
|
|
193
|
+
actorType: 'system',
|
|
194
|
+
actorId: request.actorId ?? null,
|
|
195
|
+
action: 'fiscal.documents.synced',
|
|
196
|
+
resourceType: 'client',
|
|
197
|
+
resourceId: request.clientId,
|
|
198
|
+
// Contadores e cursor. Nunca XML, nunca nome de emitente, nunca valor.
|
|
199
|
+
metadata: {
|
|
200
|
+
source: adapter.source,
|
|
201
|
+
environment: env,
|
|
202
|
+
documentType: request.documentType,
|
|
203
|
+
fetched: report.documentsFetched,
|
|
204
|
+
created: report.documentsNew,
|
|
205
|
+
duplicate: report.documentsDuplicate,
|
|
206
|
+
failed: report.documentsFailed,
|
|
207
|
+
pending: report.eventsPending,
|
|
208
|
+
reconciled: report.eventsReconciled,
|
|
209
|
+
cursorBefore,
|
|
210
|
+
cursorAfter,
|
|
211
|
+
allItemsPreserved: report.allItemsPreserved,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
return report;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Um item do lote, do XML bruto ao desfecho.
|
|
218
|
+
*
|
|
219
|
+
* Toda saída deste caminho é um desfecho NOMEADO. Não há `return` silencioso, e
|
|
220
|
+
* é por isso que o cursor pode confiar no resultado: `unpreserved` é a única
|
|
221
|
+
* forma de o item não ter deixado rastro, e ela para o cursor.
|
|
222
|
+
*
|
|
223
|
+
* ## Por que SAVEPOINT, e não só `try`
|
|
224
|
+
*
|
|
225
|
+
* A ingestão inteira roda numa transação. No PostgreSQL, um comando que falha
|
|
226
|
+
* **aborta a transação**: daí em diante todo comando devolve `25P02`, inclusive
|
|
227
|
+
* o INSERT da quarentena. Sem savepoint, o primeiro XML que violasse uma
|
|
228
|
+
* restrição derrubaria o lote inteiro E impediria a preservação do item que o
|
|
229
|
+
* derrubou — o pior dos dois mundos. Com savepoint, só a tentativa daquele item
|
|
230
|
+
* é desfeita, e o que já foi gravado antes dele permanece.
|
|
231
|
+
*/
|
|
232
|
+
async function handleItem(tx, deps, req, adapter, env, doc, now) {
|
|
233
|
+
let parsed = null;
|
|
234
|
+
try {
|
|
235
|
+
parsed = adapter.parse(doc);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
return quarantine(tx, req, adapter.source, env, doc, classOf(err), now);
|
|
239
|
+
}
|
|
240
|
+
const interpretado = parsed;
|
|
241
|
+
try {
|
|
242
|
+
return await tx.savepoint((sp) => interpretado.event
|
|
243
|
+
? persistEvent(sp, deps, req, adapter.source, env, doc, interpretado, now)
|
|
244
|
+
: persistDocument(sp, deps, req, adapter.source, env, doc, interpretado, now));
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
return quarantine(tx, req, adapter.source, env, doc, classOf(err), now, interpretado);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Reprocessa UM item já preservado — sem tocar no cursor.
|
|
252
|
+
*
|
|
253
|
+
* ## Por que o cursor fica de fora, e por que isso é a garantia
|
|
254
|
+
*
|
|
255
|
+
* O item já está guardado; a posição do provedor já passou por ele há muito
|
|
256
|
+
* tempo. Mexer no cursor aqui só poderia FAZER MAL: retrocedê-lo republicaria
|
|
257
|
+
* lotes inteiros, e adiantá-lo pularia itens que ainda não foram vistos. O
|
|
258
|
+
* reprocessamento é sobre bytes que já temos, e o cursor é sobre bytes que ainda
|
|
259
|
+
* não pedimos — são eixos independentes.
|
|
260
|
+
*
|
|
261
|
+
* ## Idempotência vem do banco, como no caminho normal
|
|
262
|
+
*
|
|
263
|
+
* `persistDocument` e `persistEvent` gravam com `ON CONFLICT DO NOTHING` sobre
|
|
264
|
+
* os índices de deduplicação. Reprocessar dez vezes o mesmo item produz uma
|
|
265
|
+
* linha e nove `duplicate` — sem `SELECT` prévio, logo sem janela entre
|
|
266
|
+
* verificar e inserir.
|
|
267
|
+
*/
|
|
268
|
+
export async function reingestPreservedItem(tx, deps, request, doc) {
|
|
269
|
+
const now = request.now ?? new Date();
|
|
270
|
+
const env = ENVIRONMENT_MAP[request.environment];
|
|
271
|
+
const disposition = await handleItem(tx, deps, request, deps.adapter, env, doc, now);
|
|
272
|
+
switch (disposition.kind) {
|
|
273
|
+
case 'document_created':
|
|
274
|
+
return { kind: 'document_created', documentId: disposition.id };
|
|
275
|
+
case 'document_duplicate':
|
|
276
|
+
return { kind: 'document_duplicate' };
|
|
277
|
+
case 'event_applied':
|
|
278
|
+
return { kind: 'event_applied', eventId: disposition.id };
|
|
279
|
+
case 'event_pending':
|
|
280
|
+
return { kind: 'event_pending' };
|
|
281
|
+
case 'event_duplicate':
|
|
282
|
+
return { kind: 'event_duplicate' };
|
|
283
|
+
case 'invalid_preserved':
|
|
284
|
+
case 'unpreserved':
|
|
285
|
+
return { kind: 'still_invalid', reason: disposition.reason };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** Classe do motivo. Nunca a mensagem do provedor, nunca trecho de XML. */
|
|
289
|
+
function classOf(err) {
|
|
290
|
+
if (err instanceof FiscalProviderError) {
|
|
291
|
+
return err.providerCode ? `${err.errorClass}:${err.providerCode}` : err.errorClass;
|
|
292
|
+
}
|
|
293
|
+
return 'unhandled';
|
|
294
|
+
}
|
|
295
|
+
/** Mensagem para a trilha: classe e código, jamais a resposta do provedor. */
|
|
296
|
+
function describeProviderError(err) {
|
|
297
|
+
return err.providerCode ? `${err.errorClass} (${err.providerCode})` : err.errorClass;
|
|
298
|
+
}
|
|
299
|
+
async function loadCursor(tx, req, source, env) {
|
|
300
|
+
const rows = await tx `
|
|
301
|
+
SELECT id, last_nsu, max_nsu, next_attempt_after, consecutive_failures
|
|
302
|
+
FROM fiscal_sync_cursor
|
|
303
|
+
WHERE client_id = ${req.clientId} AND source = ${source} AND environment = ${env}
|
|
304
|
+
`;
|
|
305
|
+
return rows[0];
|
|
306
|
+
}
|
|
307
|
+
async function saveCursor(tx, req, source, env, next) {
|
|
308
|
+
// Lote com item não preservado não zera o contador de falhas nem apaga a
|
|
309
|
+
// classe do erro: quem olha a tela precisa ver que algo ficou para trás.
|
|
310
|
+
const errorClass = next.preserved ? null : 'transient';
|
|
311
|
+
await tx `
|
|
312
|
+
INSERT INTO fiscal_sync_cursor (id, tenant_id, client_id, source, environment,
|
|
313
|
+
last_nsu, max_nsu, last_success_at, next_attempt_after,
|
|
314
|
+
consecutive_failures, last_error_class, updated_at)
|
|
315
|
+
VALUES (${uuidv7()}, ${req.tenantId}, ${req.clientId}, ${source}, ${env},
|
|
316
|
+
${next.lastNsu}, ${next.maxNsu}, ${next.lastSuccessAt}, ${next.nextAttemptAfter},
|
|
317
|
+
${next.preserved ? 0 : 1}, ${errorClass}, now())
|
|
318
|
+
ON CONFLICT (tenant_id, client_id, source, environment)
|
|
319
|
+
DO UPDATE SET last_nsu = EXCLUDED.last_nsu,
|
|
320
|
+
max_nsu = EXCLUDED.max_nsu,
|
|
321
|
+
last_success_at = EXCLUDED.last_success_at,
|
|
322
|
+
next_attempt_after = EXCLUDED.next_attempt_after,
|
|
323
|
+
consecutive_failures = CASE WHEN ${next.preserved} THEN 0
|
|
324
|
+
ELSE fiscal_sync_cursor.consecutive_failures + 1 END,
|
|
325
|
+
last_error_class = ${errorClass},
|
|
326
|
+
updated_at = now()
|
|
327
|
+
`;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Falha do provedor: conta, classifica e — quando a norma manda — recua.
|
|
331
|
+
*
|
|
332
|
+
* O cursor NÃO anda. `throttled` e `auth` recebem espera; `transient` recebe
|
|
333
|
+
* backoff exponencial com teto, porque insistir a cada minuto contra um serviço
|
|
334
|
+
* paralisado é o comportamento que o 656 existe para punir.
|
|
335
|
+
*/
|
|
336
|
+
async function recordFailure(tx, req, source, env, err, now, cursorBefore, contractVersion) {
|
|
337
|
+
const backoff = err.retryAfter ?? computeBackoff(err.errorClass, now);
|
|
338
|
+
await tx `
|
|
339
|
+
INSERT INTO fiscal_sync_cursor (id, tenant_id, client_id, source, environment,
|
|
340
|
+
last_nsu, next_attempt_after, consecutive_failures,
|
|
341
|
+
last_error_class, updated_at)
|
|
342
|
+
VALUES (${uuidv7()}, ${req.tenantId}, ${req.clientId}, ${source}, ${env},
|
|
343
|
+
${cursorBefore}, ${backoff}, 1, ${err.errorClass}, now())
|
|
344
|
+
ON CONFLICT (tenant_id, client_id, source, environment)
|
|
345
|
+
DO UPDATE SET next_attempt_after = ${backoff},
|
|
346
|
+
consecutive_failures = fiscal_sync_cursor.consecutive_failures + 1,
|
|
347
|
+
last_error_class = ${err.errorClass},
|
|
348
|
+
updated_at = now()
|
|
349
|
+
`;
|
|
350
|
+
await tx `
|
|
351
|
+
INSERT INTO fiscal_sync_run (id, tenant_id, client_id, source, environment, outcome,
|
|
352
|
+
cursor_before, cursor_after, error_class, detail,
|
|
353
|
+
contract_version,
|
|
354
|
+
job_name, job_version, lease_id, retryable, retry_after,
|
|
355
|
+
finished_at)
|
|
356
|
+
VALUES (${uuidv7()}, ${req.tenantId}, ${req.clientId}, ${source}, ${env}, 'provider_error',
|
|
357
|
+
${cursorBefore}, ${cursorBefore}, ${err.errorClass},
|
|
358
|
+
${describeProviderError(err)},
|
|
359
|
+
-- A versão do contrato importa MAIS na falha do que no sucesso: um
|
|
360
|
+
-- PROTOCOL_CONTRACT_MISMATCH só é investigável se a linha disser
|
|
361
|
+
-- contra qual contrato a resposta foi conferida.
|
|
362
|
+
${contractVersion},
|
|
363
|
+
${req.job?.name ?? null}, ${req.job?.version ?? null}, ${req.job?.leaseId ?? null},
|
|
364
|
+
-- Classes "auth" e "not_allowed" não são retentáveis: só uma pessoa
|
|
365
|
+
-- as resolve, e a trilha precisa dizer isso sem que alguém
|
|
366
|
+
-- reconstrua a decisão a partir da classe do erro.
|
|
367
|
+
${err.errorClass !== 'auth' && err.errorClass !== 'not_allowed'},
|
|
368
|
+
${backoff},
|
|
369
|
+
now())
|
|
370
|
+
`;
|
|
371
|
+
}
|
|
372
|
+
/** Espera por classe de erro. `auth` é longo porque só uma pessoa o resolve. */
|
|
373
|
+
function computeBackoff(cls, now) {
|
|
374
|
+
const seconds = cls === 'throttled' ? 3600 : cls === 'auth' ? 3600 : cls === 'not_allowed' ? 3600 : 300;
|
|
375
|
+
return new Date(now.getTime() + seconds * 1000);
|
|
376
|
+
}
|
|
377
|
+
async function recordRun(tx, req, source, env, report, now, contractVersion) {
|
|
378
|
+
// Duração MEDIDA, e não zero fixo. O valor constante que estava aqui fazia a
|
|
379
|
+
// coluna existir sem informar nada — pior do que ausente, porque um painel a
|
|
380
|
+
// apresentaria como "todo lote leva 0ms".
|
|
381
|
+
const durationMs = Math.max(0, Date.now() - now.getTime());
|
|
382
|
+
await tx `
|
|
383
|
+
INSERT INTO fiscal_sync_run (id, tenant_id, client_id, source, environment, outcome,
|
|
384
|
+
documents_fetched, documents_new, documents_duplicate,
|
|
385
|
+
documents_failed, events_pending, events_applied,
|
|
386
|
+
events_reconciled, cursor_before, cursor_after, error_class,
|
|
387
|
+
detail, contract_version, job_name, job_version, lease_id,
|
|
388
|
+
started_at, finished_at, duration_ms)
|
|
389
|
+
VALUES (${uuidv7()}, ${req.tenantId}, ${req.clientId}, ${source}, ${env}, ${report.outcome},
|
|
390
|
+
${report.documentsFetched}, ${report.documentsNew}, ${report.documentsDuplicate},
|
|
391
|
+
${report.documentsFailed}, ${report.eventsPending}, ${report.eventsApplied},
|
|
392
|
+
${report.eventsReconciled}, ${report.cursorBefore}, ${report.cursorAfter},
|
|
393
|
+
${report.errorClass}, ${report.detail}, ${contractVersion},
|
|
394
|
+
${req.job?.name ?? null}, ${req.job?.version ?? null}, ${req.job?.leaseId ?? null},
|
|
395
|
+
${now}, now(), ${durationMs})
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Guarda o XML original e devolve o `blob`.
|
|
400
|
+
*
|
|
401
|
+
* O RAW nunca é reescrito: o que vai para o storage são os bytes que o provedor
|
|
402
|
+
* entregou, e o sha256 é deles. É esse arquivo que tem valor probatório — o
|
|
403
|
+
* normalizado é conveniência de leitura, e não substitui o documento.
|
|
404
|
+
*/
|
|
405
|
+
async function persistRaw(tx, deps, req, doc, competenceLabel) {
|
|
406
|
+
const sha256 = sha256Hex(doc.raw);
|
|
407
|
+
if (!deps.storage)
|
|
408
|
+
return null;
|
|
409
|
+
const key = buildStorageKey({
|
|
410
|
+
tenantId: req.tenantId,
|
|
411
|
+
clientId: req.clientId,
|
|
412
|
+
categoryKey: 'fiscal',
|
|
413
|
+
documentId: sha256.slice(0, 32),
|
|
414
|
+
versionNo: 1,
|
|
415
|
+
competenceLabel: competenceLabel ?? undefined,
|
|
416
|
+
});
|
|
417
|
+
// Mesmo conteúdo já guardado = mesmo blob. O sha256 é a identidade, e
|
|
418
|
+
// reenviar bytes idênticos ao storage é desperdício com risco de divergência.
|
|
419
|
+
const existing = await tx `
|
|
420
|
+
SELECT id FROM blob WHERE checksum_sha256 = ${sha256} LIMIT 1
|
|
421
|
+
`;
|
|
422
|
+
if (existing[0])
|
|
423
|
+
return { blobId: existing[0].id, sha256 };
|
|
424
|
+
await deps.storage.put({ key, body: doc.raw, contentType: doc.mimeType });
|
|
425
|
+
const blobId = uuidv7();
|
|
426
|
+
await tx `
|
|
427
|
+
INSERT INTO blob (id, tenant_id, checksum_sha256, size_bytes, mime_type,
|
|
428
|
+
storage_provider, storage_key, av_status)
|
|
429
|
+
VALUES (${blobId}, ${req.tenantId}, ${sha256}, ${doc.raw.byteLength}, ${doc.mimeType},
|
|
430
|
+
${deps.storage.key}, ${key}, 'skipped')
|
|
431
|
+
`;
|
|
432
|
+
return { blobId, sha256 };
|
|
433
|
+
}
|
|
434
|
+
async function persistDocument(tx, deps, req, source, env, doc, parsed, _now) {
|
|
435
|
+
const raw = await persistRaw(tx, deps, req, doc, parsed.competenceLabel ?? null);
|
|
436
|
+
if (parsed.contentKind === 'full' && !raw) {
|
|
437
|
+
// Um documento declarado COMPLETO sem o original guardado é uma afirmação
|
|
438
|
+
// sem lastro — o banco recusa a linha, e recusar aqui devolve um motivo
|
|
439
|
+
// legível em vez de uma violação de CHECK contada como "documento ilegível".
|
|
440
|
+
// Sem storage configurado, a ingestão de documento completo não acontece —
|
|
441
|
+
// mas o item ainda vai para a caixa de entrada, e nada se perde.
|
|
442
|
+
throw new FiscalProviderError('invalid', 'documento completo exige storage configurado para preservar o XML original', 'RAW_STORAGE_REQUIRED');
|
|
443
|
+
}
|
|
444
|
+
// A direção sai do CONFRONTO com o ator consultado, não de um default. Ver
|
|
445
|
+
// `direction.ts`: `issued` fixo trocava receita por despesa no fechamento.
|
|
446
|
+
const veredito = classifyFiscalDirection({
|
|
447
|
+
actorDocument: req.actorDocument,
|
|
448
|
+
issuerDocument: parsed.issuerDocument,
|
|
449
|
+
recipientDocument: parsed.recipientDocument,
|
|
450
|
+
intermediaryDocument: parsed.intermediaryDocument,
|
|
451
|
+
});
|
|
452
|
+
const id = uuidv7();
|
|
453
|
+
const normalized = {
|
|
454
|
+
...parsed.normalized,
|
|
455
|
+
// Como se chegou à direção fica gravado junto: auditar uma classificação
|
|
456
|
+
// não pode exigir reabrir o XML.
|
|
457
|
+
directionBasis: veredito.basis,
|
|
458
|
+
};
|
|
459
|
+
// `ON CONFLICT DO NOTHING` sobre o índice de deduplicação: a idempotência é
|
|
460
|
+
// do BANCO, não de um `SELECT` anterior. Dois workers no mesmo lote não
|
|
461
|
+
// conseguem criar duas notas para o mesmo documento — não há janela entre
|
|
462
|
+
// verificar e inserir porque não há dois momentos.
|
|
463
|
+
const inserted = await tx `
|
|
464
|
+
INSERT INTO fiscal_document (
|
|
465
|
+
id, tenant_id, client_id, document_type, direction, source, source_environment,
|
|
466
|
+
source_nsu, access_key, document_number, series, issuer_document, issuer_name,
|
|
467
|
+
recipient_document, recipient_name, issued_at, competence_label, gross_amount,
|
|
468
|
+
content_kind, raw_blob_id, raw_sha256, normalized, dedupe_key
|
|
469
|
+
) VALUES (
|
|
470
|
+
${id}, ${req.tenantId}, ${req.clientId}, ${parsed.documentType}, ${veredito.direction},
|
|
471
|
+
${source}, ${env}, ${doc.nsu}, ${parsed.accessKey ?? null},
|
|
472
|
+
${parsed.documentNumber ?? null}, ${parsed.series ?? null},
|
|
473
|
+
${parsed.issuerDocument ?? null}, ${parsed.issuerName ?? null},
|
|
474
|
+
${parsed.recipientDocument ?? null}, ${parsed.recipientName ?? null},
|
|
475
|
+
${parsed.issuedAt ?? null}, ${parsed.competenceLabel ?? null},
|
|
476
|
+
${parsed.grossAmount ?? null}, ${parsed.contentKind},
|
|
477
|
+
${raw?.blobId ?? null}, ${raw?.sha256 ?? null},
|
|
478
|
+
${tx.json(normalized)}, ${parsed.dedupeKey}
|
|
479
|
+
)
|
|
480
|
+
ON CONFLICT (tenant_id, client_id, dedupe_key) DO NOTHING
|
|
481
|
+
RETURNING id
|
|
482
|
+
`;
|
|
483
|
+
return inserted[0]
|
|
484
|
+
? { kind: 'document_created', id: inserted[0].id }
|
|
485
|
+
: { kind: 'document_duplicate' };
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Evento sobre um documento.
|
|
489
|
+
*
|
|
490
|
+
* Cancelar não apaga. A nota permanece, com `status='cancelled'` — ela existiu,
|
|
491
|
+
* alguém a contabilizou, e some do painel só se uma pessoa decidir escondê-la.
|
|
492
|
+
*
|
|
493
|
+
* ## O evento cujo documento ainda não chegou
|
|
494
|
+
*
|
|
495
|
+
* O ambiente nacional ordena a distribuição por RECEPÇÃO, não por emissão, e
|
|
496
|
+
* portanto o evento pode legitimamente vir antes. Ele NÃO é duplicata, não é
|
|
497
|
+
* erro e não pode ser ignorado: é informação fiscal válida ainda não
|
|
498
|
+
* reconciliada. Vai para a caixa de entrada com os bytes originais e sai de lá
|
|
499
|
+
* quando o alvo chegar (`reconcileFiscalPendingEvents`).
|
|
500
|
+
*/
|
|
501
|
+
async function persistEvent(tx, deps, req, source, env, doc, parsed, now) {
|
|
502
|
+
const ev = parsed.event;
|
|
503
|
+
// A busca do alvo é por (cliente, chave) — e é essa dupla que impede o evento
|
|
504
|
+
// de um cliente de alcançar o documento homônimo de outro.
|
|
505
|
+
const target = await tx `
|
|
506
|
+
SELECT id FROM fiscal_document
|
|
507
|
+
WHERE client_id = ${req.clientId} AND access_key = ${ev.targetAccessKey}
|
|
508
|
+
LIMIT 1
|
|
509
|
+
`;
|
|
510
|
+
if (!target[0])
|
|
511
|
+
return enqueuePendingEvent(tx, req, source, env, doc, parsed, now);
|
|
512
|
+
const raw = await persistRaw(tx, deps, req, doc, parsed.competenceLabel ?? null);
|
|
513
|
+
const applied = await applyEventToDocument(tx, req.tenantId, {
|
|
514
|
+
documentId: target[0].id,
|
|
515
|
+
eventType: ev.type,
|
|
516
|
+
eventCode: ev.code ?? null,
|
|
517
|
+
occurredAt: ev.occurredAt ?? null,
|
|
518
|
+
sourceNsu: doc.nsu,
|
|
519
|
+
rawBlobId: raw?.blobId ?? null,
|
|
520
|
+
payload: parsed.normalized,
|
|
521
|
+
dedupeKey: parsed.dedupeKey,
|
|
522
|
+
});
|
|
523
|
+
return applied.created
|
|
524
|
+
? { kind: 'event_applied', id: applied.id }
|
|
525
|
+
: // Duplicata DE VERDADE: o evento já está gravado sobre este documento.
|
|
526
|
+
// É o único caso que merece esse nome.
|
|
527
|
+
{ kind: 'event_duplicate' };
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Grava o evento no documento e aplica o efeito de estado.
|
|
531
|
+
*
|
|
532
|
+
* Idempotente pelo índice de deduplicação do banco: reprocessar o mesmo evento
|
|
533
|
+
* não cria segunda linha nem reaplica efeito. Quando o `INSERT` não retorna
|
|
534
|
+
* nada, o id é buscado — a linha existe, e quem chama precisa dela para marcar
|
|
535
|
+
* a caixa de entrada como aplicada.
|
|
536
|
+
*/
|
|
537
|
+
async function applyEventToDocument(tx, tenantId, input) {
|
|
538
|
+
const id = uuidv7();
|
|
539
|
+
const inserted = await tx `
|
|
540
|
+
INSERT INTO fiscal_document_event (
|
|
541
|
+
id, tenant_id, fiscal_document_id, event_type, event_code, occurred_at,
|
|
542
|
+
source_nsu, raw_blob_id, payload, dedupe_key
|
|
543
|
+
) VALUES (
|
|
544
|
+
${id}, ${tenantId}, ${input.documentId}, ${input.eventType}, ${input.eventCode},
|
|
545
|
+
${input.occurredAt}, ${input.sourceNsu}, ${input.rawBlobId},
|
|
546
|
+
${tx.json(input.payload)}, ${input.dedupeKey}
|
|
547
|
+
)
|
|
548
|
+
ON CONFLICT (tenant_id, fiscal_document_id, dedupe_key) DO NOTHING
|
|
549
|
+
RETURNING id
|
|
550
|
+
`;
|
|
551
|
+
if (!inserted[0]) {
|
|
552
|
+
const [existing] = await tx `
|
|
553
|
+
SELECT id FROM fiscal_document_event
|
|
554
|
+
WHERE fiscal_document_id = ${input.documentId} AND dedupe_key = ${input.dedupeKey}
|
|
555
|
+
LIMIT 1
|
|
556
|
+
`;
|
|
557
|
+
return { created: false, id: existing.id };
|
|
558
|
+
}
|
|
559
|
+
if (input.eventType === 'cancellation') {
|
|
560
|
+
await tx `UPDATE fiscal_document SET status = 'cancelled', updated_at = now()
|
|
561
|
+
WHERE id = ${input.documentId}`;
|
|
562
|
+
}
|
|
563
|
+
else if (input.eventType === 'replacement') {
|
|
564
|
+
await tx `UPDATE fiscal_document SET status = 'replaced', updated_at = now()
|
|
565
|
+
WHERE id = ${input.documentId}`;
|
|
566
|
+
}
|
|
567
|
+
return { created: true, id: inserted[0].id };
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Guarda o evento que ainda não tem alvo.
|
|
571
|
+
*
|
|
572
|
+
* Os BYTES vão inteiros para a linha, e não para o storage. Não é economia: é
|
|
573
|
+
* a única forma de a preservação e o avanço do cursor acontecerem na MESMA
|
|
574
|
+
* transação. Com o RAW num objeto externo existiria a janela entre o `PUT` e o
|
|
575
|
+
* commit — e uma janela nesse ponto é exatamente o defeito que esta correção
|
|
576
|
+
* fecha.
|
|
577
|
+
*/
|
|
578
|
+
async function enqueuePendingEvent(tx, req, source, env, doc, parsed, _now) {
|
|
579
|
+
const ev = parsed.event;
|
|
580
|
+
const inserted = await tx `
|
|
581
|
+
INSERT INTO fiscal_ingestion_inbox (
|
|
582
|
+
id, tenant_id, client_id, source, source_environment, source_nsu, state,
|
|
583
|
+
document_type, event_type, event_code, event_sequence, occurred_at,
|
|
584
|
+
target_access_key, raw, raw_sha256, normalized, dedupe_key
|
|
585
|
+
) VALUES (
|
|
586
|
+
${uuidv7()}, ${req.tenantId}, ${req.clientId}, ${source}, ${env}, ${doc.nsu},
|
|
587
|
+
'pending_target', ${parsed.documentType}, ${ev.type}, ${ev.code ?? null},
|
|
588
|
+
${ev.sequence ?? null}, ${ev.occurredAt ?? null}, ${ev.targetAccessKey},
|
|
589
|
+
${Buffer.from(doc.raw)}, ${sha256Hex(doc.raw)},
|
|
590
|
+
${tx.json(parsed.normalized)}, ${parsed.dedupeKey}
|
|
591
|
+
)
|
|
592
|
+
ON CONFLICT (tenant_id, client_id, dedupe_key) DO NOTHING
|
|
593
|
+
RETURNING id
|
|
594
|
+
`;
|
|
595
|
+
// Sem linha nova, o evento JÁ estava guardado (ou já foi aplicado): a
|
|
596
|
+
// deduplicação é do banco, não de memória, e sobrevive a replay e a crash.
|
|
597
|
+
return inserted[0] ? { kind: 'event_pending' } : { kind: 'event_duplicate' };
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Guarda o item que não pôde ser interpretado.
|
|
601
|
+
*
|
|
602
|
+
* `invalid` não é "descartado": os bytes ficam, com o NSU e a classe do motivo,
|
|
603
|
+
* e é isso que permite a recuperação pontual (`consNSU`) sem pedir nada ao
|
|
604
|
+
* provedor. Se nem esta gravação for possível, o item é declarado NÃO
|
|
605
|
+
* preservado e o cursor para — a única saída honesta.
|
|
606
|
+
*/
|
|
607
|
+
async function quarantine(tx, req, source, env, doc, reason, _now, parsed) {
|
|
608
|
+
const sha = sha256Hex(doc.raw);
|
|
609
|
+
try {
|
|
610
|
+
await tx.savepoint((sp) => sp `
|
|
611
|
+
INSERT INTO fiscal_ingestion_inbox (
|
|
612
|
+
id, tenant_id, client_id, source, source_environment, source_nsu, state,
|
|
613
|
+
document_type, event_type, event_code, event_sequence, occurred_at,
|
|
614
|
+
target_access_key, raw, raw_sha256, normalized, reject_reason, dedupe_key
|
|
615
|
+
) VALUES (
|
|
616
|
+
${uuidv7()}, ${req.tenantId}, ${req.clientId}, ${source}, ${env}, ${doc.nsu},
|
|
617
|
+
'invalid', ${parsed?.documentType ?? null}, ${null}, ${null}, ${null}, ${null},
|
|
618
|
+
${null}, ${Buffer.from(doc.raw)}, ${sha}, ${tx.json({})},
|
|
619
|
+
${reason}, ${`raw:${sha}`}
|
|
620
|
+
)
|
|
621
|
+
ON CONFLICT (tenant_id, client_id, dedupe_key) DO NOTHING
|
|
622
|
+
`);
|
|
623
|
+
return { kind: 'invalid_preserved', reason };
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return { kind: 'unpreserved', reason };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Aplica os eventos guardados cujo documento-alvo já existe.
|
|
631
|
+
*
|
|
632
|
+
* ## O `JOIN` é a prova de isolamento
|
|
633
|
+
*
|
|
634
|
+
* A junção é por `(tenant_id, client_id, access_key)`. Um evento do Cliente A
|
|
635
|
+
* não alcança o documento do Cliente B mesmo com a MESMA chave de acesso, e
|
|
636
|
+
* nenhum evento atravessa escritórios — antes da RLS, que fecha por cima.
|
|
637
|
+
*
|
|
638
|
+
* ## A ordem importa
|
|
639
|
+
*
|
|
640
|
+
* Dois eventos sobre a mesma nota (cancelamento e depois análise fiscal, por
|
|
641
|
+
* exemplo) mudam o estado final conforme a sequência. Aplicar na ordem de
|
|
642
|
+
* ocorrência, com o NSU e a sequência do leiaute como desempate, é o que faz o
|
|
643
|
+
* resultado da reconciliação ser igual ao de tê-los recebido em ordem.
|
|
644
|
+
*
|
|
645
|
+
* Exportada porque um varredor futuro precisa dela sem repetir a lógica.
|
|
646
|
+
*/
|
|
647
|
+
export async function reconcileFiscalPendingEvents(tx, _deps, req) {
|
|
648
|
+
const pendentes = await tx `
|
|
649
|
+
SELECT i.id, d.id AS document_id, i.event_type, i.event_code, i.occurred_at,
|
|
650
|
+
i.source_nsu, i.normalized, i.dedupe_key
|
|
651
|
+
FROM fiscal_ingestion_inbox i
|
|
652
|
+
JOIN fiscal_document d
|
|
653
|
+
ON d.tenant_id = i.tenant_id
|
|
654
|
+
AND d.client_id = i.client_id
|
|
655
|
+
AND d.access_key = i.target_access_key
|
|
656
|
+
WHERE i.client_id = ${req.clientId}
|
|
657
|
+
AND i.state = 'pending_target'
|
|
658
|
+
ORDER BY i.occurred_at NULLS LAST,
|
|
659
|
+
CASE WHEN i.source_nsu ~ '^[0-9]+$' THEN i.source_nsu::numeric END NULLS LAST,
|
|
660
|
+
CASE WHEN i.event_sequence ~ '^[0-9]+$' THEN i.event_sequence::numeric END NULLS LAST,
|
|
661
|
+
i.received_at
|
|
662
|
+
`;
|
|
663
|
+
let aplicados = 0;
|
|
664
|
+
for (const p of pendentes) {
|
|
665
|
+
try {
|
|
666
|
+
// Um savepoint por evento: um pendente problemático não pode desfazer a
|
|
667
|
+
// reconciliação dos outros nem abortar o lote que acabou de ser gravado.
|
|
668
|
+
// Ele simplesmente CONTINUA pendente, e a próxima varredura tenta de novo.
|
|
669
|
+
await tx.savepoint(async (sp) => {
|
|
670
|
+
const resultado = await applyEventToDocument(sp, req.tenantId, {
|
|
671
|
+
documentId: p.document_id,
|
|
672
|
+
eventType: p.event_type,
|
|
673
|
+
eventCode: p.event_code,
|
|
674
|
+
occurredAt: p.occurred_at,
|
|
675
|
+
sourceNsu: p.source_nsu,
|
|
676
|
+
// O blob do evento reconciliado fica nulo: os bytes originais
|
|
677
|
+
// continuam na caixa de entrada, que é onde foram preservados.
|
|
678
|
+
// Duplicá-los no storage não acrescentaria garantia nenhuma.
|
|
679
|
+
rawBlobId: null,
|
|
680
|
+
payload: p.normalized,
|
|
681
|
+
dedupeKey: p.dedupe_key,
|
|
682
|
+
});
|
|
683
|
+
// `applied` mesmo quando o evento já existia: o que a caixa registra é
|
|
684
|
+
// que este item saiu da espera, não que ele criou a linha.
|
|
685
|
+
await sp `
|
|
686
|
+
UPDATE fiscal_ingestion_inbox
|
|
687
|
+
SET state = 'applied', applied_at = now(), applied_event_id = ${resultado.id}
|
|
688
|
+
WHERE id = ${p.id}
|
|
689
|
+
`;
|
|
690
|
+
});
|
|
691
|
+
aplicados += 1;
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
// Segue pendente. Nada é perdido, e a caixa de entrada mostra a idade.
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return aplicados;
|
|
698
|
+
}
|
|
699
|
+
//# sourceMappingURL=pipeline.js.map
|