feat: add feedback resolver
This commit is contained in:
@@ -92,6 +92,70 @@ func (s *FeedbackService) Submit(ctx context.Context, userID *string, category,
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ErrAlreadyResolved is returned by Resolve when the row was already
|
||||
// closed (or doesn't exist). The handler maps it to a benign redirect
|
||||
// rather than an error — the end state the admin wanted is already true.
|
||||
var ErrAlreadyResolved = errors.New("feedback already resolved")
|
||||
|
||||
// Resolve marks a feedback row resolved (one-way) and, if a recipient
|
||||
// address is known, sends the author a Vaishnava-styled thank-you note.
|
||||
//
|
||||
// The DB update is the source of truth: MarkResolved's `resolved_at IS
|
||||
// NULL` guard guarantees the email fires at most once even under
|
||||
// concurrent button presses. The email is fire-and-forget — a send
|
||||
// failure logs but doesn't un-resolve the row.
|
||||
func (s *FeedbackService) Resolve(ctx context.Context, id string) error {
|
||||
resolved, err := s.repo.MarkResolved(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !resolved {
|
||||
return ErrAlreadyResolved
|
||||
}
|
||||
|
||||
// Re-read with author joins so we know where to send the thank-you.
|
||||
f, err := s.repo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
// Row IS resolved; we just can't assemble the email. Log and
|
||||
// succeed — the resolve itself stuck.
|
||||
s.logger.Warn("feedback resolved but hydrate failed; skipping thank-you email",
|
||||
zap.String("feedback_id", id), zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
to := feedbackReplyTo(f)
|
||||
if to == "" {
|
||||
// No email on file (anonymous submitter who left no contact, or
|
||||
// a since-deleted account). Nothing to send — that's fine.
|
||||
s.logger.Info("feedback resolved; no recipient address, skipping thank-you email",
|
||||
zap.String("feedback_id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
body, err := renderFeedbackResolvedEmail(f)
|
||||
if err != nil {
|
||||
s.logger.Warn("render feedback-resolved email failed", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
if err := s.mail.Send(to, "Your feedback has been addressed — Prabhupāda Says", body); err != nil {
|
||||
s.logger.Warn("feedback-resolved email send failed",
|
||||
zap.String("to", to), zap.Error(err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// feedbackReplyTo picks the address to thank: the authed submitter's
|
||||
// account email if present, else the anonymous contact_email, else "".
|
||||
func feedbackReplyTo(f *model.Feedback) string {
|
||||
if f.AuthorEmail != nil && strings.TrimSpace(*f.AuthorEmail) != "" {
|
||||
return strings.TrimSpace(*f.AuthorEmail)
|
||||
}
|
||||
if f.ContactEmail != nil && strings.TrimSpace(*f.ContactEmail) != "" {
|
||||
return strings.TrimSpace(*f.ContactEmail)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// notifyAdmins fires one HTML email per configured admin. Each
|
||||
// failure logs and falls through — the row is persisted so a missed
|
||||
// email is annoying but not data-losing.
|
||||
@@ -165,3 +229,50 @@ func derefOr(p *string, fallback string) string {
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
// feedbackResolvedEmailTpl is the author-facing thank-you sent when an
|
||||
// admin marks their feedback resolved. Vaishnava-styled — warm,
|
||||
// devotional register, addressed to the submitter by name when known.
|
||||
var feedbackResolvedEmailTpl = template.Must(template.New("feedbackResolved").Parse(`<div style="font-family:Georgia,'Times New Roman',serif;color:#2a2118;max-width:560px;margin:0 auto;line-height:1.7">
|
||||
<p style="font-size:15px;color:#9a6b2f;letter-spacing:0.04em;margin-bottom:1.25em">Hare Kṛṣṇa{{if .Name}}, {{.Name}}{{end}} 🙏</p>
|
||||
|
||||
<p>Please accept our humble obeisances. All glories to Śrīla Prabhupāda.</p>
|
||||
|
||||
<p>We are writing to let you know that the feedback you kindly shared with
|
||||
us has been <strong>acknowledged and addressed</strong>. The necessary
|
||||
features and fixes you pointed us toward have now been implemented.</p>
|
||||
|
||||
<p>Your observations help us serve Śrīla Prabhupāda's <em>vāṇī</em> more
|
||||
faithfully, and we are sincerely grateful that you took the time to write.
|
||||
Devotee feedback like yours is one of the ways this humble service keeps
|
||||
improving.</p>
|
||||
|
||||
<p>We warmly invite you to continue using
|
||||
<a href="https://prabhupadasays.org" style="color:#9a6b2f;font-weight:600">prabhupadasays.org</a>
|
||||
with even greater enthusiasm — there is always more of Śrīla Prabhupāda's
|
||||
nectar waiting to be relished.</p>
|
||||
|
||||
<p style="margin-top:1.75em">Your servants,<br>
|
||||
<strong>The Prabhupāda Says team</strong></p>
|
||||
|
||||
<hr style="border:none;border-top:1px solid #e8ddc9;margin:2em 0">
|
||||
<p style="font-size:12px;color:#a89878;font-style:italic">
|
||||
“Books are the basis.” — His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda
|
||||
</p>
|
||||
</div>`))
|
||||
|
||||
func renderFeedbackResolvedEmail(f *model.Feedback) (string, error) {
|
||||
// Prefer the account display name; fall back to no name (the
|
||||
// template drops the comma cleanly when Name is empty).
|
||||
name := ""
|
||||
if f.AuthorName != nil {
|
||||
name = strings.TrimSpace(*f.AuthorName)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := feedbackResolvedEmailTpl.Execute(&buf, map[string]any{
|
||||
"Name": name,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user