Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
package marketing
import (
"context"
"encoding/json"
"fmt"
"sort"
"time"
"github.com/descrybe/descrybe-v2/apps/api/internal/feeds"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
// PreparedCampaign is a seasonal preset linked to an export feed.
type PreparedCampaign struct {
PresetID PresetID `json:"preset_id"`
Name string `json:"name"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Year int `json:"year"`
ExportFeedID string `json:"export_feed_id"`
ExportFeedName string `json:"export_feed_name"`
Created bool `json:"created"`
}
// Service prepares content-calendar campaigns via export feeds (no new tables).
type Service struct {
Pool *pgxpool.Pool
Feeds *feeds.Service
}
// Cap matches httpapi maxPageLimit; seasonal presets over years stay well under this.
const maxPreparedCampaigns = 200
// listPreparedCampaignsSQL filters to campaign feeds in SQL (jsonb key) and caps rows.
const listPreparedCampaignsSQL = `
SELECT id, name, template
FROM export_feeds
WHERE company_id = $1
AND template ? $2
ORDER BY created_at DESC
LIMIT $3`
// ListPreparedCampaigns finds export feeds whose template contains _campaign meta.
func (s *Service) ListPreparedCampaigns(ctx context.Context, companyID uuid.UUID) ([]PreparedCampaign, error) {
rows, err := s.Pool.Query(ctx, listPreparedCampaignsSQL, companyID, CampaignStructureKey, maxPreparedCampaigns)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]PreparedCampaign, 0)
for rows.Next() {
var id uuid.UUID
var name string
var tpl []byte
if err := rows.Scan(&id, &name, &tpl); err != nil {
return nil, err
}
meta, ok := readCampaignMeta(tpl)
if !ok {
continue
}
out = append(out, PreparedCampaign{
PresetID: meta.PresetID,
Name: meta.Name,
StartDate: meta.StartDate,
EndDate: meta.EndDate,
Year: meta.Year,
ExportFeedID: id.String(),
ExportFeedName: name,
Created: false,
})
}
if err := rows.Err(); err != nil {
return nil, err
}
sort.Slice(out, func(i, j int) bool {
return out[i].StartDate < out[j].StartDate
})
return out, nil
}
// PrepareInput creates or reuses a seasonal export feed.
type PrepareInput struct {
PresetID PresetID
Year int
Format string
ForceNew bool
}
// PrepareCampaign creates (or reuses) a CSV/XML export feed for a seasonal preset.
func (s *Service) PrepareCampaign(ctx context.Context, companyID uuid.UUID, in PrepareInput) (PreparedCampaign, error) {
year := in.Year
if year == 0 {
year = time.Now().UTC().Year()
}
preset, err := ResolvePreset(in.PresetID, year)
if err != nil {
return PreparedCampaign{}, err
}
format := in.Format
if format == "" {
format = "csv"
}
if format != "csv" && format != "xml" {
return PreparedCampaign{}, ClientMsg("format must be csv or xml")
}
if !in.ForceNew {
existing, err := s.ListPreparedCampaigns(ctx, companyID)
if err != nil {
return PreparedCampaign{}, err
}
for _, c := range existing {
if c.PresetID == in.PresetID && c.Year == year {
c.Created = false
return c, nil
}
}
}
meta := StructureMeta{
PresetID: preset.ID,
Name: preset.Name,
StartDate: preset.StartDate,
EndDate: preset.EndDate,
Year: preset.Year,
PreparedAt: time.Now().UTC().Format(time.RFC3339),
}
template := map[string]any{
CampaignStructureKey: meta,
"mappings": DefaultCampaignMappings(),
}
if format == "xml" {
template["root"] = "rss"
template["item"] = "channel/item"
}
feedName := fmt.Sprintf("%s %d", preset.Name, year)
created, err := s.Feeds.CreateExportFeed(ctx, companyID, feeds.CreateExportInput{
Name: feedName,
Format: format,
Template: template,
Filters: map[string]any{"statuses": []string{"completed"}},
})
if err != nil {
return PreparedCampaign{}, err
}
id, _ := created["id"].(uuid.UUID)
name, _ := created["name"].(string)
if name == "" {
name = feedName
}
return PreparedCampaign{
PresetID: preset.ID,
Name: preset.Name,
StartDate: preset.StartDate,
EndDate: preset.EndDate,
Year: preset.Year,
ExportFeedID: id.String(),
ExportFeedName: name,
Created: true,
}, nil
}
func readCampaignMeta(raw []byte) (StructureMeta, bool) {
if len(raw) == 0 || string(raw) == "{}" || string(raw) == "null" {
return StructureMeta{}, false
}
var root map[string]any
if err := json.Unmarshal(raw, &root); err != nil {
return StructureMeta{}, false
}
metaRaw, ok := root[CampaignStructureKey]
if !ok || metaRaw == nil {
return StructureMeta{}, false
}
b, err := json.Marshal(metaRaw)
if err != nil {
return StructureMeta{}, false
}
var meta StructureMeta
if err := json.Unmarshal(b, &meta); err != nil {
return StructureMeta{}, false
}
if meta.PresetID == "" || meta.StartDate == "" || meta.EndDate == "" {
return StructureMeta{}, false
}
return meta, true
}