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
+546
View File
@@ -0,0 +1,546 @@
// Command seed-woo-demo inserts sample WooCommerce orders, line items, reviews,
// and a draft campaign so UI + audience targeting work without a live store.
//
// When WOO_STORE_URL + WOO_CONSUMER_KEY + WOO_CONSUMER_SECRET are set (or -live),
// also upserts woocommerce_configs and optionally tests the REST connection.
//
// Usage:
//
// go run ./cmd/seed-woo-demo -postgres "$DATABASE_URL"
// go run ./cmd/seed-woo-demo -company "A1 Slovenija"
// go run ./cmd/seed-woo-demo -live # require WOO_* and test connection
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/descrybe/descrybe-v2/apps/api/internal/woocommerce"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
const (
demoCategoryName = "Demo Electronics"
demoCategoryUID = "demo-electronics"
demoSKUPrefix = "DEMO-WOO-"
)
type demoProduct struct {
SKU string
Name string
Category string
Price string
WCID int64
}
type demoCustomer struct {
Email string
Name string
}
func main() {
postgresURL := flag.String("postgres", os.Getenv("DATABASE_URL"), "Postgres URL")
companyName := flag.String("company", "A1 Slovenija", "Target company name")
live := flag.Bool("live", false, "Require WOO_* env and test REST connection after seeding")
flag.Parse()
if strings.TrimSpace(*postgresURL) == "" {
log.Fatal("-postgres / DATABASE_URL is required")
}
companyNameNorm := strings.TrimSpace(*companyName)
if companyNameNorm == "" {
log.Fatal("-company is required")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
pg, err := pgxpool.New(ctx, *postgresURL)
if err != nil {
log.Fatalf("postgres: %v", err)
}
defer pg.Close()
companyID, err := resolveCompany(ctx, pg, companyNameNorm)
if err != nil {
log.Fatalf("company: %v", err)
}
tx, err := pg.Begin(ctx)
if err != nil {
log.Fatalf("begin: %v", err)
}
defer tx.Rollback(ctx)
categoryID, err := ensureDemoCategory(ctx, tx, companyID)
if err != nil {
log.Fatalf("category: %v", err)
}
products := []demoProduct{
{SKU: demoSKUPrefix + "TV-50", Name: "Demo 4K TV 50\"", Category: demoCategoryUID, Price: "499.00", WCID: 90001},
{SKU: demoSKUPrefix + "SOUND", Name: "Demo Soundbar", Category: demoCategoryUID, Price: "149.00", WCID: 90002},
{SKU: demoSKUPrefix + "HEAD", Name: "Demo Wireless Headphones", Category: demoCategoryUID, Price: "89.00", WCID: 90003},
}
if err := seedDemoProducts(ctx, tx, companyID, products); err != nil {
log.Fatalf("products: %v", err)
}
customers := []demoCustomer{
{Email: "anna.buyer@example.com", Name: "Anna Buyer"},
{Email: "ben.buyer@example.com", Name: "Ben Buyer"},
{Email: "cara.buyer@example.com", Name: "Cara Buyer"},
{Email: "dan.other@example.com", Name: "Dan Other"},
}
orderCount, itemCount, err := seedDemoOrders(ctx, tx, companyID, products, customers)
if err != nil {
log.Fatalf("orders: %v", err)
}
reviewCount, err := seedDemoReviews(ctx, tx, companyID, products, customers)
if err != nil {
log.Fatalf("reviews: %v", err)
}
encKey := woocommerce.DeriveKey(
os.Getenv("CREDENTIALS_ENCRYPTION_KEY"),
os.Getenv("TOKEN_SIGNING_SECRET")+"|"+*postgresURL,
)
storeURL, key, secret, fromEnv := wooCredsFromEnv()
if err := upsertWooConfig(ctx, tx, companyID, encKey, storeURL, key, secret, fromEnv); err != nil {
log.Fatalf("woocommerce_configs: %v", err)
}
campaignID, err := seedDemoCampaign(ctx, tx, companyID, categoryID)
if err != nil {
log.Fatalf("campaign: %v", err)
}
if err := tx.Commit(ctx); err != nil {
log.Fatalf("commit: %v", err)
}
woo := &woocommerce.Service{Pool: pg}
audience, err := woo.AudienceBoughtCategories(ctx, companyID, demoCategoryName, "", 100)
if err != nil {
log.Fatalf("audience check: %v", err)
}
fmt.Println("=== Descrybe v2 WooCommerce demo seed ===")
fmt.Printf("company: %s (%s)\n", companyNameNorm, companyID)
fmt.Printf("category: %s (%s)\n", demoCategoryName, categoryID)
fmt.Printf("demo products: %d (SKU prefix %s)\n", len(products), demoSKUPrefix)
fmt.Printf("orders upserted: %d\n", orderCount)
fmt.Printf("order items: %d\n", itemCount)
fmt.Printf("reviews upserted: %d\n", reviewCount)
fmt.Printf("audience (%s): %d customers\n", demoCategoryName, audience.Total)
for _, c := range audience.Customers {
fmt.Printf(" - %s <%s>\n", c.Name, c.Email)
}
fmt.Printf("draft campaign: %s\n", campaignID)
fmt.Printf("config store_url: %s\n", storeURL)
if fromEnv {
fmt.Println("credentials: from WOO_* env (encrypted at rest)")
} else {
fmt.Println("credentials: demo placeholders (no live REST)")
}
fmt.Println()
fmt.Println("Next:")
fmt.Println(" 1. Open /woocommerce — Orders & Reviews tabs should list seeded rows")
fmt.Println(" 2. POST /api/woocommerce/audience {\"bought_category\":\"Demo Electronics\"}")
fmt.Println(" 3. Open /campaigns — draft \"Woo demo — purchased electronics\" uses purchased audience")
fmt.Println(" 4. Live store: set WOO_STORE_URL / WOO_CONSUMER_KEY / WOO_CONSUMER_SECRET then re-run with -live")
if *live || (fromEnv && flag.Lookup("live").Value.String() == "true") {
// handled below when -live
}
if *live {
if !fromEnv {
log.Fatal("-live requires WOO_STORE_URL, WOO_CONSUMER_KEY, WOO_CONSUMER_SECRET")
}
client := woocommerce.NewClient(storeURL, key, secret, &http.Client{Timeout: 20 * time.Second})
if err := client.TestConnection(ctx); err != nil {
log.Fatalf("live Woo test failed: %v", err)
}
fmt.Println("live Woo test: OK")
} else if fromEnv {
client := woocommerce.NewClient(storeURL, key, secret, &http.Client{Timeout: 20 * time.Second})
if err := client.TestConnection(ctx); err != nil {
fmt.Printf("live Woo test: skipped/failed (%v) — demo DB seed still applied\n", err)
} else {
fmt.Println("live Woo test: OK (WOO_* present)")
}
}
}
func resolveCompany(ctx context.Context, pg *pgxpool.Pool, name string) (uuid.UUID, error) {
var id uuid.UUID
err := pg.QueryRow(ctx, `
SELECT id FROM companies WHERE name = $1 ORDER BY updated_at DESC LIMIT 1`, name).Scan(&id)
if err != nil {
return uuid.Nil, fmt.Errorf("%q: %w", name, err)
}
return id, nil
}
func ensureDemoCategory(ctx context.Context, tx pgx.Tx, companyID uuid.UUID) (uuid.UUID, error) {
var id uuid.UUID
err := tx.QueryRow(ctx, `
INSERT INTO categories (company_id, name, unique_id, path, level, position, is_active, updated_at)
VALUES ($1, $2, $3, $2, 0, 0, true, now())
ON CONFLICT (company_id, unique_id) DO UPDATE SET
name = EXCLUDED.name,
is_active = true,
updated_at = now()
RETURNING id`, companyID, demoCategoryName, demoCategoryUID).Scan(&id)
return id, err
}
func seedDemoProducts(ctx context.Context, tx pgx.Tx, companyID uuid.UUID, products []demoProduct) error {
for _, p := range products {
mapped, _ := json.Marshal(map[string]any{
"sku": p.SKU,
"price": p.Price,
"regular_price": p.Price,
"images": []string{},
"source": "seed-woo-demo",
})
attrs, _ := json.Marshal(map[string]any{"Brand": "Descrybe Demo"})
_, err := tx.Exec(ctx, `
INSERT INTO processed_products (
company_id, product_id, name, category, description, processed_name, processed_description,
attributes, processed_attributes, status, updated_at
) VALUES (
$1, $2, $3, $4, $5, $3, $5, $6::jsonb, $6::jsonb, 'completed', now()
)
ON CONFLICT DO NOTHING`,
companyID, p.SKU, p.Name, p.Category,
"Seeded demo product for WooCommerce integration testing.", attrs)
if err != nil {
// processed_products may lack a unique on product_id; fall back to upsert-by-lookup.
var existing uuid.UUID
qerr := tx.QueryRow(ctx, `
SELECT id FROM processed_products
WHERE company_id = $1 AND product_id = $2 LIMIT 1`, companyID, p.SKU).Scan(&existing)
if qerr == nil {
_, err = tx.Exec(ctx, `
UPDATE processed_products SET
name = $3, category = $4, description = $5, processed_name = $3,
processed_description = $5, attributes = $6::jsonb, processed_attributes = $6::jsonb,
status = 'completed', updated_at = now()
WHERE id = $2 AND company_id = $1`,
companyID, existing, p.Name, p.Category,
"Seeded demo product for WooCommerce integration testing.", attrs)
if err != nil {
return err
}
} else if qerr == pgx.ErrNoRows {
_, err = tx.Exec(ctx, `
INSERT INTO processed_products (
company_id, product_id, name, category, description, processed_name, processed_description,
attributes, processed_attributes, status, updated_at
) VALUES (
$1, $2, $3, $4, $5, $3, $5, $6::jsonb, $6::jsonb, 'completed', now()
)`,
companyID, p.SKU, p.Name, p.Category,
"Seeded demo product for WooCommerce integration testing.", attrs)
if err != nil {
return err
}
} else {
return qerr
}
}
_ = mapped
}
return nil
}
func seedDemoOrders(ctx context.Context, tx pgx.Tx, companyID uuid.UUID, products []demoProduct, customers []demoCustomer) (int, int, error) {
type line struct {
product demoProduct
qty int
cats []string
}
type orderSpec struct {
externalID int64
status string
customer demoCustomer
total string
lines []line
daysAgo int
}
specs := []orderSpec{
{
externalID: 88001, status: "completed", customer: customers[0], total: "648.00", daysAgo: 12,
lines: []line{
{product: products[0], qty: 1, cats: []string{demoCategoryName}},
{product: products[1], qty: 1, cats: []string{demoCategoryName}},
},
},
{
externalID: 88002, status: "processing", customer: customers[1], total: "89.00", daysAgo: 5,
lines: []line{{product: products[2], qty: 1, cats: []string{demoCategoryName}}},
},
{
externalID: 88003, status: "completed", customer: customers[2], total: "499.00", daysAgo: 3,
lines: []line{{product: products[0], qty: 1, cats: []string{demoCategoryName}}},
},
{
externalID: 88004, status: "completed", customer: customers[3], total: "29.00", daysAgo: 8,
lines: []line{{
product: demoProduct{SKU: "OTHER-SKU-1", Name: "Demo Cable Pack", Price: "29.00", WCID: 90100},
qty: 1,
cats: []string{"Accessories"},
}},
},
}
orders := 0
items := 0
for _, spec := range specs {
payload, _ := json.Marshal(map[string]any{
"id": spec.externalID,
"status": spec.status,
"currency": "EUR",
"total": spec.total,
"billing": map[string]any{"email": spec.customer.Email, "first_name": strings.Split(spec.customer.Name, " ")[0]},
"line_items": len(spec.lines),
"seed": "seed-woo-demo",
})
orderedAt := time.Now().UTC().Add(-time.Duration(spec.daysAgo) * 24 * time.Hour)
var orderID uuid.UUID
err := tx.QueryRow(ctx, `
INSERT INTO woo_orders (
company_id, external_id, status, currency, total, customer_email, customer_name,
ordered_at, payload, synced_at, updated_at
) VALUES (
$1, $2, $3, 'EUR', $4::numeric, $5, $6, $7, $8::jsonb, now(), now()
)
ON CONFLICT (company_id, external_id) DO UPDATE SET
status = EXCLUDED.status,
total = EXCLUDED.total,
customer_email = EXCLUDED.customer_email,
customer_name = EXCLUDED.customer_name,
ordered_at = EXCLUDED.ordered_at,
payload = EXCLUDED.payload,
synced_at = now(),
updated_at = now()
RETURNING id`,
companyID, spec.externalID, spec.status, spec.total,
strings.ToLower(spec.customer.Email), spec.customer.Name, orderedAt, payload,
).Scan(&orderID)
if err != nil {
return orders, items, err
}
orders++
if _, err := tx.Exec(ctx, `DELETE FROM woo_order_items WHERE company_id = $1 AND order_id = $2`, companyID, orderID); err != nil {
return orders, items, err
}
for i, ln := range spec.lines {
catsRaw, _ := json.Marshal(ln.cats)
itemPayload, _ := json.Marshal(map[string]any{
"id": int64(spec.externalID*10 + int64(i+1)),
"product_id": ln.product.WCID,
"sku": ln.product.SKU,
"name": ln.product.Name,
"quantity": ln.qty,
"total": ln.product.Price,
"categories": ln.cats,
})
_, err := tx.Exec(ctx, `
INSERT INTO woo_order_items (
company_id, order_id, external_id, product_id, sku, name, quantity, total, categories, payload, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8::numeric, $9::jsonb, $10::jsonb, now()
)`,
companyID, orderID, spec.externalID*10+int64(i+1), ln.product.WCID,
ln.product.SKU, ln.product.Name, ln.qty, ln.product.Price, catsRaw, itemPayload,
)
if err != nil {
return orders, items, err
}
items++
}
}
return orders, items, nil
}
func seedDemoReviews(ctx context.Context, tx pgx.Tx, companyID uuid.UUID, products []demoProduct, customers []demoCustomer) (int, error) {
type rev struct {
externalID int64
product demoProduct
customer demoCustomer
rating int
status string
body string
daysAgo int
}
specs := []rev{
{88011, products[0], customers[0], 5, "approved", "Picture quality is excellent for the price.", 10},
{88012, products[2], customers[1], 4, "approved", "Comfortable and clear sound.", 4},
{88013, products[1], customers[2], 3, "hold", "Good bass, wish the remote was better.", 2},
}
n := 0
for _, r := range specs {
payload, _ := json.Marshal(map[string]any{
"id": r.externalID, "product_id": r.product.WCID, "rating": r.rating, "seed": "seed-woo-demo",
})
reviewedAt := time.Now().UTC().Add(-time.Duration(r.daysAgo) * 24 * time.Hour)
_, err := tx.Exec(ctx, `
INSERT INTO product_reviews (
company_id, external_id, product_id, product_name, status, reviewer, reviewer_email,
rating, review, reviewed_at, payload, synced_at, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, now(), now()
)
ON CONFLICT (company_id, external_id) DO UPDATE SET
product_id = EXCLUDED.product_id,
product_name = EXCLUDED.product_name,
status = EXCLUDED.status,
reviewer = EXCLUDED.reviewer,
reviewer_email = EXCLUDED.reviewer_email,
rating = EXCLUDED.rating,
review = EXCLUDED.review,
reviewed_at = EXCLUDED.reviewed_at,
payload = EXCLUDED.payload,
synced_at = now(),
updated_at = now()`,
companyID, r.externalID, r.product.WCID, r.product.Name, r.status,
r.customer.Name, strings.ToLower(r.customer.Email), r.rating, r.body, reviewedAt, payload,
)
if err != nil {
return n, err
}
n++
}
return n, nil
}
func wooCredsFromEnv() (storeURL, key, secret string, ok bool) {
storeURL = strings.TrimSpace(os.Getenv("WOO_STORE_URL"))
if storeURL == "" {
storeURL = strings.TrimSpace(os.Getenv("WOOCOMMERCE_STORE_URL"))
}
key = strings.TrimSpace(os.Getenv("WOO_CONSUMER_KEY"))
if key == "" {
key = strings.TrimSpace(os.Getenv("WOOCOMMERCE_CONSUMER_KEY"))
}
secret = strings.TrimSpace(os.Getenv("WOO_CONSUMER_SECRET"))
if secret == "" {
secret = strings.TrimSpace(os.Getenv("WOOCOMMERCE_CONSUMER_SECRET"))
}
if storeURL != "" && key != "" && secret != "" {
return storeURL, key, secret, true
}
return "https://demo.woocommerce.local", "ck_demo_placeholder", "cs_demo_placeholder", false
}
func upsertWooConfig(ctx context.Context, tx pgx.Tx, companyID uuid.UUID, encKey []byte, storeURL, key, secret string, live bool) error {
normalized, err := woocommerce.NormalizeStoreURL(storeURL)
if err != nil {
if errors.Is(err, woocommerce.ErrBlockedStoreURL) || live {
return err
}
// Offline demo placeholder only (never a private/metadata IP).
normalized = "https://demo.woocommerce.local"
}
keyEnc, err := woocommerce.EncryptSecret(encKey, key)
if err != nil {
return err
}
secretEnc, err := woocommerce.EncryptSecret(encKey, secret)
if err != nil {
return err
}
now := time.Now().UTC()
opt := woocommerce.SyncOptions{
MatchStrategy: "sku",
LastSyncStatus: "success",
LastOrdersSyncStatus: "success",
LastReviewsSyncStatus: "success",
LastOrdersSyncedAt: &now,
LastReviewsSyncedAt: &now,
ProductIDs: map[string]int{},
CategoryMappings: map[string]woocommerce.CategoryMap{},
AttributeMappings: map[string]woocommerce.AttributeMap{},
}
raw, err := json.Marshal(opt)
if err != nil {
return err
}
enabled := live
testStatus := "demo"
if live {
testStatus = "ok"
}
_, err = tx.Exec(ctx, `
INSERT INTO woocommerce_configs (
company_id, store_url, consumer_key, consumer_secret, is_enabled, sync_options,
last_synced_at, last_test_at, last_test_status, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6::jsonb, now(), now(), $7, now()
)
ON CONFLICT (company_id) DO UPDATE SET
store_url = EXCLUDED.store_url,
consumer_key = EXCLUDED.consumer_key,
consumer_secret = EXCLUDED.consumer_secret,
is_enabled = EXCLUDED.is_enabled,
sync_options = EXCLUDED.sync_options,
last_synced_at = EXCLUDED.last_synced_at,
last_test_at = EXCLUDED.last_test_at,
last_test_status = EXCLUDED.last_test_status,
updated_at = now()`,
companyID, normalized, keyEnc, secretEnc, enabled, raw, testStatus)
return err
}
func seedDemoCampaign(ctx context.Context, tx pgx.Tx, companyID, categoryID uuid.UUID) (uuid.UUID, error) {
af, _ := json.Marshal(map[string]any{
"type": "purchased",
"category_ids": []string{categoryID.String()},
"bought_category": demoCategoryName,
"bought_categories": []string{demoCategoryName},
})
name := "Woo demo — purchased electronics"
var id uuid.UUID
err := tx.QueryRow(ctx, `
SELECT id FROM email_campaigns
WHERE company_id = $1 AND name = $2
ORDER BY created_at DESC LIMIT 1`, companyID, name).Scan(&id)
if err == nil {
_, err = tx.Exec(ctx, `
UPDATE email_campaigns SET
category_ids = ARRAY[$2]::uuid[],
audience_filter = $3::jsonb,
status = 'draft',
updated_at = now()
WHERE id = $4 AND company_id = $1`,
companyID, categoryID, af, id)
return id, err
}
if err != pgx.ErrNoRows {
return uuid.Nil, err
}
err = tx.QueryRow(ctx, `
INSERT INTO email_campaigns (
company_id, name, template_key, status, category_ids, product_ids,
prompt, use_default_prompt, audience_filter, updated_at
) VALUES (
$1, $2, 'black_friday', 'draft', ARRAY[$3]::uuid[], ARRAY[]::uuid[],
'Highlight Demo Electronics for past buyers.', true, $4::jsonb, now()
)
RETURNING id`, companyID, name, categoryID, af).Scan(&id)
return id, err
}