From 785b07313bee137e32548483a65431bac2a9e6d0 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 2 Jun 2026 13:00:48 +0200 Subject: [PATCH] :sparkles: Add nitrate endpoint to send renewal email --- .../resources/app/email/invite-to-org/en.html | 6 +- .../resources/app/email/invite-to-org/en.subj | 2 +- .../resources/app/email/invite-to-org/en.txt | 2 +- .../app/email/renewal-notice/en.html | 270 ++++++++++++++++++ .../app/email/renewal-notice/en.subj | 1 + .../resources/app/email/renewal-notice/en.txt | 17 ++ backend/src/app/email.clj | 28 +- .../app/rpc/commands/teams_invitations.clj | 7 +- backend/src/app/rpc/management/nitrate.clj | 43 ++- common/src/app/common/types/organization.cljc | 9 + 10 files changed, 365 insertions(+), 20 deletions(-) create mode 100644 backend/resources/app/email/renewal-notice/en.html create mode 100644 backend/resources/app/email/renewal-notice/en.subj create mode 100644 backend/resources/app/email/renewal-notice/en.txt diff --git a/backend/resources/app/email/invite-to-org/en.html b/backend/resources/app/email/invite-to-org/en.html index 35cbc1e14e..babfca5a9e 100644 --- a/backend/resources/app/email/invite-to-org/en.html +++ b/backend/resources/app/email/invite-to-org/en.html @@ -198,14 +198,14 @@
- {% if organization-initials %}{{organization-initials}}{% endif %} + {% if organization.initials %}{{organization.initials}}{% endif %}
- {{ organization-name|abbreviate:25 }} + {{ organization.name|abbreviate:50 }} diff --git a/backend/resources/app/email/invite-to-org/en.subj b/backend/resources/app/email/invite-to-org/en.subj index 765d186236..adda2f23be 100644 --- a/backend/resources/app/email/invite-to-org/en.subj +++ b/backend/resources/app/email/invite-to-org/en.subj @@ -1 +1 @@ -{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization-name|abbreviate:25 }}” +{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:25 }}” diff --git a/backend/resources/app/email/invite-to-org/en.txt b/backend/resources/app/email/invite-to-org/en.txt index 8e22eba453..ff8eabf194 100644 --- a/backend/resources/app/email/invite-to-org/en.txt +++ b/backend/resources/app/email/invite-to-org/en.txt @@ -1,6 +1,6 @@ Hello! -{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization-name|abbreviate:25 }}”. +{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:25 }}”. Accept invitation using this link: diff --git a/backend/resources/app/email/renewal-notice/en.html b/backend/resources/app/email/renewal-notice/en.html new file mode 100644 index 0000000000..2b6b7a04ad --- /dev/null +++ b/backend/resources/app/email/renewal-notice/en.html @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + +
+
+ Hi{% if user-name %} {{ user-name|abbreviate:25 }}{% endif %}, +
+
+
+ Your Enterprise subscription is coming up for renewal. Here's a summary of what's included. +
+
+ Renewal date: {{ renewal-date }} +
+
+ Estimated amount: {{ estimated-amount }} +
+
+ Organizations covered: {% if organizations|empty? %}No organizations yet.{% endif %} +
+ + {% for org in organizations %} +
+ + + + +
+ {% if org.initials %}{{org.initials}}{% endif %} +
+ + {{ org.name|abbreviate:50 }} + +
+ {% endfor %} +
+
+ This amount is based on current member counts across your organizations. You can adjust members from the Admin Console.
+
+ +
+
+ Enjoy!
+
+
+ The Penpot team.
+
+
+ +
+
+ + {% include "app/email/includes/footer.html" %} + +
+ + + diff --git a/backend/resources/app/email/renewal-notice/en.subj b/backend/resources/app/email/renewal-notice/en.subj new file mode 100644 index 0000000000..18160c4080 --- /dev/null +++ b/backend/resources/app/email/renewal-notice/en.subj @@ -0,0 +1 @@ +Your Enterprise subscription renews on {{ renewal-date }} diff --git a/backend/resources/app/email/renewal-notice/en.txt b/backend/resources/app/email/renewal-notice/en.txt new file mode 100644 index 0000000000..c1dd7dd5b0 --- /dev/null +++ b/backend/resources/app/email/renewal-notice/en.txt @@ -0,0 +1,17 @@ +Hi {% if user-name %}{{ user-name }}{% endif %}, + +Your Enterprise subscription is coming up for renewal. Here's a summary of what's included. + +Renewal date: {{ renewal-date }} +Estimated amount: {{ estimated-amount }} + +Organizations covered: {% if organizations|empty? %}No organizations yet.{% endif %} +{% for org in organizations %} +- {{ org.name }} +{% endfor %} +This amount is based on current member counts across your organizations. You can adjust members from the Admin Console. + +Check our Terms and Conditions and Privacy Policy. + +Enjoy! +The Penpot team. diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index a7e484abc1..f15c6cbbc1 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -431,14 +431,19 @@ :id ::invite-to-team :schema schema:invite-to-team)) +(def ^:private schema:organization-data + [:map + [:name ::sm/text] + [:initials [:maybe :string]] + [:logo [:maybe ::sm/uri]] + [:avatar-bg-url [:maybe ::sm/uri]]]) + (def ^:private schema:invite-to-org [:map [:invited-by ::sm/text] - [:organization-name ::sm/text] - [:organization-initials [:maybe :string]] - [:organization-logo ::sm/uri] [:user-name [:maybe ::sm/text]] - [:token ::sm/text]]) + [:token ::sm/text] + [:organization schema:organization-data]]) (def invite-to-org "Org member invitation email." @@ -446,6 +451,21 @@ :id ::invite-to-org :schema schema:invite-to-org)) + + +(def ^:private schema:renewal-notice + [:map + [:user-name [:maybe ::sm/text]] + [:renewal-date ::sm/text] + [:estimated-amount ::sm/text] + [:organizations [:vector schema:organization-data]]]) + +(def renewal-notice + "Enterprise subscription renewal notice email." + (template-factory + :id ::renewal-notice + :schema schema:renewal-notice)) + (def ^:private schema:join-team [:map [:invited-by ::sm/text] diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index 66ea605b15..50d3ba691b 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -237,9 +237,7 @@ :to email :invited-by (:fullname profile) :user-name (:fullname member) - :organization-name (:name organization) - :organization-logo (:logo organization) - :organization-initials (:initials organization) + :organization organization :token itoken :extra-data ptoken})) (eml/send! {::eml/conn conn @@ -255,11 +253,10 @@ itoken))))) (defn create-org-invitation - [cfg {:keys [::rpc/profile-id id name initials logo] :as params}] + [cfg {:keys [::rpc/profile-id] :as params}] (let [profile (db/get-by-id cfg :profile profile-id)] (create-invitation cfg (assoc params - :organization {:id id :name name :initials initials :logo logo} :profile profile :role :editor)))) diff --git a/backend/src/app/rpc/management/nitrate.clj b/backend/src/app/rpc/management/nitrate.clj index 98259b91e7..e3d3c41312 100644 --- a/backend/src/app/rpc/management/nitrate.clj +++ b/backend/src/app/rpc/management/nitrate.clj @@ -12,11 +12,12 @@ [app.common.exceptions :as ex] [app.common.schema :as sm] [app.common.time :as ct] - [app.common.types.organization :refer [schema:team-with-organization]] + [app.common.types.organization :refer [schema:team-with-organization schema:organization-with-avatar]] [app.common.types.profile :refer [schema:profile, schema:basic-profile]] [app.common.types.team :refer [schema:team]] [app.config :as cf] [app.db :as db] + [app.email :as eml] [app.media :as media] [app.nitrate :as nitrate] [app.rpc :as-alias rpc] @@ -29,7 +30,8 @@ [app.rpc.notifications :as notifications] [app.storage :as sto] [app.util.services :as sv] - [app.worker :as wrk])) + [app.worker :as wrk] + [cuerdas.core :as str])) (defn- profile-to-map [profile] @@ -472,10 +474,7 @@ RETURNING id, deleted_at;") {::doc/added "2.15" ::sm/params [:map [:email ::sm/email] - [:id ::sm/uuid] - [:name ::sm/text] - [:initials [:maybe :string]] - [:logo ::sm/uri]]} + [:organization schema:organization-with-avatar]]} [cfg params] (db/tx-run! cfg ti/create-org-invitation params) nil) @@ -677,6 +676,38 @@ LEFT JOIN profile AS p (count valid-teams-to-transfer) (count valid-teams-to-exit)))) +;; API: send-renewal-email + +(def ^:private schema:send-renewal-email-params + [:map + [:profile-id ::sm/uuid] + [:user-email ::sm/email] + [:user-name [:maybe ::sm/text]] + [:renewal-date :string] + [:estimated-amount :double] + [:organizations [:vector schema:organization-with-avatar]]]) + +(sv/defmethod ::send-renewal-email + "Send an Enterprise subscription renewal notice email to a user." + {::doc/added "2.17" + ::sm/params schema:send-renewal-email-params + ::rpc/auth false} + [cfg {:keys [profile-id user-email user-name renewal-date estimated-amount organizations]}] + (let [amount-str (format "$%.2f" estimated-amount) + user-name (if (str/empty? user-name) + (:fullname (profile/get-profile cfg profile-id)) + user-name)] + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (eml/send! {::eml/conn conn + ::eml/factory eml/renewal-notice + :public-uri (cf/get :public-uri) + :to user-email + :user-name user-name + :renewal-date renewal-date + :estimated-amount amount-str + :organizations organizations})))) + nil) + ;; API: cleanup-org-team-invitations (def ^:private sql:get-profile-emails-by-ids diff --git a/common/src/app/common/types/organization.cljc b/common/src/app/common/types/organization.cljc index 32fcfef6e4..cec08459ac 100644 --- a/common/src/app/common/types/organization.cljc +++ b/common/src/app/common/types/organization.cljc @@ -52,3 +52,12 @@ (or (:organization team) {}) organization->team-keys)) (dissoc team :organization)))) + + +(def schema:organization-with-avatar + [:map + [:id ::sm/uuid] + [:name ::sm/text] + [:initials [:maybe :string]] + [:logo [:maybe ::sm/uri]] + [:avatar-bg-url [:maybe ::sm/uri]]])