more cart

This commit is contained in:
2026-07-01 10:40:28 +02:00
parent 75db64ce75
commit b1e99891e9
30 changed files with 1058 additions and 93 deletions
+221
View File
@@ -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
}