more cart
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/order"
|
||||
)
|
||||
|
||||
// emailTemplateInfo is the metadata returned by the list endpoint.
|
||||
type emailTemplateInfo struct {
|
||||
Name string `json:"name"`
|
||||
Embedded bool `json:"embedded,omitempty"`
|
||||
}
|
||||
|
||||
// emailTemplateHandler serves CRUD operations for email templates stored in the
|
||||
// ORDER_EMAIL_TEMPLATES directory. Embedded templates are read-only; on-disk
|
||||
// copies override them and can be edited via PUT.
|
||||
type emailTemplateHandler struct {
|
||||
dir string // the on-disk templates directory, from ORDER_EMAIL_TEMPLATES env
|
||||
}
|
||||
|
||||
func newEmailTemplateHandler(dir string) *emailTemplateHandler {
|
||||
if dir == "" {
|
||||
dir = "data/order-email-templates"
|
||||
}
|
||||
return &emailTemplateHandler{dir: dir}
|
||||
}
|
||||
|
||||
// list returns all known email templates (embedded + on-disk) with their source.
|
||||
func (h *emailTemplateHandler) list(w http.ResponseWriter, r *http.Request) {
|
||||
embedded := knownEmbeddedTemplates()
|
||||
onDisk := h.listOnDisk()
|
||||
|
||||
seen := map[string]bool{}
|
||||
var infos []emailTemplateInfo
|
||||
|
||||
// List on-disk first (they take precedence).
|
||||
for _, name := range onDisk {
|
||||
infos = append(infos, emailTemplateInfo{Name: name})
|
||||
seen[name] = true
|
||||
}
|
||||
// Then add embedded templates not overridden by disk.
|
||||
for _, name := range embedded {
|
||||
if !seen[name] {
|
||||
infos = append(infos, emailTemplateInfo{Name: name, Embedded: true})
|
||||
seen[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
if infos == nil {
|
||||
infos = []emailTemplateInfo{}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, infos)
|
||||
}
|
||||
|
||||
// get returns the template content for the given name.
|
||||
func (h *emailTemplateHandler) get(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("name")
|
||||
if name == "" {
|
||||
writeErr(w, http.StatusBadRequest, fmt.Errorf("template name is required"))
|
||||
return
|
||||
}
|
||||
store, err := order.LoadEmailTemplates(h.dir)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, fmt.Errorf("load templates: %w", err))
|
||||
return
|
||||
}
|
||||
tmpl, err := store.Get(name)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusNotFound, fmt.Errorf("template %q not found", name))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tmpl)
|
||||
}
|
||||
|
||||
// save persists a template to the on-disk directory. Returns the saved template.
|
||||
func (h *emailTemplateHandler) save(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("name")
|
||||
if name == "" || !validEmailTemplateName(name) {
|
||||
writeErr(w, http.StatusBadRequest, fmt.Errorf("invalid template name %q", name))
|
||||
return
|
||||
}
|
||||
var tmpl order.EmailTemplate
|
||||
if err := json.NewDecoder(r.Body).Decode(&tmpl); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, fmt.Errorf("invalid template body: %w", err))
|
||||
return
|
||||
}
|
||||
if err := tmpl.Validate(name); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(h.dir, 0o755); err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, fmt.Errorf("create templates dir: %w", err))
|
||||
return
|
||||
}
|
||||
data, err := json.MarshalIndent(tmpl, "", " ")
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, fmt.Errorf("marshal template: %w", err))
|
||||
return
|
||||
}
|
||||
path := filepath.Join(h.dir, name+".json")
|
||||
if err := os.WriteFile(path, append(data, '\n'), 0o644); err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, fmt.Errorf("write template: %w", err))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tmpl)
|
||||
}
|
||||
|
||||
// delete removes an on-disk template. Embedded templates cannot be deleted.
|
||||
func (h *emailTemplateHandler) delete(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("name")
|
||||
if name == "" {
|
||||
writeErr(w, http.StatusBadRequest, fmt.Errorf("template name is required"))
|
||||
return
|
||||
}
|
||||
// Check if it's embedded (read-only).
|
||||
if isEmbeddedTemplate(name) {
|
||||
writeErr(w, http.StatusForbidden, fmt.Errorf("template %q is built-in and cannot be deleted; save an override instead", name))
|
||||
return
|
||||
}
|
||||
path := filepath.Join(h.dir, name+".json")
|
||||
if err := os.Remove(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
writeErr(w, http.StatusNotFound, fmt.Errorf("template %q not found on disk", name))
|
||||
return
|
||||
}
|
||||
writeErr(w, http.StatusInternalServerError, fmt.Errorf("delete template: %w", err))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// preview renders a template with sample data and returns the result.
|
||||
func (h *emailTemplateHandler) preview(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("name")
|
||||
if name == "" {
|
||||
writeErr(w, http.StatusBadRequest, fmt.Errorf("template name is required"))
|
||||
return
|
||||
}
|
||||
store, err := order.LoadEmailTemplates(h.dir)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, fmt.Errorf("load templates: %w", err))
|
||||
return
|
||||
}
|
||||
tmpl, err := store.Get(name)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusNotFound, fmt.Errorf("template %q not found", name))
|
||||
return
|
||||
}
|
||||
|
||||
// Build sample data for preview.
|
||||
sampleData := order.SampleTemplateData()
|
||||
subject, _ := order.RenderTextTemplate("preview-subject", tmpl.Subject, sampleData)
|
||||
textBody, _ := order.RenderTextTemplate("preview-text", tmpl.Text, sampleData)
|
||||
htmlBody, _ := order.RenderOptionalHTMLTemplate("preview-html", tmpl.HTML, sampleData)
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"subject": subject,
|
||||
"text": textBody,
|
||||
"html": htmlBody,
|
||||
})
|
||||
}
|
||||
|
||||
// listOnDisk returns template names from the on-disk directory.
|
||||
func (h *emailTemplateHandler) listOnDisk() []string {
|
||||
matches, err := filepath.Glob(filepath.Join(h.dir, "*.json"))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var names []string
|
||||
for _, m := range matches {
|
||||
name := strings.TrimSuffix(filepath.Base(m), ".json")
|
||||
if validEmailTemplateName(name) {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// knownEmbeddedTemplates returns the names of templates embedded in the binary.
|
||||
// This must be kept in sync with the files in pkg/order/email_templates/.
|
||||
func knownEmbeddedTemplates() []string {
|
||||
return []string{
|
||||
"fulfillment-shipped",
|
||||
"order-confirmation",
|
||||
"payment-receipt",
|
||||
"shipment-delivered",
|
||||
"return-confirmed",
|
||||
"refund-issued",
|
||||
}
|
||||
}
|
||||
|
||||
// isEmbeddedTemplate reports whether name matches one of the embedded templates.
|
||||
func isEmbeddedTemplate(name string) bool {
|
||||
for _, e := range knownEmbeddedTemplates() {
|
||||
if e == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// validEmailTemplateName validates a template name (alphanumeric + hyphens).
|
||||
func validEmailTemplateName(s string) bool {
|
||||
if s == "" || len(s) > 64 {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
if !(r == '-' || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user