@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.

    The two-actor activation control lives here: status: "ACTIVE" returns EMAIL_TEMPLATE_SAME_ACTOR_ACTIVATION when the publishing actor also saved the copy being published. 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
    instance: (...args: []) => EmailTemplateAdminService = ...
    • 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"
                | "INVOICE"
                | "RECEIPT"
                | "AUTH"
                | "APPLICATION"
            )[];
            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"
              | "INVOICE"
              | "RECEIPT"
              | "AUTH"
              | "APPLICATION"
          )[]
        • Optionallive?: boolean

          Only variants with a live version.

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

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

    • 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: EmailTemplateVersion[]; total: number };
          }
          | { error: ServiceError; ok: false },
      >

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

      The outcome names the exact copy exercised (version + content hash) on a completed send, and the reason nothing arrived on a skip. Throws on SES failure.

      Parameters

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

      Returns Promise<{ ok: true; value: EmailSendOutcome }>

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

      Parameters

      • input: { actorId: string; data: EmailTemplateUpdateData; id: string }
        • actorId: string

          The acting admin, adusr_…; saved content records it as its author.

        • data: EmailTemplateUpdateData
        • id: string

      Returns Promise<
          | { ok: true; value: EmailTemplate }
          | {
              error:
                  | ServiceError<"EMAIL_TEMPLATE_NOT_FOUND">
                  | ServiceError<
                      "EMAIL_TEMPLATE_INVALID",
                      {
                          issues: Readonly<
                              {
                                  code: EmailTemplateValidationCode;
                                  field: EmailTemplateValidationField;
                                  message: string;
                                  offset?: number;
                                  renderCase?: Readonly<(...)>;
                                  value?: string;
                              },
                          >[];
                      },
                  >
                  | ServiceError<"EMAIL_TEMPLATE_SAME_ACTOR_ACTIVATION">
                  | ServiceError<"EMAIL_TEMPLATE_VERSION_NOT_FOUND">
                  | ServiceError<"EMAIL_TEMPLATE_NOT_DEACTIVATABLE">;
              ok: false;
          },
      >