Phase 4 Stage 1: backend foundation (additive endpoints + schema)
Schema (idempotent migration): - payment_sessions.is_free_trial -> is_first_session_discount (data copied) - payment_sessions.mode TEXT NOT NULL DEFAULT 'chat' CHECK (chat|call) - chat_sessions.topics TEXT[] for ESP picks (info-only) New endpoints: - GET /api/client/onboarding-state (drives verif sheet + S6 paywall gate) - GET /api/client/chat-pricing (rewrite: chat+call groups + first-session discount block, per-customer eligibility) - GET /api/shared/auth-providers (env-probed; replaces ENABLE_SOCIAL_AUTH build flag — frontend cutover lands in stage 2) - GET /api/client/support-handles (Tanya Admin handles, CC-config-driven) session_warning WS event fires once at 180s remaining. app_config seeds (mock pricing tiers, first-session discount, support handles, payment method order, end-session 2-step toggle). CC SettingsPage: 3 new sections (first-session discount, pricing tiers JSON editors, support handles). 15/15 Vitest passing. chat_sessions.is_free_trial also renamed for consistency (plan only specified payment_sessions; pairing.service.js read both). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -549,6 +549,111 @@ const migrate = async () => {
|
||||
ON CONFLICT (key) DO NOTHING
|
||||
`
|
||||
|
||||
// --- Phase 4 — Customer Flow Redesign ---
|
||||
|
||||
// 1. payment_sessions + chat_sessions: replace is_free_trial with is_first_session_discount.
|
||||
// Phase 3.7 was the first ship of is_free_trial and never went live with real users
|
||||
// (per project memory), so we copy whatever values exist and drop the old column.
|
||||
// Idempotent: ADD/DROP both use IF [NOT] EXISTS, and each UPDATE is gated on the
|
||||
// old column still existing.
|
||||
await sql`
|
||||
ALTER TABLE payment_sessions
|
||||
ADD COLUMN IF NOT EXISTS is_first_session_discount BOOLEAN NOT NULL DEFAULT false
|
||||
`
|
||||
await sql`
|
||||
ALTER TABLE chat_sessions
|
||||
ADD COLUMN IF NOT EXISTS is_first_session_discount BOOLEAN NOT NULL DEFAULT false
|
||||
`
|
||||
|
||||
// Copy values from the legacy column to the new one. We must use dynamic SQL
|
||||
// (EXECUTE) inside the DO block — a static reference to is_free_trial would fail
|
||||
// to parse when the column has already been dropped on a previous re-run.
|
||||
//
|
||||
// The IF EXISTS check resolves the column against the *current* search_path so
|
||||
// test schemas don't false-positive on the dev `public` schema's leftover columns.
|
||||
// We use to_regclass + pg_attribute (which is search_path-aware) instead of
|
||||
// information_schema.columns (which lists every schema).
|
||||
await sql`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_attribute
|
||||
WHERE attrelid = to_regclass('payment_sessions')
|
||||
AND attname = 'is_free_trial'
|
||||
AND NOT attisdropped
|
||||
) THEN
|
||||
EXECUTE 'UPDATE payment_sessions
|
||||
SET is_first_session_discount = is_free_trial
|
||||
WHERE is_free_trial = true
|
||||
AND is_first_session_discount = false';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_attribute
|
||||
WHERE attrelid = to_regclass('chat_sessions')
|
||||
AND attname = 'is_free_trial'
|
||||
AND NOT attisdropped
|
||||
) THEN
|
||||
EXECUTE 'UPDATE chat_sessions
|
||||
SET is_first_session_discount = is_free_trial
|
||||
WHERE is_free_trial = true
|
||||
AND is_first_session_discount = false';
|
||||
END IF;
|
||||
END
|
||||
$$
|
||||
`
|
||||
|
||||
await sql`ALTER TABLE payment_sessions DROP COLUMN IF EXISTS is_free_trial`
|
||||
await sql`ALTER TABLE chat_sessions DROP COLUMN IF EXISTS is_free_trial`
|
||||
|
||||
// 2. payment_sessions.mode — chat (default) vs voice call. Voice call is just chat
|
||||
// with a different price group + a header badge; no extra media handling.
|
||||
await sql`
|
||||
ALTER TABLE payment_sessions
|
||||
ADD COLUMN IF NOT EXISTS mode TEXT NOT NULL DEFAULT 'chat'
|
||||
CHECK (mode IN ('chat', 'call'))
|
||||
`
|
||||
|
||||
// 3. chat_sessions.topics — ESP picks persisted for info-only display to mitra.
|
||||
// Does NOT affect matching, pricing, or routing.
|
||||
await sql`
|
||||
ALTER TABLE chat_sessions
|
||||
ADD COLUMN IF NOT EXISTS topics TEXT[]
|
||||
`
|
||||
|
||||
// 4. Phase 4 app_config rows. Use ON CONFLICT (key) DO NOTHING so re-runs don't
|
||||
// clobber operator edits, and the migration is idempotent against partially
|
||||
// populated DBs.
|
||||
await sql`
|
||||
INSERT INTO app_config (key, value) VALUES
|
||||
('payment_method_qris_first', ${sql.json({ value: true })}),
|
||||
('searching_timeout_minutes', ${sql.json({ value: 5 })}),
|
||||
('end_session_two_step_confirm', ${sql.json({ value: true })}),
|
||||
('three_minute_warning_enabled', ${sql.json({ value: true })}),
|
||||
('first_session_discount_enabled', ${sql.json({ value: true })}),
|
||||
('first_session_discount_actual_price_idr', ${sql.json({ value: 2000 })}),
|
||||
('first_session_discount_gimmick_price_idr', ${sql.json({ value: 12000 })}),
|
||||
('first_session_discount_duration_minutes', ${sql.json({ value: 12 })}),
|
||||
('first_session_discount_modes', ${sql.json({ value: ['chat'] })}),
|
||||
('pricing_chat_tiers_json', ${sql.json({ tiers: [
|
||||
{ id: '5', minutes: 5, price_idr: 5000, tag: null },
|
||||
{ id: '12', minutes: 12, price_idr: 12000, tag: 'paling pas' },
|
||||
{ id: '30', minutes: 30, price_idr: 25000, tag: 'hemat' },
|
||||
{ id: '60', minutes: 60, price_idr: 45000, tag: null },
|
||||
{ id: '120', minutes: 120, price_idr: 80000, tag: 'best deal' },
|
||||
]})}),
|
||||
('pricing_call_tiers_json', ${sql.json({ tiers: [
|
||||
{ id: '10', minutes: 10, price_idr: 9000, tag: null },
|
||||
{ id: '20', minutes: 20, price_idr: 17000, tag: 'paling pas' },
|
||||
{ id: '45', minutes: 45, price_idr: 35000, tag: null },
|
||||
{ id: '60', minutes: 60, price_idr: 45000, tag: 'hemat' },
|
||||
]})}),
|
||||
('support_handles_json', ${sql.json({
|
||||
wa: { label: 'WhatsApp', deeplink: 'https://wa.me/6285173310010' },
|
||||
telegram: { label: 'Telegram', deeplink: 'https://t.me/halobestie' },
|
||||
})})
|
||||
ON CONFLICT (key) DO NOTHING
|
||||
`
|
||||
|
||||
console.log('Migration complete.')
|
||||
await sql.end()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user