547 lines
18 KiB
Go
547 lines
18 KiB
Go
// 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
|
||
|
|
}
|