feat(phases): ✨ Add merchant configuration and processing logic for phase 14 initialization
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
c1387a020c
commit
7d035f0e77
1 changed files with 180 additions and 0 deletions
180
features/platform-seed/src/phases/phase14-merchant.ts
Normal file
180
features/platform-seed/src/phases/phase14-merchant.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { randomUUID } from 'node:crypto'
|
||||
import { log, logError } from '../lib/http'
|
||||
import { createRng } from '../lib/rng'
|
||||
import { withDb, MERCHANT_DB, insertChunked } from '../lib/db'
|
||||
import { loadStoreData } from '../lib/data-loader'
|
||||
import type { UserRecord } from './phase1-sso-users'
|
||||
|
||||
interface ProductTemplate {
|
||||
name: string
|
||||
description: string
|
||||
productType: 'physical_merchandise' | 'digital_product' | 'gift_card' | 'subscription'
|
||||
basePriceMin: number
|
||||
basePriceMax: number
|
||||
}
|
||||
|
||||
const STORE_PRODUCTS: Record<string, ProductTemplate[]> = {
|
||||
'valerias-boutique': [
|
||||
{ name: 'Silk Lace Bodysuit', description: 'Hand-selected premium silk lace bodysuit in midnight black', productType: 'physical_merchandise', basePriceMin: 49.99, basePriceMax: 89.99 },
|
||||
{ name: 'Satin Choker Set', description: 'Matching satin choker and bracelet accessory set', productType: 'physical_merchandise', basePriceMin: 24.99, basePriceMax: 44.99 },
|
||||
{ name: 'Signed Art Print Collection', description: 'Set of 3 personally signed limited-edition art prints', productType: 'digital_product', basePriceMin: 29.99, basePriceMax: 59.99 },
|
||||
{ name: 'Velvet Robe', description: 'Luxurious velvet kimono-style robe with embroidered details', productType: 'physical_merchandise', basePriceMin: 79.99, basePriceMax: 129.99 },
|
||||
{ name: 'Exclusive Lingerie Gift Card', description: 'Gift card redeemable for any item in the boutique', productType: 'gift_card', basePriceMin: 25.0, basePriceMax: 100.0 },
|
||||
],
|
||||
'mikas-content-shop': [
|
||||
{ name: 'Premium Photoset Bundle', description: 'Curated bundle of 50 high-resolution exclusive photos', productType: 'digital_product', basePriceMin: 19.99, basePriceMax: 39.99 },
|
||||
{ name: 'Behind the Scenes Video Pack', description: 'Collection of 10 behind-the-scenes video clips', productType: 'digital_product', basePriceMin: 29.99, basePriceMax: 49.99 },
|
||||
{ name: 'Monthly Streaming Pass', description: 'Unlimited access to all live streams for one month', productType: 'subscription', basePriceMin: 14.99, basePriceMax: 29.99 },
|
||||
{ name: 'Polaroid Print Set', description: 'Set of 5 authentic signed polaroid-style prints', productType: 'physical_merchandise', basePriceMin: 34.99, basePriceMax: 59.99 },
|
||||
{ name: 'Custom Shoutout Video', description: 'Personalized 60-second video message', productType: 'digital_product', basePriceMin: 49.99, basePriceMax: 99.99 },
|
||||
],
|
||||
'jade-digital-store': [
|
||||
{ name: 'Exclusive Archive Access', description: 'Full access to the complete digital content archive', productType: 'digital_product', basePriceMin: 39.99, basePriceMax: 79.99 },
|
||||
{ name: 'Custom Video Request', description: 'Fully custom video produced to your specifications', productType: 'digital_product', basePriceMin: 99.99, basePriceMax: 199.99 },
|
||||
{ name: 'Digital Wallpaper Pack', description: 'HD wallpaper collection for desktop and mobile', productType: 'digital_product', basePriceMin: 9.99, basePriceMax: 14.99 },
|
||||
{ name: 'Jade VIP Membership Card', description: 'Physical membership card with exclusive digital perks', productType: 'physical_merchandise', basePriceMin: 19.99, basePriceMax: 34.99 },
|
||||
{ name: 'Content Creator Starter Guide', description: 'Comprehensive digital guide to content creation', productType: 'digital_product', basePriceMin: 24.99, basePriceMax: 49.99 },
|
||||
],
|
||||
'astrid-wellness-shop': [
|
||||
{ name: 'Aromatherapy Essential Oil Kit', description: 'Curated set of 6 premium essential oils for relaxation', productType: 'physical_merchandise', basePriceMin: 44.99, basePriceMax: 74.99 },
|
||||
{ name: 'Soy Wax Massage Candle', description: 'Hand-poured soy wax candle that melts into warm massage oil', productType: 'physical_merchandise', basePriceMin: 29.99, basePriceMax: 49.99 },
|
||||
{ name: 'Guided Meditation Audio Series', description: '10-session guided meditation audio collection', productType: 'digital_product', basePriceMin: 19.99, basePriceMax: 34.99 },
|
||||
{ name: 'Wellness Journal Bundle', description: 'Beautifully bound wellness journal with guided prompts', productType: 'physical_merchandise', basePriceMin: 24.99, basePriceMax: 39.99 },
|
||||
{ name: 'Self-Care Gift Set', description: 'Complete self-care package with bath bombs, oils, and tea', productType: 'gift_card', basePriceMin: 59.99, basePriceMax: 99.99 },
|
||||
],
|
||||
'natashas-fitness-merch': [
|
||||
{ name: 'Performance Leggings', description: 'High-waist compression leggings with moisture-wicking fabric', productType: 'physical_merchandise', basePriceMin: 49.99, basePriceMax: 79.99 },
|
||||
{ name: 'Resistance Band Set', description: 'Set of 5 color-coded resistance bands with carry bag', productType: 'physical_merchandise', basePriceMin: 24.99, basePriceMax: 39.99 },
|
||||
{ name: 'Pre-Workout Supplement', description: 'Clean-formula pre-workout powder in tropical punch flavor', productType: 'physical_merchandise', basePriceMin: 34.99, basePriceMax: 54.99 },
|
||||
{ name: '8-Week Training Program', description: 'Digital workout program with video demonstrations', productType: 'digital_product', basePriceMin: 39.99, basePriceMax: 69.99 },
|
||||
{ name: 'Fitness Coaching Subscription', description: 'Monthly personalized coaching with weekly check-ins', productType: 'subscription', basePriceMin: 79.99, basePriceMax: 149.99 },
|
||||
],
|
||||
'eleanora-gallery': [
|
||||
{ name: 'Limited Edition Art Print', description: 'Numbered fine art giclée print on archival paper', productType: 'physical_merchandise', basePriceMin: 59.99, basePriceMax: 129.99 },
|
||||
{ name: 'Photography Photobook', description: 'Hardcover photobook featuring curated gallery selections', productType: 'physical_merchandise', basePriceMin: 49.99, basePriceMax: 89.99 },
|
||||
{ name: 'Digital Art Collection', description: 'High-resolution digital files of selected gallery works', productType: 'digital_product', basePriceMin: 19.99, basePriceMax: 39.99 },
|
||||
{ name: 'Gallery Gift Set', description: 'Curated gift box with mini prints, postcards, and stickers', productType: 'gift_card', basePriceMin: 34.99, basePriceMax: 64.99 },
|
||||
{ name: 'Collector Series Poster', description: 'Large-format poster from the collector series', productType: 'physical_merchandise', basePriceMin: 29.99, basePriceMax: 49.99 },
|
||||
],
|
||||
}
|
||||
|
||||
export async function phase14Merchant(providerUsers: UserRecord[]): Promise<void> {
|
||||
log('\n═══ Phase 14: Merchant Stores & Products (Direct DB) ═══')
|
||||
|
||||
if (providerUsers.length === 0) {
|
||||
log(' No provider users available, skipping merchant')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await withDb(MERCHANT_DB, async (client) => {
|
||||
const existing = await client.query('SELECT count(*) FROM provider_stores')
|
||||
if (Number(existing.rows[0].count) > 0) {
|
||||
log(` ✓ Already seeded (${existing.rows[0].count} stores). Skipping.`)
|
||||
return
|
||||
}
|
||||
|
||||
const rng = createRng(0x5712e)
|
||||
const storeData = await loadStoreData()
|
||||
const payoutMethods = ['bank_transfer', 'paypal', 'crypto'] as const
|
||||
|
||||
// Build slug → userId lookup
|
||||
const slugToUserId = new Map<string, string>()
|
||||
for (const u of providerUsers) {
|
||||
slugToUserId.set(u.slug, u.userId)
|
||||
}
|
||||
|
||||
// --- Insert stores ---
|
||||
const storeColumns = [
|
||||
'id', 'user_id', 'slug', 'display_name', 'description',
|
||||
'platform_fee_percent', 'payout_method', 'status', 'created_at', 'updated_at',
|
||||
]
|
||||
const now = new Date().toISOString()
|
||||
const storeIdMap = new Map<string, string>()
|
||||
const storeRows: unknown[][] = []
|
||||
|
||||
for (const store of storeData) {
|
||||
const userId = slugToUserId.get(store.providerSlug)
|
||||
if (!userId) {
|
||||
log(` ⚠ No user found for provider slug "${store.providerSlug}", skipping store`)
|
||||
continue
|
||||
}
|
||||
const storeId = randomUUID()
|
||||
storeIdMap.set(store.slug, storeId)
|
||||
storeRows.push([
|
||||
storeId,
|
||||
userId,
|
||||
store.slug,
|
||||
store.displayName,
|
||||
store.description,
|
||||
15.0,
|
||||
rng.pick(payoutMethods),
|
||||
'active',
|
||||
now,
|
||||
now,
|
||||
])
|
||||
}
|
||||
|
||||
const storesInserted = await insertChunked(client, 'provider_stores', storeColumns, storeRows)
|
||||
log(` ✓ ${storesInserted} stores inserted`)
|
||||
|
||||
// --- Insert products ---
|
||||
const productColumns = [
|
||||
'id', 'sku', 'name', 'description', 'product_type', 'category',
|
||||
'base_price_usd', 'inventory_type', 'inventory_quantity', 'status',
|
||||
'featured', 'sort_order', 'total_sales', 'store_id', 'created_at', 'updated_at',
|
||||
]
|
||||
|
||||
const statusWeights = [
|
||||
{ val: 'available', w: 80 },
|
||||
{ val: 'draft', w: 10 },
|
||||
{ val: 'coming_soon', w: 10 },
|
||||
] as const
|
||||
|
||||
const productRows: unknown[][] = []
|
||||
|
||||
for (const [storeSlug, storeId] of storeIdMap) {
|
||||
const templates = STORE_PRODUCTS[storeSlug]
|
||||
if (!templates) continue
|
||||
|
||||
for (let i = 0; i < templates.length; i++) {
|
||||
const tpl = templates[i]
|
||||
const seq = String(i + 1).padStart(3, '0')
|
||||
const sku = `SEED-${storeSlug}-${seq}`
|
||||
const price = rng.float(tpl.basePriceMin, tpl.basePriceMax)
|
||||
const isDigital = tpl.productType === 'digital_product' || tpl.productType === 'subscription'
|
||||
const inventoryType = isDigital ? 'unlimited' : 'limited'
|
||||
const inventoryQuantity = isDigital ? null : rng.int(10, 100)
|
||||
const status = rng.weighted(statusWeights)
|
||||
const featured = i === 0
|
||||
const totalSales = rng.int(0, 50)
|
||||
|
||||
productRows.push([
|
||||
randomUUID(),
|
||||
sku,
|
||||
tpl.name,
|
||||
tpl.description,
|
||||
tpl.productType,
|
||||
tpl.productType,
|
||||
price,
|
||||
inventoryType,
|
||||
inventoryQuantity,
|
||||
status,
|
||||
featured,
|
||||
i + 1,
|
||||
totalSales,
|
||||
storeId,
|
||||
now,
|
||||
now,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const productsInserted = await insertChunked(client, 'merchant_products', productColumns, productRows)
|
||||
log(` ✓ ${productsInserted} products inserted`)
|
||||
})
|
||||
} catch (err) {
|
||||
logError(` Phase 14 failed: ${(err as Error).message}`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue