feat: add feedback resolver

This commit is contained in:
Krispire Admin
2026-06-03 23:43:17 +05:30
parent 61b91c473b
commit e9a96f1ff9
7 changed files with 211 additions and 16 deletions
+111
View File
@@ -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
}