@cfxlabsinc/b2b-services
    Preparing search index...

    Authoring and lifecycle for stored email templates (docs/EMAIL_TEMPLATES.md §4.5). update spans both tables in one transaction — content writes an immutable version row (the trigger snaps draft_version_id), a status change moves the pointers on the identity row — and every write commits an admin.dashboard_user_activity row in the same transaction.

    The two-actor activation control lives here: status: "ACTIVE" is rejected when the publishing actor is also the lastEditedBy of the copy being published (assertSecondActor). status: "DISABLED" is deliberately single-actor — it is the kill switch, reviewed after the fact (§4.6).

    Receives the shared EmailTemplateCache as an injected dependency and reads its authoring projection through the admin partition. Every write busts both surfaces via EmailTemplateCache.invalidate — the shared template-key tag vocabulary evicts this surface's stale pages AND the send surface's pages through the one namespace.

    Index
    • Discard a draft: clear draft_version_id. The version row remains — it is immutable history. live_version_id is untouched, so a variant with a live version keeps sending.

      Parameters

      • input: { actor: { externalId: string }; id: string }

      Returns Promise<
          | { error: ServiceError; ok: false }
          | { ok: true; value: { activity: EmailTemplateActivity } },
      >

    • Authoring list/read, cached under the admin key-prefix partition. Projects invalidReason from the validator (collect mode), so a broken variant shows its problem in the admin list without a write or render. Write/ render validation stays throwing (update re-validates).

      Parameters

      • input: {
            ids?: readonly string[];
            keys?: readonly (
                | "IDENTITY"
                | "ORGANIZATION"
                | "LEDGER_ACCOUNT"
                | "PAYMENT_INSTRUMENT"
                | "DEPOSIT"
                | "WITHDRAW"
                | "TRANSFER"
                | "CARD_TRANSACTION"
            )[];
            live?: boolean;
            page?: number;
            pageSize?: number;
            statusVariants?: readonly string[];
        }
        • Optionalids?: readonly string[]
        • Optionalkeys?: readonly (
              | "IDENTITY"
              | "ORGANIZATION"
              | "LEDGER_ACCOUNT"
              | "PAYMENT_INSTRUMENT"
              | "DEPOSIT"
              | "WITHDRAW"
              | "TRANSFER"
              | "CARD_TRANSACTION"
          )[]
        • Optionallive?: boolean

          Only variants with a live version.

        • Optionalpage?: number
        • OptionalpageSize?: number
        • OptionalstatusVariants?: readonly string[]

      Returns Promise<{ ok: true; value: { hasNext: boolean; items: EmailTemplate[] } }>

    • A variant's full authoring history, newest first — what the editor's Versions tab lists and diffs (EMAIL_TEMPLATES.md §4.6: "field-level diff between any two versions to make rollback informed"). versions stays the two-pointer read for the working copy; this is the append-only trail behind it.

      Uncached, like versions: the tab renders straight after a save, and version rows are immutable so there is nothing to invalidate anyway.

      Parameters

      • input: { id: string; page?: number; pageSize?: number }

      Returns Promise<
          | {
              ok: true;
              value: { hasNext: boolean; items: EmailTemplateVersionRow[] };
          }
          | { error: ServiceError; ok: false },
      >

    • Send a test email via EmailSendCore, exercising the full send pipeline (recipient dedupe → active template resolution → render → SendGrid) in the same way the lambda does for event-driven sends. One implementation, two entrypoints.

      A completed send writes emailTemplate.testSendRequested carrying the exact copy exercised (version + content hash — EMAIL_TEMPLATES.md §4.6); the skip outcomes are returned un-audited so the caller can tell the operator why nothing arrived. Throws on SendGrid failure, before any activity row exists.

      Parameters

      • input: {
            actor: { externalId: string };
            notification: EmailNotification;
            recipient: { email: string; name?: string };
            templateKey:
                | "IDENTITY"
                | "ORGANIZATION"
                | "LEDGER_ACCOUNT"
                | "PAYMENT_INSTRUMENT"
                | "DEPOSIT"
                | "WITHDRAW"
                | "TRANSFER"
                | "CARD_TRANSACTION";
        }

      Returns Promise<EmailSendOutcome & { activity?: EmailTemplateActivity }>

    • Save + lifecycle (PATCH semantics, §4.5). Content fields validate (mode throw) and insert a version row; a status change moves the pointers, running the two-actor guard on ACTIVE. Both happen in one transaction with one audit row; every write then busts the shared cache.

      Parameters

      Returns Promise<
          | { error: ServiceError; ok: false }
          | { ok: true; value: EmailTemplate & { activity: EmailTemplateActivity } },
      >

    • The full live-vs-draft content pair for a variant, plus the pointers' copy. The authoring list flattens to the working copy (EmailTemplate) so the versions tab diffs live against draft here instead.

      Parameters

      • __namedParameters: { id: string }

      Returns Promise<
          | {
              draft: StoredEmailTemplate
              | null;
              live: StoredEmailTemplate | null;
          }
          | null,
      >