@warpgogol/werkstatt-shared 0.2.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/package.json +352 -0
- package/src/checks/index.ts +24 -0
- package/src/checks/lib/astro-site-url.ts +30 -0
- package/src/checks/lib/i18n.ts +30 -0
- package/src/checks/result-helpers.ts +102 -0
- package/src/checks/suppressions-config.ts +195 -0
- package/src/index.ts +14 -0
- package/src/integration/README.md +38 -0
- package/src/integration/adapters.ts +143 -0
- package/src/integration/crm-buffer.ts +491 -0
- package/src/integration/dispatch.ts +106 -0
- package/src/integration/funnel.ts +379 -0
- package/src/integration/index.ts +56 -0
- package/src/integration/lifecycle.ts +155 -0
- package/src/integration/orchestration.ts +439 -0
- package/src/integration/port-barrel.ts +124 -0
- package/src/integration/port.ts +143 -0
- package/src/integration/qstash.ts +134 -0
- package/src/integration/sharding.ts +133 -0
- package/src/integration/tests/deliver-event.test.ts +52 -0
- package/src/integration/tests/dispatch.test.ts +88 -0
- package/src/integration/tests/funnel-bridge.test.ts +46 -0
- package/src/integration/tests/hub-runtime.test.ts +128 -0
- package/src/integration/tests/lifecycle.test.ts +69 -0
- package/src/integration/tests/qstash.test.ts +83 -0
- package/src/integration/tests/sharding.test.ts +64 -0
- package/src/integration/vitest.config.ts +8 -0
- package/src/integration-adapter-supabase-crm/adapter.ts +437 -0
- package/src/integration-adapter-supabase-crm/client.ts +605 -0
- package/src/integration-adapter-supabase-crm/index.ts +28 -0
- package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +482 -0
- package/src/integration-adapter-supabase-crm/supabase-rest.ts +89 -0
- package/src/integration-adapter-supabase-crm/tenant-registry.ts +334 -0
- package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +83 -0
- package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +409 -0
- package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +288 -0
- package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +80 -0
- package/src/integration-adapter-supabase-crm/vitest.config.ts +8 -0
- package/src/integration-adapter-supabase-crm/worker.ts +223 -0
- package/src/observability/conventions.ts +93 -0
- package/src/observability/env-resolver.ts +57 -0
- package/src/observability/index.ts +57 -0
- package/src/observability/metric-registry.ts +184 -0
- package/src/observability/otlp-converter.ts +125 -0
- package/src/observability/otlp-json.ts +135 -0
- package/src/observability/otlp-transport.ts +68 -0
- package/src/observability/pusher.ts +142 -0
- package/src/observability/redact.ts +27 -0
- package/src/observability/tests/conventions.test.ts +100 -0
- package/src/observability/tests/metric-registry.test.ts +117 -0
- package/src/observability/tests/otlp-converter.pbt.test.ts +77 -0
- package/src/observability/tests/otlp-converter.test.ts +157 -0
- package/src/observability/tests/otlp-json.test.ts +120 -0
- package/src/observability/tests/pusher.test.ts +75 -0
- package/src/observability/tests/redact.pbt.test.ts +71 -0
- package/src/observability/tests/redact.test.ts +37 -0
- package/src/observability/typed-refs.ts +137 -0
- package/src/observability/vitest.config.ts +8 -0
- package/src/ontology/analytics/matomo-binding.yaml +172 -0
- package/src/ontology/analytics/matomo-fleet.registry.yaml +26 -0
- package/src/ontology/analytics/messkanon.yaml +151 -0
- package/src/ontology/archetype-registry.ts +61 -0
- package/src/ontology/archetypes/components/brand-label.yaml +19 -0
- package/src/ontology/archetypes/components/copyright.yaml +19 -0
- package/src/ontology/archetypes/components/currency-aware-price-display.yaml +19 -0
- package/src/ontology/archetypes/components/currency-selector.yaml +19 -0
- package/src/ontology/archetypes/components/footer-promo.yaml +19 -0
- package/src/ontology/archetypes/components/lang-switcher.yaml +19 -0
- package/src/ontology/archetypes/components/layout.yaml +19 -0
- package/src/ontology/archetypes/components/live-photo.yaml +20 -0
- package/src/ontology/archetypes/components/material-credit.yaml +20 -0
- package/src/ontology/archetypes/components/media.yaml +21 -0
- package/src/ontology/archetypes/components/nachweis-card.yaml +34 -0
- package/src/ontology/archetypes/components/nachweis-detail.yaml +43 -0
- package/src/ontology/archetypes/components/nachweis-list.yaml +37 -0
- package/src/ontology/archetypes/components/nachweis-verify.yaml +31 -0
- package/src/ontology/archetypes/components/not-found.yaml +22 -0
- package/src/ontology/archetypes/components/offer-capacity.yaml +21 -0
- package/src/ontology/archetypes/components/passport-header.yaml +19 -0
- package/src/ontology/archetypes/components/passport-provenance.yaml +20 -0
- package/src/ontology/archetypes/components/passport-score-grid.yaml +20 -0
- package/src/ontology/archetypes/components/passport-star-map.yaml +20 -0
- package/src/ontology/archetypes/components/person-profile.yaml +20 -0
- package/src/ontology/archetypes/components/pulsar.yaml +20 -0
- package/src/ontology/archetypes/components/responsive-image.yaml +19 -0
- package/src/ontology/archetypes/components/scroll-to-top.yaml +22 -0
- package/src/ontology/archetypes/components/social-meta.yaml +20 -0
- package/src/ontology/archetypes/components/structured-data.yaml +19 -0
- package/src/ontology/archetypes/index.json +1003 -0
- package/src/ontology/archetypes/index.yaml +821 -0
- package/src/ontology/archetypes/sections/approach.yaml +23 -0
- package/src/ontology/archetypes/sections/article-list.yaml +30 -0
- package/src/ontology/archetypes/sections/audience-cards.yaml +30 -0
- package/src/ontology/archetypes/sections/breadcrumbs.yaml +21 -0
- package/src/ontology/archetypes/sections/chat-widget.yaml +37 -0
- package/src/ontology/archetypes/sections/comparison-cards.yaml +31 -0
- package/src/ontology/archetypes/sections/controlled-responsibility-block.yaml +34 -0
- package/src/ontology/archetypes/sections/credits.yaml +29 -0
- package/src/ontology/archetypes/sections/donation-card.yaml +53 -0
- package/src/ontology/archetypes/sections/donation-use.yaml +23 -0
- package/src/ontology/archetypes/sections/dynamic-status-block.yaml +35 -0
- package/src/ontology/archetypes/sections/faq-list.yaml +26 -0
- package/src/ontology/archetypes/sections/final-cta.yaml +53 -0
- package/src/ontology/archetypes/sections/hero-decision-card.yaml +65 -0
- package/src/ontology/archetypes/sections/hero.yaml +50 -0
- package/src/ontology/archetypes/sections/impact.yaml +23 -0
- package/src/ontology/archetypes/sections/markdown.yaml +43 -0
- package/src/ontology/archetypes/sections/mountain-journey.yaml +43 -0
- package/src/ontology/archetypes/sections/navigation.yaml +20 -0
- package/src/ontology/archetypes/sections/notausgang-block.yaml +41 -0
- package/src/ontology/archetypes/sections/open-source-registry.yaml +26 -0
- package/src/ontology/archetypes/sections/ownership-block.yaml +57 -0
- package/src/ontology/archetypes/sections/people.yaml +48 -0
- package/src/ontology/archetypes/sections/price-card.yaml +50 -0
- package/src/ontology/archetypes/sections/problem.yaml +23 -0
- package/src/ontology/archetypes/sections/send-message.yaml +63 -0
- package/src/ontology/archetypes/sections/service-metadata-block.yaml +40 -0
- package/src/ontology/archetypes/sections/social-proof.yaml +27 -0
- package/src/ontology/archetypes/sections/transparency.yaml +32 -0
- package/src/ontology/archetypes/sections/trust-strip.yaml +42 -0
- package/src/ontology/archetypes/sections/video-section.yaml +27 -0
- package/src/ontology/archetypes/sections/women.yaml +29 -0
- package/src/ontology/biome-token-projection.ts +229 -0
- package/src/ontology/biomes/check-concrete-blueprint.yaml +118 -0
- package/src/ontology/biomes/handwerk-material-warm.yaml +135 -0
- package/src/ontology/biomes/nonprofit-trust.yaml +123 -0
- package/src/ontology/blueprints/offer.yaml +39 -0
- package/src/ontology/blueprints/prompts/local-market-signal.md +13 -0
- package/src/ontology/blueprints/prompts/website-local-narrative.md +24 -0
- package/src/ontology/blueprints/ratgeber.yaml +62 -0
- package/src/ontology/blueprints/website-local.yaml +309 -0
- package/src/ontology/blueprints/website-service.yaml +84 -0
- package/src/ontology/capabilities/lead.submit.yaml +44 -0
- package/src/ontology/cosmic/index.ts +48 -0
- package/src/ontology/cosmic/moon-catalog.ts +173 -0
- package/src/ontology/cosmic/planet-catalog.ts +82 -0
- package/src/ontology/cosmic/star-catalog.ts +337 -0
- package/src/ontology/enums.ts +159 -0
- package/src/ontology/external-surfaces/index.ts +158 -0
- package/src/ontology/external-surfaces/jsonld-types.yaml +135 -0
- package/src/ontology/external-surfaces/sitemap-shape.yaml +6 -0
- package/src/ontology/external-surfaces/url-schema.yaml +95 -0
- package/src/ontology/growth/events/contact-email-click.yaml +21 -0
- package/src/ontology/growth/events/contact-form-submit.yaml +25 -0
- package/src/ontology/growth/events/contact-phone-click.yaml +21 -0
- package/src/ontology/growth/events/contact-route-click.yaml +21 -0
- package/src/ontology/growth/events/contact-submit.yaml +28 -0
- package/src/ontology/growth/events/contact-whatsapp-click.yaml +21 -0
- package/src/ontology/growth/events/cta-click.yaml +30 -0
- package/src/ontology/growth/events/donation-confirmed.yaml +32 -0
- package/src/ontology/growth/events/donation-form-start.yaml +21 -0
- package/src/ontology/growth/events/donation-form-submit.yaml +29 -0
- package/src/ontology/growth/events/donation-intent.yaml +29 -0
- package/src/ontology/growth/events/form-error.yaml +27 -0
- package/src/ontology/growth/events/form-start.yaml +21 -0
- package/src/ontology/growth/events/form-submit.yaml +21 -0
- package/src/ontology/growth/events/outbound-click.yaml +25 -0
- package/src/ontology/growth/events/page-view.yaml +24 -0
- package/src/ontology/growth/events/passport-view.yaml +28 -0
- package/src/ontology/growth/events/star-map-navigate.yaml +30 -0
- package/src/ontology/index.ts +165 -0
- package/src/ontology/manifest.ts +344 -0
- package/src/ontology/operations/index.ts +1 -0
- package/src/ontology/schemas/biome.ts +303 -0
- package/src/ontology/schemas/capability.ts +104 -0
- package/src/ontology/schemas/constellation.ts +114 -0
- package/src/ontology/schemas/dns-records.ts +53 -0
- package/src/ontology/schemas/index.ts +95 -0
- package/src/ontology/schemas/manifest-resolver.ts +123 -0
- package/src/ontology/schemas/page-entry.ts +213 -0
- package/src/ontology/schemas/section-archetype.ts +109 -0
- package/src/ontology/schemas/site-family.ts +98 -0
- package/src/ontology/schemas/system/growth.ts +80 -0
- package/src/ontology/schemas/system/integrations.ts +143 -0
- package/src/ontology/schemas/system/manifest.ts +492 -0
- package/src/ontology/schemas/system/page-output.ts +99 -0
- package/src/ontology/schemas/system/release.ts +58 -0
- package/src/ontology/schemas/system/text.ts +58 -0
- package/src/ontology/schemas/system.ts +53 -0
- package/src/ontology/shared-section-props/body-fragments.ts +182 -0
- package/src/ontology/shared-section-props/catalog.ts +195 -0
- package/src/ontology/shared-section-props/common.ts +178 -0
- package/src/ontology/shared-section-props/index.ts +34 -0
- package/src/ontology/shared-section-props/visual-header.ts +163 -0
- package/src/ontology/site-families/charity-donation-trust/cultural-rules.yaml +6 -0
- package/src/ontology/site-families/charity-donation-trust/family.yaml +44 -0
- package/src/ontology/site-families/charity-donation-trust/linguistic-rules.yaml +6 -0
- package/src/ontology/site-families/check-quality-operator/cultural-rules.yaml +6 -0
- package/src/ontology/site-families/check-quality-operator/family.yaml +40 -0
- package/src/ontology/site-families/check-quality-operator/linguistic-rules.yaml +6 -0
- package/src/ontology/site-families/handwerk-trust-engineering/cultural-rules.yaml +6 -0
- package/src/ontology/site-families/handwerk-trust-engineering/family.yaml +53 -0
- package/src/ontology/site-families/handwerk-trust-engineering/linguistic-rules.yaml +6 -0
- package/src/ontology/tests/catalogs.pbt.test.ts +89 -0
- package/src/ontology/tests/enums.test.ts +203 -0
- package/src/ontology/tests/sternsystem-owner.test.ts +63 -0
- package/src/ontology/vitest.config.ts +8 -0
- package/src/passport/dht-sign.test.ts +99 -0
- package/src/passport/dht-sign.ts +73 -0
- package/src/passport/identity-sign.test.ts +134 -0
- package/src/passport/identity-sign.ts +85 -0
- package/src/passport/index.ts +53 -0
- package/src/passport/key-rotate.ts +133 -0
- package/src/passport/schema.ts +198 -0
- package/src/passport/sign.ts +225 -0
- package/src/passport/tests/schema.pbt.test.ts +93 -0
- package/src/passport/tests/schema.test.ts +222 -0
- package/src/passport/vitest.config.ts +8 -0
- package/src/share/agent/api-catalog.ts +77 -0
- package/src/share/agent/capability.ts +68 -0
- package/src/share/agent/dns-aid.test.ts +74 -0
- package/src/share/agent/dns-aid.ts +53 -0
- package/src/share/agent/fleet-catalog.ts +149 -0
- package/src/share/agent/index.ts +29 -0
- package/src/share/agent/knowledge.ts +96 -0
- package/src/share/agent/manifest.ts +126 -0
- package/src/share/agent/mcp-card.ts +63 -0
- package/src/share/agent/omit-empty.ts +77 -0
- package/src/share/agent/openapi.ts +157 -0
- package/src/share/agent/proof.ts +61 -0
- package/src/share/attribution-display.ts +86 -0
- package/src/share/check-hints.ts +38 -0
- package/src/share/content/entity-id.ts +65 -0
- package/src/share/content/index.ts +16 -0
- package/src/share/content/merge.ts +86 -0
- package/src/share/content/resolve-field-path.ts +39 -0
- package/src/share/content/substitute-deep.ts +47 -0
- package/src/share/content-discipline/index.ts +14 -0
- package/src/share/content-discipline/parsers.ts +66 -0
- package/src/share/content-discipline/types.ts +118 -0
- package/src/share/content-reference.ts +333 -0
- package/src/share/counter-utils.ts +75 -0
- package/src/share/css-value-normalize.ts +48 -0
- package/src/share/dev-props-validator.ts +131 -0
- package/src/share/entitlement.ts +172 -0
- package/src/share/env.d.ts +73 -0
- package/src/share/feature-policy.ts +367 -0
- package/src/share/formula-eval.ts +364 -0
- package/src/share/fs/index.ts +96 -0
- package/src/share/i18n/localization.ts +57 -0
- package/src/share/image-provider.ts +314 -0
- package/src/share/index.ts +86 -0
- package/src/share/knowledge/claim.ts +129 -0
- package/src/share/knowledge/derivation.ts +55 -0
- package/src/share/knowledge/freshness.ts +127 -0
- package/src/share/knowledge/index.ts +23 -0
- package/src/share/knowledge/ledger.ts +222 -0
- package/src/share/knowledge/plan.ts +122 -0
- package/src/share/knowledge/source.ts +170 -0
- package/src/share/legal/translation-policy.ts +133 -0
- package/src/share/material-credits.ts +181 -0
- package/src/share/middleware/language-redirect.ts +49 -0
- package/src/share/offer-capacity.ts +149 -0
- package/src/share/onboarding-yaml/index.ts +162 -0
- package/src/share/page.ts +351 -0
- package/src/share/redirects.test.ts +62 -0
- package/src/share/redirects.ts +54 -0
- package/src/share/rfc0042-utils.ts +95 -0
- package/src/share/runtime-context.ts +70 -0
- package/src/share/schemas/claim-records.ts +69 -0
- package/src/share/schemas/claims.ts +150 -0
- package/src/share/schemas/effects.ts +212 -0
- package/src/share/schemas/features.ts +147 -0
- package/src/share/schemas/horizontal-align.ts +22 -0
- package/src/share/schemas/icon-color.ts +27 -0
- package/src/share/schemas/index.ts +43 -0
- package/src/share/schemas/live-photo.ts +53 -0
- package/src/share/schemas/material-credit.ts +292 -0
- package/src/share/schemas/media.ts +220 -0
- package/src/share/schemas/navigation.ts +74 -0
- package/src/share/schemas/page-base.ts +33 -0
- package/src/share/schemas/participant.ts +247 -0
- package/src/share/schemas/person.ts +24 -0
- package/src/share/schemas/print.ts +93 -0
- package/src/share/schemas/section-background.ts +106 -0
- package/src/share/schemas/section-body.ts +138 -0
- package/src/share/schemas/section-cards.ts +37 -0
- package/src/share/schemas/section-cta.ts +74 -0
- package/src/share/schemas/section-header.ts +77 -0
- package/src/share/schemas/section-image.ts +77 -0
- package/src/share/schemas/section-motion.ts +52 -0
- package/src/share/schemas/section-shell.ts +63 -0
- package/src/share/schemas/section-stats.ts +37 -0
- package/src/share/schemas/site-background.ts +78 -0
- package/src/share/schemas/standard-list-item.ts +38 -0
- package/src/share/scripts/external-links.ts +45 -0
- package/src/share/scripts/gsap-counter.ts +168 -0
- package/src/share/scripts/gsap-mountain-journey.ts +236 -0
- package/src/share/scripts/gsap-parallax.ts +50 -0
- package/src/share/scripts/gsap-reveal.ts +65 -0
- package/src/share/scripts/gsap-stagger.ts +54 -0
- package/src/share/scripts/index.ts +25 -0
- package/src/share/scripts/inline-number-animation.ts +120 -0
- package/src/share/scripts/lenis.ts +109 -0
- package/src/share/scripts/live-photos.ts +250 -0
- package/src/share/scripts/lordicon.ts +94 -0
- package/src/share/scripts/orchestrator.ts +138 -0
- package/src/share/scripts/scheduler.ts +53 -0
- package/src/share/scripts/video-player.ts +116 -0
- package/src/share/semantic/ai.ts +140 -0
- package/src/share/semantic/block-extraction.ts +52 -0
- package/src/share/semantic/block-extractors/index.ts +502 -0
- package/src/share/semantic/breadcrumbs.ts +120 -0
- package/src/share/semantic/build-page.ts +309 -0
- package/src/share/semantic/business-projection.ts +278 -0
- package/src/share/semantic/derived-prices-loader.ts +36 -0
- package/src/share/semantic/extract.ts +118 -0
- package/src/share/semantic/feed.ts +124 -0
- package/src/share/semantic/ids.ts +76 -0
- package/src/share/semantic/image-sitemap.ts +153 -0
- package/src/share/semantic/index.ts +36 -0
- package/src/share/semantic/jsonld/article.ts +47 -0
- package/src/share/semantic/jsonld/breadcrumb.ts +47 -0
- package/src/share/semantic/jsonld/collection-list.ts +30 -0
- package/src/share/semantic/jsonld/context.ts +46 -0
- package/src/share/semantic/jsonld/faq.ts +49 -0
- package/src/share/semantic/jsonld/initiative.ts +53 -0
- package/src/share/semantic/jsonld/organization.ts +101 -0
- package/src/share/semantic/jsonld/person.ts +71 -0
- package/src/share/semantic/jsonld/service.ts +80 -0
- package/src/share/semantic/jsonld/shared.ts +31 -0
- package/src/share/semantic/jsonld/types.ts +18 -0
- package/src/share/semantic/jsonld/webpage.ts +98 -0
- package/src/share/semantic/jsonld/website.ts +27 -0
- package/src/share/semantic/jsonld.ts +75 -0
- package/src/share/semantic/llms-policy.ts +74 -0
- package/src/share/semantic/llms.ts +394 -0
- package/src/share/semantic/markdown-hygiene.ts +68 -0
- package/src/share/semantic/markdown-twin-provenance.ts +242 -0
- package/src/share/semantic/models.ts +402 -0
- package/src/share/semantic/organization-profile.ts +160 -0
- package/src/share/semantic/output-projection.ts +131 -0
- package/src/share/semantic/page-builders/markdown-page.ts +84 -0
- package/src/share/semantic/page-markdown.ts +234 -0
- package/src/share/semantic/page-utils.ts +125 -0
- package/src/share/semantic/price-marker-resolver.ts +92 -0
- package/src/share/semantic/robots.ts +127 -0
- package/src/share/semantic/update-stamp.ts +136 -0
- package/src/share/shared-context.ts +177 -0
- package/src/share/string-utils.ts +19 -0
- package/src/share/tests/api-catalog.test.ts +97 -0
- package/src/share/tests/breadcrumbs.test.ts +126 -0
- package/src/share/tests/build-page-price-markers.test.ts +160 -0
- package/src/share/tests/capability.test.ts +107 -0
- package/src/share/tests/claim.test.ts +90 -0
- package/src/share/tests/content-ref-index.test.ts +328 -0
- package/src/share/tests/create-dev-props-validator.test.ts +38 -0
- package/src/share/tests/derivation.test.ts +39 -0
- package/src/share/tests/dev-props-validator.test.ts +88 -0
- package/src/share/tests/fleet-catalog.test.ts +134 -0
- package/src/share/tests/formula-eval.test.ts +619 -0
- package/src/share/tests/freshness.test.ts +60 -0
- package/src/share/tests/hero-section-numbering-parity.test.ts +51 -0
- package/src/share/tests/image-sitemap.test.ts +101 -0
- package/src/share/tests/jsonld-service.test.ts +282 -0
- package/src/share/tests/knowledge.test.ts +86 -0
- package/src/share/tests/ledger.test.ts +125 -0
- package/src/share/tests/llms-0789.test.ts +63 -0
- package/src/share/tests/manifest.test.ts +100 -0
- package/src/share/tests/markdown-twin-provenance.test.ts +108 -0
- package/src/share/tests/mcp-card.test.ts +57 -0
- package/src/share/tests/onboarding-yaml.test.ts +111 -0
- package/src/share/tests/ontology-role-by-cosmic-name.test.ts +28 -0
- package/src/share/tests/openapi.test.ts +116 -0
- package/src/share/tests/organization-jsonld.test.ts +74 -0
- package/src/share/tests/output-projection.test.ts +61 -0
- package/src/share/tests/plan.test.ts +97 -0
- package/src/share/tests/price-marker-resolver.test.ts +141 -0
- package/src/share/tests/proof.test.ts +72 -0
- package/src/share/tests/resolve-field-path.test.ts +52 -0
- package/src/share/tests/source.test.ts +81 -0
- package/src/share/tests/substitute-deep.test.ts +65 -0
- package/src/share/tests/text-normalize.pbt.test.ts +143 -0
- package/src/share/tests/text-normalize.test.ts +285 -0
- package/src/share/tests/twin-path.test.ts +43 -0
- package/src/share/text-normalize.ts +548 -0
- package/src/share/text-position.ts +19 -0
- package/src/share/types/hls-js-light.d.ts +16 -0
- package/src/share/visibility.ts +167 -0
- package/src/share/vitest.config.ts +8 -0
- package/src/share/wrap-inline-numbers.ts +139 -0
- package/src/surface/blueprint-schema.ts +314 -0
- package/src/surface/blueprint-types.ts +256 -0
- package/src/surface/blueprint.ts +300 -0
- package/src/surface/breaker.ts +57 -0
- package/src/surface/decision-composer.ts +156 -0
- package/src/surface/eligibility.ts +372 -0
- package/src/surface/evidence-records.ts +97 -0
- package/src/surface/fleet.ts +60 -0
- package/src/surface/geo.ts +170 -0
- package/src/surface/governance.ts +146 -0
- package/src/surface/index.ts +216 -0
- package/src/surface/io/index.ts +18 -0
- package/src/surface/io/surface-module-context-io.ts +60 -0
- package/src/surface/io/visibility-outcomes-io.ts +46 -0
- package/src/surface/module-context.ts +120 -0
- package/src/surface/substance.ts +161 -0
- package/src/surface/tests/eligibility.pbt.test.ts +137 -0
- package/src/surface/tests/eligibility.test.ts +302 -0
- package/src/surface/types.ts +289 -0
- package/src/surface/visibility.ts +57 -0
- package/src/surface/vitest.config.ts +8 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>RFC-0176 DestinationAdapter implementation for the Supabase CRM buffer (Lagebild MVP).
|
|
4
|
+
Receives an IntegrationEvent from the delivery callback, writes/updates the contact and deal
|
|
5
|
+
in the buffer, appends a stage transition, and queues an outbox task for the async Pipedrive
|
|
6
|
+
sync worker. Does NOT call Pipedrive directly — that is the sync worker's job. Follows the
|
|
7
|
+
same pattern as pipedriveDestinationAdapter in adapters.ts.</purpose>
|
|
8
|
+
<non-goals>
|
|
9
|
+
<item>Do not call Pipedrive — the sync worker reads the outbox and does that.</item>
|
|
10
|
+
<item>Do not import astro:env — secrets arrive via the IntegrationSecrets bag.</item>
|
|
11
|
+
</non-goals>
|
|
12
|
+
</MODULE_CONTRACT>
|
|
13
|
+
<CHANGE_SUMMARY>
|
|
14
|
+
<item>Lagebild MVP: initial buffer DestinationAdapter.</item>
|
|
15
|
+
<item>RFC-0190: resolve-or-create the target Organization and link the deal.</item>
|
|
16
|
+
<item>RFC-0188 Phase 4: persist canonical funnel_stage + offer snapshot + funnel/consent rows;
|
|
17
|
+
extract the injectable persistEventToBuffer core.</item>
|
|
18
|
+
</CHANGE_SUMMARY>
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
DestinationAdapter,
|
|
23
|
+
IntegrationEvent,
|
|
24
|
+
IntegrationSecrets,
|
|
25
|
+
LifecycleEventPayload,
|
|
26
|
+
VisitorBuyerType,
|
|
27
|
+
VisitorFunnelEventKind,
|
|
28
|
+
VisitorFunnelEventPayload,
|
|
29
|
+
VisitorFunnelStage,
|
|
30
|
+
} from "@warpgogol/werkstatt-shared/integration";
|
|
31
|
+
import {
|
|
32
|
+
FUNNEL_VERSION,
|
|
33
|
+
VISITOR_FUNNEL_EVENT_KINDS,
|
|
34
|
+
bridgeFunnelStage,
|
|
35
|
+
isFunnelStage,
|
|
36
|
+
} from "@warpgogol/werkstatt-shared/integration";
|
|
37
|
+
import type {
|
|
38
|
+
BufferDealStage,
|
|
39
|
+
BufferUpsertResult,
|
|
40
|
+
CrmBufferWriter,
|
|
41
|
+
} from "@warpgogol/werkstatt-shared/integration/crm-buffer";
|
|
42
|
+
import { BUFFER_DEAL_STAGES } from "@warpgogol/werkstatt-shared/integration/crm-buffer";
|
|
43
|
+
import { createSupabaseCrmBufferClient } from "./client.ts";
|
|
44
|
+
|
|
45
|
+
/** Map an IntegrationEvent.source onto the buffer's append-only actor enum. */
|
|
46
|
+
function mapActor(source: string): "uchat" | "stripe" | "operator" | "send-message" {
|
|
47
|
+
if (source === "stripe" || source === "operator" || source === "send-message") return source;
|
|
48
|
+
return "uchat";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True when `value` is a typed funnel event kind (RFC-0188). */
|
|
52
|
+
function isFunnelEventKind(value: unknown): value is VisitorFunnelEventKind {
|
|
53
|
+
return (
|
|
54
|
+
typeof value === "string" && (VISITOR_FUNNEL_EVENT_KINDS as readonly string[]).includes(value)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* RFC-0188: extract the typed funnel payload from the normalized event. Returns null when the
|
|
60
|
+
* event is not a funnel event (no recognized `eventKind`) — e.g. a plain contact-form lead —
|
|
61
|
+
* so those keep the back-compatible generic path.
|
|
62
|
+
*/
|
|
63
|
+
function extractFunnelPayload(event: IntegrationEvent): VisitorFunnelEventPayload | null {
|
|
64
|
+
const p = event.payload;
|
|
65
|
+
if (!isFunnelEventKind(p.eventKind)) return null;
|
|
66
|
+
const stage = typeof p.stage === "string" && isFunnelStage(p.stage) ? p.stage : undefined;
|
|
67
|
+
const previousStage =
|
|
68
|
+
typeof p.previousStage === "string" && isFunnelStage(p.previousStage)
|
|
69
|
+
? p.previousStage
|
|
70
|
+
: undefined;
|
|
71
|
+
return {
|
|
72
|
+
funnelVersion: typeof p.funnelVersion === "string" ? p.funnelVersion : FUNNEL_VERSION,
|
|
73
|
+
eventKind: p.eventKind,
|
|
74
|
+
stage,
|
|
75
|
+
previousStage,
|
|
76
|
+
intent: p.intent as VisitorFunnelEventPayload["intent"],
|
|
77
|
+
locale: event.locale,
|
|
78
|
+
contact: event.contact,
|
|
79
|
+
organization: p.organization as VisitorFunnelEventPayload["organization"],
|
|
80
|
+
qualification: p.qualification as VisitorFunnelEventPayload["qualification"],
|
|
81
|
+
offer: p.offer as VisitorFunnelEventPayload["offer"],
|
|
82
|
+
legal: p.legal as VisitorFunnelEventPayload["legal"],
|
|
83
|
+
changeRequest: p.changeRequest as VisitorFunnelEventPayload["changeRequest"],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the deal's stage. When the event carries a canonical funnel stage (RFC-0188), the
|
|
89
|
+
* precise `funnel` stage is kept and the generic `stage` is its bridge (RFC-0188 §bridge).
|
|
90
|
+
* Otherwise falls back to the generic heuristic for non-funnel leads.
|
|
91
|
+
*/
|
|
92
|
+
function resolveStages(
|
|
93
|
+
event: IntegrationEvent,
|
|
94
|
+
funnel: VisitorFunnelEventPayload | null,
|
|
95
|
+
): { funnelStage?: VisitorFunnelStage; generic: BufferDealStage } {
|
|
96
|
+
if (funnel?.stage) {
|
|
97
|
+
return { funnelStage: funnel.stage, generic: bridgeFunnelStage(funnel.stage) };
|
|
98
|
+
}
|
|
99
|
+
return { generic: resolveStage(event) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Required server secret names for the Supabase buffer adapter.
|
|
104
|
+
* Read via `astro:env/server` in the delivery callback; injected into the secrets bag.
|
|
105
|
+
*
|
|
106
|
+
* - SUPABASE_BUFFER_URL — Supabase project URL (e.g. https://xyz.supabase.co)
|
|
107
|
+
* - SUPABASE_BUFFER_SERVICE_KEY — Service role key (server-only)
|
|
108
|
+
* - SUPABASE_BUFFER_TENANT_ID — Client UUID for RLS isolation (warpgogol-com pilot)
|
|
109
|
+
*/
|
|
110
|
+
export const SUPABASE_BUFFER_SECRETS = [
|
|
111
|
+
"SUPABASE_BUFFER_URL",
|
|
112
|
+
"SUPABASE_BUFFER_SERVICE_KEY",
|
|
113
|
+
"SUPABASE_BUFFER_TENANT_ID",
|
|
114
|
+
] as const;
|
|
115
|
+
|
|
116
|
+
/** Resolve the deal stage from an IntegrationEvent payload (UChat funnel stage). */
|
|
117
|
+
function resolveStage(event: IntegrationEvent): BufferDealStage {
|
|
118
|
+
const raw = event.payload.stage ?? event.payload.deal_stage ?? event.payload.funnel_stage;
|
|
119
|
+
if (typeof raw === "string" && (BUFFER_DEAL_STAGES as readonly string[]).includes(raw)) {
|
|
120
|
+
return raw as BufferDealStage;
|
|
121
|
+
}
|
|
122
|
+
// Default: new contacts start at 'new'; messages keep the existing stage.
|
|
123
|
+
return event.kind === "lead" ? "new" : "contacted";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* RFC-0190: resolve-or-create the target Organization from the funnel event's
|
|
128
|
+
* `organization` payload ({ name, legalName?, industry?, region? }). Returns the buffer
|
|
129
|
+
* row, or undefined when the event carries no organization (e.g. a pure free question) —
|
|
130
|
+
* in which case the deal keeps organization_id null (back-compatible).
|
|
131
|
+
*/
|
|
132
|
+
async function resolveOrganization(
|
|
133
|
+
client: CrmBufferWriter,
|
|
134
|
+
tenantId: string,
|
|
135
|
+
event: IntegrationEvent,
|
|
136
|
+
): Promise<BufferUpsertResult | undefined> {
|
|
137
|
+
const org = event.payload.organization as
|
|
138
|
+
| { name?: string; legalName?: string; legal_name?: string; industry?: string; region?: string }
|
|
139
|
+
| undefined;
|
|
140
|
+
const name = typeof org?.name === "string" ? org.name.trim() : "";
|
|
141
|
+
if (!name) return undefined;
|
|
142
|
+
return client.upsertOrganization(tenantId, {
|
|
143
|
+
name,
|
|
144
|
+
legal_name: org?.legalName ?? org?.legal_name,
|
|
145
|
+
industry: org?.industry,
|
|
146
|
+
region: org?.region,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Derive a deal title from the event (UChat flow name or fallback). */
|
|
151
|
+
function resolveDealTitle(event: IntegrationEvent): string {
|
|
152
|
+
const flowName = event.payload.flow_name ?? event.payload.conversation_name;
|
|
153
|
+
if (typeof flowName === "string" && flowName.trim()) return flowName.trim();
|
|
154
|
+
return `${event.source}: ${event.contact?.name ?? event.contact?.email ?? event.eventId.slice(0, 8)}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* RFC-0176 DestinationAdapter: writes the event into the Supabase CRM buffer
|
|
159
|
+
* and queues outbox tasks for async Pipedrive sync.
|
|
160
|
+
*
|
|
161
|
+
* kind = "crm", vendor = "supabase-buffer"
|
|
162
|
+
*
|
|
163
|
+
* Registered alongside pipedriveDestinationAdapter in DESTINATION_ADAPTERS.
|
|
164
|
+
* When BOTH this adapter AND the direct Pipedrive adapter are active for the
|
|
165
|
+
* same site, integration.config.validate reports "multiple-active-executors"
|
|
166
|
+
* for (kind=crm). Choose one: use this adapter (buffered) OR the direct
|
|
167
|
+
* Pipedrive adapter (immediate), never both.
|
|
168
|
+
*/
|
|
169
|
+
export const supabaseBufferDestinationAdapter: DestinationAdapter = {
|
|
170
|
+
kind: "crm",
|
|
171
|
+
vendor: "supabase-buffer",
|
|
172
|
+
requiredSecrets: SUPABASE_BUFFER_SECRETS,
|
|
173
|
+
|
|
174
|
+
async route(
|
|
175
|
+
event: IntegrationEvent,
|
|
176
|
+
secrets: IntegrationSecrets,
|
|
177
|
+
): Promise<{ id: string } | null> {
|
|
178
|
+
const url = secrets.SUPABASE_BUFFER_URL;
|
|
179
|
+
const serviceKey = secrets.SUPABASE_BUFFER_SERVICE_KEY;
|
|
180
|
+
const tenantId = secrets.SUPABASE_BUFFER_TENANT_ID;
|
|
181
|
+
|
|
182
|
+
if (!url || !serviceKey || !tenantId) {
|
|
183
|
+
throw new Error("supabase-buffer: missing credentials");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const client = createSupabaseCrmBufferClient({ url, serviceKey, tenantId });
|
|
187
|
+
return persistEventToBuffer(client, tenantId, event);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* RFC-0191 / spec §08: persist a Stripe lifecycle event. Resolves the Organization
|
|
193
|
+
* (metadata `lagebild_organization_id` → fallback by Stripe Customer) and mirrors an
|
|
194
|
+
* append-only invoice row for invoice events (idempotent by stripe_invoice_id). Returns null
|
|
195
|
+
* when the Customer maps to no Organization yet (soft no-op) or for lifecycle kinds whose
|
|
196
|
+
* persistence is a later bounded delta (subscription upsert, MRR, change-balance reset).
|
|
197
|
+
*/
|
|
198
|
+
async function persistLifecycleEvent(
|
|
199
|
+
client: CrmBufferWriter,
|
|
200
|
+
tenantId: string,
|
|
201
|
+
lc: LifecycleEventPayload,
|
|
202
|
+
occurredAt: string,
|
|
203
|
+
): Promise<{ id: string } | null> {
|
|
204
|
+
const orgId =
|
|
205
|
+
lc.lagebildOrganizationId ??
|
|
206
|
+
(await client.findOrganizationByStripeCustomer(tenantId, lc.stripeCustomerId))?.id;
|
|
207
|
+
if (!orgId) return null; // unattributable — no Organization for this Stripe Customer yet
|
|
208
|
+
|
|
209
|
+
// Subscription lifecycle → mirror the subscription row. `included_changes_balance` is set only
|
|
210
|
+
// on creation (so a mid-cycle update never clobbers a decremented balance); it resets on each
|
|
211
|
+
// paid cycle invoice (below).
|
|
212
|
+
if (
|
|
213
|
+
lc.eventKind === "subscription.created" ||
|
|
214
|
+
lc.eventKind === "subscription.updated" ||
|
|
215
|
+
lc.eventKind === "subscription.canceled"
|
|
216
|
+
) {
|
|
217
|
+
if (!lc.lagebildDealId || !lc.stripeSubscriptionId || !lc.plan) return null; // need these for the row
|
|
218
|
+
const isCreate = lc.eventKind === "subscription.created";
|
|
219
|
+
const status =
|
|
220
|
+
lc.subscriptionStatus ?? (lc.eventKind === "subscription.canceled" ? "canceled" : "active");
|
|
221
|
+
const perCycle = lc.includedChangesPerCycle ?? 0;
|
|
222
|
+
const res = await client.upsertSubscription(tenantId, {
|
|
223
|
+
organization_id: orgId,
|
|
224
|
+
deal_id: lc.lagebildDealId,
|
|
225
|
+
stripe_subscription_id: lc.stripeSubscriptionId,
|
|
226
|
+
status,
|
|
227
|
+
plan: lc.plan,
|
|
228
|
+
mrr_cents: lc.mrrCents ?? 0,
|
|
229
|
+
currency: lc.currency ?? "EUR",
|
|
230
|
+
current_period_end: lc.currentPeriodEnd,
|
|
231
|
+
included_changes_per_cycle: perCycle,
|
|
232
|
+
...(isCreate ? { included_changes_balance: perCycle } : {}),
|
|
233
|
+
});
|
|
234
|
+
// RFC-0386: enqueue outbox task so the sync worker drains the subscription to Pipedrive P3.
|
|
235
|
+
await client.writeOutbox(tenantId, [
|
|
236
|
+
{ op: "upsert_subscription", payload: { subscription_id: res.id }, maxRetries: 5 },
|
|
237
|
+
]);
|
|
238
|
+
return { id: res.id };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
(lc.eventKind === "invoice.paid" || lc.eventKind === "invoice.payment_failed") &&
|
|
243
|
+
lc.stripeInvoiceId
|
|
244
|
+
) {
|
|
245
|
+
const id = await client.appendInvoice(tenantId, {
|
|
246
|
+
organization_id: orgId,
|
|
247
|
+
deal_id: lc.lagebildDealId,
|
|
248
|
+
stripe_invoice_id: lc.stripeInvoiceId,
|
|
249
|
+
kind: lc.invoiceKind ?? "adhoc",
|
|
250
|
+
amount_cents: lc.amountCents ?? 0,
|
|
251
|
+
currency: lc.currency ?? "EUR",
|
|
252
|
+
status: lc.eventKind === "invoice.paid" ? "paid" : "open",
|
|
253
|
+
paid_at: lc.eventKind === "invoice.paid" ? occurredAt : undefined,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// A paid cycle invoice resets the included-changes allowance for the new period (spec §08).
|
|
257
|
+
if (lc.eventKind === "invoice.paid" && lc.invoiceKind === "cycle" && lc.stripeSubscriptionId) {
|
|
258
|
+
const sub = await client.findSubscriptionByStripeId(tenantId, lc.stripeSubscriptionId);
|
|
259
|
+
if (sub) {
|
|
260
|
+
const delta = sub.included_changes_per_cycle - sub.included_changes_balance;
|
|
261
|
+
if (delta !== 0) await client.adjustChangeBalance(tenantId, sub.id, delta);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// RFC-0386: enqueue outbox task so the sync worker drains the invoice to Pipedrive P3/P4.
|
|
265
|
+
await client.writeOutbox(tenantId, [
|
|
266
|
+
{ op: "upsert_invoice", payload: { invoice_id: id }, maxRetries: 5 },
|
|
267
|
+
]);
|
|
268
|
+
return { id };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// A refund is recorded as a negative ad-hoc ledger entry (idempotent by the Stripe id);
|
|
272
|
+
// operator follow-up (e.g. a Pipedrive note) is the sync worker's concern (spec §08).
|
|
273
|
+
if (lc.eventKind === "payment.refunded" && lc.stripeInvoiceId) {
|
|
274
|
+
const id = await client.appendInvoice(tenantId, {
|
|
275
|
+
organization_id: orgId,
|
|
276
|
+
deal_id: lc.lagebildDealId,
|
|
277
|
+
stripe_invoice_id: lc.stripeInvoiceId,
|
|
278
|
+
kind: "adhoc",
|
|
279
|
+
amount_cents: -(lc.amountCents ?? 0),
|
|
280
|
+
currency: lc.currency ?? "EUR",
|
|
281
|
+
status: "void",
|
|
282
|
+
paid_at: occurredAt,
|
|
283
|
+
});
|
|
284
|
+
return { id };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* RFC-0176/0188: write a normalized event into the Lagebild buffer. For a typed funnel event it
|
|
292
|
+
* persists the canonical funnel_stage (bridged to the generic stage), the deal-time offer
|
|
293
|
+
* snapshot, the append-only funnel-event row, and append-only consent evidence; for a plain lead
|
|
294
|
+
* it keeps the back-compatible generic path. The client is injected (testable without network).
|
|
295
|
+
*/
|
|
296
|
+
export async function persistEventToBuffer(
|
|
297
|
+
client: CrmBufferWriter,
|
|
298
|
+
tenantId: string,
|
|
299
|
+
event: IntegrationEvent,
|
|
300
|
+
): Promise<{ id: string } | null> {
|
|
301
|
+
// RFC-0191: Stripe lifecycle events (invoices/subscriptions) take the billing path —
|
|
302
|
+
// they mirror billing facts against the Organization, not the funnel deal flow.
|
|
303
|
+
if (event.lifecycle) {
|
|
304
|
+
return persistLifecycleEvent(client, tenantId, event.lifecycle, event.occurredAt);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// RFC-0188: the typed funnel payload (null for plain non-funnel leads).
|
|
308
|
+
const funnel = extractFunnelPayload(event);
|
|
309
|
+
const actor = mapActor(event.source);
|
|
310
|
+
|
|
311
|
+
// 1. Upsert the contact (dedup by uchat_contact_id when present).
|
|
312
|
+
const uchatContactId =
|
|
313
|
+
typeof event.payload.contact_id === "string" ? event.payload.contact_id : undefined;
|
|
314
|
+
|
|
315
|
+
const contactResult = await client.upsertContact(tenantId, {
|
|
316
|
+
uchat_contact_id: uchatContactId,
|
|
317
|
+
name: event.contact?.name,
|
|
318
|
+
email: event.contact?.email,
|
|
319
|
+
phone: event.contact?.phone,
|
|
320
|
+
uchat_meta:
|
|
321
|
+
Object.keys(event.payload).length > 0
|
|
322
|
+
? (event.payload as Record<string, unknown>)
|
|
323
|
+
: undefined,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// 2. RFC-0190: resolve-or-create the target Organization from the funnel event
|
|
327
|
+
// (no new visitor question — the funnel already asks "for which company?").
|
|
328
|
+
const orgResult = await resolveOrganization(client, tenantId, event);
|
|
329
|
+
|
|
330
|
+
// 3. Upsert the deal. RFC-0188: keep the precise funnel_stage; the generic `stage` is its
|
|
331
|
+
// bridge. RFC-0188: freeze the deal-time offer snapshot on offer.selected.
|
|
332
|
+
const { funnelStage, generic: stage } = resolveStages(event, funnel);
|
|
333
|
+
const offerSnapshot = funnel?.offer?.priceSnapshot;
|
|
334
|
+
// Buffer deal/transition actor enum is uchat | pipedrive | manual.
|
|
335
|
+
const dealActor: "uchat" | "manual" = actor === "operator" ? "manual" : "uchat";
|
|
336
|
+
const dealResult = await client.upsertDeal(tenantId, {
|
|
337
|
+
contact_id: contactResult.id,
|
|
338
|
+
organization_id: orgResult?.id,
|
|
339
|
+
title: resolveDealTitle(event),
|
|
340
|
+
stage,
|
|
341
|
+
funnel_stage: funnelStage,
|
|
342
|
+
offer_snapshot:
|
|
343
|
+
offerSnapshot && Object.keys(offerSnapshot).length > 0 ? offerSnapshot : undefined,
|
|
344
|
+
value: typeof event.payload.deal_value === "number" ? event.payload.deal_value : undefined,
|
|
345
|
+
currency: typeof event.payload.currency === "string" ? event.payload.currency : undefined,
|
|
346
|
+
last_actor: dealActor,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// 4. Append a stage transition (always, to preserve the full audit trail). The transition is
|
|
350
|
+
// recorded in GENERIC terms; a canonical previousStage is bridged down.
|
|
351
|
+
const fromStage: BufferDealStage | undefined = funnel?.previousStage
|
|
352
|
+
? bridgeFunnelStage(funnel.previousStage)
|
|
353
|
+
: typeof event.payload.previous_stage === "string" &&
|
|
354
|
+
(BUFFER_DEAL_STAGES as readonly string[]).includes(event.payload.previous_stage)
|
|
355
|
+
? (event.payload.previous_stage as BufferDealStage)
|
|
356
|
+
: undefined;
|
|
357
|
+
|
|
358
|
+
await client.appendStageTransition(tenantId, {
|
|
359
|
+
deal_id: dealResult.id,
|
|
360
|
+
from_stage: fromStage,
|
|
361
|
+
to_stage: stage,
|
|
362
|
+
actor: dealActor,
|
|
363
|
+
occurred_at: event.occurredAt,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// 4b. RFC-0188: append the typed funnel-event snapshot (idempotent by eventId) so the deal's
|
|
367
|
+
// history is auditable and never re-derived.
|
|
368
|
+
if (funnel) {
|
|
369
|
+
await client.appendFunnelEvent(tenantId, {
|
|
370
|
+
deal_id: dealResult.id,
|
|
371
|
+
idempotency_key: event.eventId,
|
|
372
|
+
event_kind: funnel.eventKind,
|
|
373
|
+
funnel_version: funnel.funnelVersion,
|
|
374
|
+
from_stage: funnel.previousStage,
|
|
375
|
+
to_stage: funnel.stage,
|
|
376
|
+
payload: funnel,
|
|
377
|
+
actor,
|
|
378
|
+
occurred_at: event.occurredAt,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 4c. RFC-0188: legal consent is append-only evidence — never overwritten.
|
|
383
|
+
if (funnel?.eventKind === "legal.consent.recorded" && funnel.legal?.buyerType) {
|
|
384
|
+
const buyerType: VisitorBuyerType = funnel.legal.buyerType;
|
|
385
|
+
await client.appendConsentEvent(tenantId, {
|
|
386
|
+
deal_id: dealResult.id,
|
|
387
|
+
buyer_type: buyerType,
|
|
388
|
+
consent_kind:
|
|
389
|
+
buyerType === "business" ? "b2b_start_before_completion" : "b2c_withdrawal_acknowledged",
|
|
390
|
+
start_before_withdrawal_period: funnel.legal.startBeforeWithdrawalPeriod,
|
|
391
|
+
withdrawal_expiry_acknowledged: funnel.legal.withdrawalExpiryAcknowledged,
|
|
392
|
+
locale: event.locale,
|
|
393
|
+
occurred_at: event.occurredAt,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 4d. RFC-0191 / spec §08: a submitted included change consumes one from the subscription's
|
|
398
|
+
// allowance. Only when the change is actually described (not at the balance check) and the
|
|
399
|
+
// Organization has an active subscription with a positive balance (else it is a paid change,
|
|
400
|
+
// handled by Stripe). Redelivery is deduped upstream (RFC-0181), so this fires once.
|
|
401
|
+
if (
|
|
402
|
+
funnel?.eventKind === "change.requested" &&
|
|
403
|
+
funnel.stage === "change_description_requested" &&
|
|
404
|
+
orgResult
|
|
405
|
+
) {
|
|
406
|
+
const sub = await client.findActiveSubscriptionByOrganization(tenantId, orgResult.id);
|
|
407
|
+
if (sub && sub.included_changes_balance > 0) {
|
|
408
|
+
await client.adjustChangeBalance(tenantId, sub.id, -1);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// 5. Queue outbox tasks for async Pipedrive sync. The Organization syncs BEFORE the
|
|
413
|
+
// deal (RFC-0190) so the deal can reference pipedrive_org_id.
|
|
414
|
+
await client.writeOutbox(tenantId, [
|
|
415
|
+
...(orgResult
|
|
416
|
+
? [
|
|
417
|
+
{
|
|
418
|
+
op: "upsert_organization" as const,
|
|
419
|
+
payload: { organization_id: orgResult.id },
|
|
420
|
+
maxRetries: 5,
|
|
421
|
+
},
|
|
422
|
+
]
|
|
423
|
+
: []),
|
|
424
|
+
{
|
|
425
|
+
op: "upsert_contact",
|
|
426
|
+
payload: { contact_id: contactResult.id },
|
|
427
|
+
maxRetries: 5,
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
op: "upsert_deal",
|
|
431
|
+
payload: { deal_id: dealResult.id },
|
|
432
|
+
maxRetries: 5,
|
|
433
|
+
},
|
|
434
|
+
]);
|
|
435
|
+
|
|
436
|
+
return { id: dealResult.id };
|
|
437
|
+
}
|