feat(ssr): add report email hand-off to ledger export modal
The SSR ledger reports could generate and download a PDF but had lost the SPA's ability to hand the report off to the client's email contacts. This restores it for Profit and Loss, Balance Sheet and Cash Flows. The server still sends nothing: the modal offers a mailto: link, pre-filled with the client's email contacts, subject and body, that opens in the user's own mail client so they can review before sending. Extracts the modal the three reports duplicated into a shared auto-ap.ssr.ledger.export-modal namespace, and tightens the SPA's rules along the way: - admin-only, uniformly (the SPA's Cash Flows page skipped this check) - single client only, uniformly (the SPA's Balance Sheet page did not check, so a multi-client report could be mailed to one client's contacts) - recipients joined with "," per RFC 6068 rather than Outlook's ";" - subject percent-encoded, like the body already was - body links built from :base-url and bidi routes, fixing the dead /reports/ link (the page now lives at /company/reports) and the hardcoded prod domain in the requires-feedback link Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
86
test/clj/auto_ap/ssr/ledger/export_modal_test.clj
Normal file
86
test/clj/auto_ap/ssr/ledger/export_modal_test.clj
Normal file
@@ -0,0 +1,86 @@
|
||||
(ns auto-ap.ssr.ledger.export-modal-test
|
||||
(:require
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.integration.util :refer [admin-token test-client user-token
|
||||
wrap-setup]]
|
||||
[auto-ap.ssr.ledger.export-modal :as sut]
|
||||
[clojure.string :as str]
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[datomic.api :as dc]))
|
||||
|
||||
(use-fixtures :each wrap-setup)
|
||||
|
||||
(def report {:report/name "Profit-and-loss-2026-07-31-for-Acme"
|
||||
:report/url "https://data.example.com/reports/pnl/abc/report.pdf"})
|
||||
|
||||
(defn client-with-emails
|
||||
"Transacts a client with `emails` contacts. :client/emails is a plain ref, not
|
||||
a component, so each nested contact needs its own tempid."
|
||||
[emails]
|
||||
(let [suffix (rand-int 1000000)
|
||||
client-tid (str "client-" suffix)
|
||||
contacts (map-indexed (fn [i e] (assoc e :db/id (str "email-contact-" suffix "-" i)))
|
||||
emails)
|
||||
tempids (:tempids @(dc/transact conn
|
||||
(into [(test-client :db/id client-tid
|
||||
:client/emails (map :db/id contacts))]
|
||||
contacts)))]
|
||||
{:db/id (get tempids client-tid)}))
|
||||
|
||||
(defn modal-body [request report]
|
||||
(:body (sut/export-modal request report)))
|
||||
|
||||
(deftest export-modal-download-link
|
||||
(testing "Should always offer the report for download"
|
||||
(let [client (client-with-emails [])
|
||||
body (modal-body {:identity (admin-token)}
|
||||
(assoc report :report/clients [client]))]
|
||||
(is (str/includes? body (:report/url report))
|
||||
"the S3 url should be linked")
|
||||
(is (str/includes? body "to download")))))
|
||||
|
||||
(deftest export-modal-email-handoff
|
||||
(testing "Should offer a mailto hand-off addressed to the client's contacts"
|
||||
(let [client (client-with-emails [{:email-contact/email "owner@acme.com"
|
||||
:email-contact/description "Owner"}
|
||||
{:email-contact/email "cpa@acme.com"}])
|
||||
body (modal-body {:identity (admin-token)}
|
||||
(assoc report :report/clients [client]))]
|
||||
(is (str/includes? body "mailto:"))
|
||||
(is (str/includes? body "owner@acme.com"))
|
||||
(is (str/includes? body "cpa@acme.com"))
|
||||
(is (str/includes? body "Owner")
|
||||
"contacts should be described so the sender can see who it goes to")))
|
||||
|
||||
(testing "Should not offer a hand-off when the client has no email contacts"
|
||||
(let [client (client-with-emails [])
|
||||
body (modal-body {:identity (admin-token)}
|
||||
(assoc report :report/clients [client]))]
|
||||
(is (not (str/includes? body "mailto:")))))
|
||||
|
||||
(testing "Should not offer a hand-off to non-admins"
|
||||
(let [client (client-with-emails [{:email-contact/email "owner@acme.com"}])
|
||||
body (modal-body {:identity (user-token (:db/id client))}
|
||||
(assoc report :report/clients [client]))]
|
||||
(is (not (str/includes? body "mailto:")))))
|
||||
|
||||
(testing "Should not offer a hand-off for a multi-client report"
|
||||
(let [a (client-with-emails [{:email-contact/email "owner@acme.com"}])
|
||||
b (client-with-emails [{:email-contact/email "owner@beta.com"}])
|
||||
body (modal-body {:identity (admin-token)}
|
||||
(assoc report :report/clients [a b]))]
|
||||
(is (not (str/includes? body "mailto:"))
|
||||
"one client's numbers must not be mailed to another client's contacts"))))
|
||||
|
||||
(deftest mailto-href-encoding
|
||||
(let [contacts [{:email-contact/email "owner@acme.com"}
|
||||
{:email-contact/email "cpa@acme.com"}]
|
||||
href (sut/mailto-href contacts report)]
|
||||
(testing "Should address every contact"
|
||||
(is (str/starts-with? href "mailto:owner@acme.com,cpa@acme.com?")))
|
||||
(testing "Should percent-encode the subject and body"
|
||||
(is (str/includes? href "subject=Profit-and-loss-2026-07-31-for-Acme%20is%20ready"))
|
||||
(is (not (re-find #"[\s\n]" href))
|
||||
"raw whitespace would truncate the href"))
|
||||
(testing "Should carry the report url in the body"
|
||||
(is (str/includes? (sut/email-body (:report/url report)) (:report/url report))))))
|
||||
Reference in New Issue
Block a user