Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e8fff1d
feat(messaging): lock code-managed templates and adopt templates into…
Silthus Oct 31, 2026
98e10b5
chore(messaging): test the key workflow check when an upload adopts a…
Silthus Oct 31, 2026
c3a9531
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 1, 2026
62c59f3
fix(messaging): check the key workflow's access on adoption and name …
Silthus Nov 1, 2026
5cc95f4
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 1, 2026
c9db420
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 1, 2026
4584bd8
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 1, 2026
d090f43
fix(messaging): refuse an if_code_managed upload to a template edited…
Silthus Nov 2, 2026
8ab6006
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 2, 2026
002a08f
fix(messaging): import HogFlow through the workflows facade in the ad…
Silthus Nov 2, 2026
a0ecc32
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 3, 2026
9976f6c
chore(messaging): expect the transactional email api docs url in refu…
Silthus Nov 3, 2026
cf3521f
Merge the template uploads layer into the template lock and adopt layer
Silthus Nov 6, 2026
7f77981
Merge branch 'email-v2/04-template-uploads' into email-v2/04c-templat…
Silthus Nov 6, 2026
16f3d2d
chore(messaging): fit the lock tests to master's object access hook
Silthus Nov 6, 2026
38c228c
chore: update OpenAPI generated types
tests-posthog[bot] Nov 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions products/messaging/backend/facade/contracts.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections.abc import Mapping
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
Expand Down Expand Up @@ -321,6 +322,10 @@ class TemplateManagedBy(LabeledStrEnum):
CODE = "code", "Code"


class TemplateReleasedTo(LabeledStrEnum):
GUI = "gui", "GUI"


class TemplateUploadResult(LabeledStrEnum):
CREATED = "created", "Created"
UPDATED = "updated", "Updated"
Expand All @@ -335,6 +340,34 @@ class ContentHashState(LabeledStrEnum):
OUTDATED = "outdated", "Outdated"


@frozen
class TemplateWriteIntent:
action: str
payload: Mapping[str, Any]


class TemplateWriteRefused(Exception):
def __init__(self, *, code: str, detail: str, why: str, fix: str) -> None:
super().__init__(detail)
self.code = code
self.detail = detail
self.why = why
self.fix = fix


class TemplateManagedByCode(TemplateWriteRefused):
def __init__(self, *, detail: str, why: str, fix: str) -> None:
super().__init__(code="immutable", detail=detail, why=why, fix=fix)


class TemplateKeyRefused(TemplateWriteRefused):
pass


class TemplateEditedInPostHog(Exception):
code = "template_edited_in_posthog"


@frozen
class TemplateSource:
repository: str | None
Expand All @@ -353,3 +386,4 @@ class TemplateUpload:
design: dict[str, Any]
source: TemplateSource | None
if_not_superseded: bool
if_code_managed: bool
58 changes: 56 additions & 2 deletions products/messaging/backend/facade/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@
from posthog.api.tagged_item import current_tag_names
from posthog.dataclasses import frozen

from products.messaging.backend.facade.contracts import TemplateManagedBy, TemplateUpload, TemplateUploadResult
from products.messaging.backend.facade.contracts import (
TemplateManagedBy,
TemplateUpload,
TemplateUploadResult,
TemplateWriteIntent,
)
from products.messaging.backend.models.message_template import MessageTemplate
from products.messaging.backend.services import (
message_templates as templates_service,
template_keys,
template_ownership,
template_saves,
template_uploads,
)
Expand Down Expand Up @@ -123,10 +130,13 @@ def save_template_draft(
template_id: UUID,
draft: dict[str, Any] | None,
*,
intent: TemplateWriteIntent,
require_workflow_editor: Callable[[UUID], None],
) -> MessageTemplateRow:
return _to_contract(
template_saves.save_locked_draft(team_id, template_id, draft, require_workflow_editor=require_workflow_editor)
template_saves.save_locked_draft(
team_id, template_id, draft, intent=intent, require_workflow_editor=require_workflow_editor
)
)


Expand All @@ -135,6 +145,7 @@ def save_template_fields(
template_id: UUID,
fields: dict[str, Any],
*,
intent: TemplateWriteIntent,
user_id: int | None,
require_workflow_editor: Callable[[UUID], None],
) -> MessageTemplateRow:
Expand All @@ -150,6 +161,7 @@ def write(template: MessageTemplate) -> None:
team_id,
template_id,
write,
intent=intent,
user_id=user_id,
require_workflow_editor=require_workflow_editor,
)
Expand All @@ -161,6 +173,7 @@ def save_template_content(
template_id: UUID,
edit: Callable[[dict[str, Any]], dict[str, Any]],
*,
intent: TemplateWriteIntent,
user_id: int | None,
require_workflow_editor: Callable[[UUID], None],
) -> MessageTemplateRow:
Expand All @@ -169,12 +182,53 @@ def save_template_content(
team_id,
template_id,
lambda template: templates_service.edit_content(template, edit),
intent=intent,
user_id=user_id,
require_workflow_editor=require_workflow_editor,
)
)


def check_template_write(template: MessageTemplateRow, intent: TemplateWriteIntent) -> None:
template_keys.check_key_kept(template, intent)
template_ownership.check_write(template, intent)


def is_release(intent: TemplateWriteIntent) -> bool:
return template_ownership.is_release(intent)


def release_template(
team_id: int, template_id: UUID, *, require_workflow_editor: Callable[[UUID], None]
) -> MessageTemplateRow:
return _to_contract(
template_saves.release_template(team_id, template_id, require_workflow_editor=require_workflow_editor)
)


def is_adoption(intent: TemplateWriteIntent) -> bool:
return template_keys.is_adoption(intent)


def check_template_key_write(template: MessageTemplateRow, intent: TemplateWriteIntent) -> None:
template_keys.check_key_unchanged(template, intent)


def adopt_template(
team_id: int,
template_id: UUID,
key: str,
*,
intent: TemplateWriteIntent,
require_workflow_editor: Callable[[UUID], None],
) -> MessageTemplateRow:
return _to_contract(
template_keys.adopt_template(
team_id, template_id, key, intent=intent, require_workflow_editor=require_workflow_editor
)
)


def upload_template(
team_id: int,
upload: TemplateUpload,
Expand Down
Loading
Loading