Spaces:
Sleeping
Sleeping
File size: 4,455 Bytes
aa9892b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | -- ๊ตฌ๋
+ ๊ฒฐ์ ์ด๋ฒคํธ (2026-05-03)
--
-- CCBill / Epoch ๊ณ์ฝ ์ ์ค์ผ๋ ํค. ๊ณ์ฝ ํ webhook ํตํฉ์ผ๋ก ์ฑ์๋ฃ์.
--
-- ํ๋ ๊ตฌ์กฐ:
-- free: $0/์ ยท ์ผ 100 ํฌ๋ ๋ง
-- standard: $20/์ ยท ์ผ 1,000 ํฌ๋ ๋ง + ๐ hard ๋ชจ๋ ํ์ฑํ
-- pro: $100/์ ยท ์ผ 5,000 ํฌ๋ ๋ง + ์ฐ์ ์ฒ๋ฆฌ
--
-- ๊ฒฐ์ ์ฌ (processor):
-- 'ccbill' : ๋ฉ์ธ (์ด๋ํธ ์นด๋ ๊ฒฐ์ )
-- 'epoch' : ๋ฐฑ์
(์ด๋ํธ ์นด๋ ๊ฒฐ์ )
-- 'manual' : ๊ด๋ฆฌ์ ์ง์ ๋ถ์ฌ (๋ฒ ํยทํ๋ก๋ชจ์
)
-- 'coupon' : ์ฟ ํฐ ์ฝ๋๋ก ๋ถ์ฌ๋ ์์ ๊ตฌ๋
CREATE TABLE IF NOT EXISTS public.subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
plan VARCHAR(16) NOT NULL CHECK (plan IN ('free', 'standard', 'pro')),
status VARCHAR(16) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'active', 'past_due', 'canceled', 'expired')),
processor VARCHAR(16) NOT NULL DEFAULT 'manual'
CHECK (processor IN ('ccbill', 'epoch', 'manual', 'coupon')),
processor_subscription_id VARCHAR(128), -- CCBill subscription ID ๋ฑ ์ธ๋ถ ์๋ณ์
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
current_period_start TIMESTAMPTZ NOT NULL DEFAULT NOW(),
current_period_end TIMESTAMPTZ, -- ๋ค์ ๊ฒฐ์ ์ผ (๊ตฌ๋
์ข
๋ฃ ์์ ์์ )
canceled_at TIMESTAMPTZ, -- ์ฌ์ฉ์๊ฐ ์ทจ์ ์์ฒญํ ์์ (period_end ๊น์ง ํ์ฑ)
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ํ ์ฌ์ฉ์์ ๋ํด active ๊ตฌ๋
์ ์ต๋ 1๊ฐ (DB ๋ ๋ฒจ enforcement)
CREATE UNIQUE INDEX IF NOT EXISTS idx_subscriptions_user_active
ON public.subscriptions(user_id)
WHERE status = 'active';
-- ์ฌ์ฉ์๋ณ ๊ตฌ๋
์ด๋ ฅ ์กฐํ ๋น๋ ๋์
CREATE INDEX IF NOT EXISTS idx_subscriptions_user_created
ON public.subscriptions(user_id, created_at DESC);
-- ๊ฒฐ์ ์ฌ webhook ID ์กฐํ (์ค๋ณต ์ฒ๋ฆฌ ๋ฐฉ์ง์ฉ)
CREATE INDEX IF NOT EXISTS idx_subscriptions_processor_id
ON public.subscriptions(processor, processor_subscription_id)
WHERE processor_subscription_id IS NOT NULL;
ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can read own subscriptions" ON public.subscriptions
FOR SELECT USING (auth.uid() = user_id);
-- ๊ฒฐ์ ์ด๋ฒคํธ ๋ก๊ทธ โ webhook ์์ ยท์ฌ์๋ยทrefundยทchargeback ๋ชจ๋ ์ถ์
CREATE TABLE IF NOT EXISTS public.billing_events (
id BIGSERIAL PRIMARY KEY,
subscription_id UUID REFERENCES public.subscriptions(id) ON DELETE SET NULL,
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
event_type VARCHAR(32) NOT NULL,
-- 'subscription_created', 'subscription_renewed', 'subscription_canceled',
-- 'payment_success', 'payment_failed', 'refund_issued', 'chargeback',
-- 'webhook_received', 'webhook_processed', 'webhook_failed'
processor VARCHAR(16) NOT NULL DEFAULT 'manual',
processor_event_id VARCHAR(128), -- ์ธ๋ถ ๊ฒฐ์ ์ฌ ์ด๋ฒคํธ ID (idempotency)
amount_usd_cents INTEGER, -- ๊ธ์ก (USD ์ผํธ, ๊ฒฐ์ ์ด๋ฒคํธ๋ง)
payload JSONB NOT NULL DEFAULT '{}',
error_message TEXT,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_billing_events_user
ON public.billing_events(user_id, occurred_at DESC);
CREATE INDEX IF NOT EXISTS idx_billing_events_subscription
ON public.billing_events(subscription_id, occurred_at DESC);
-- webhook ์ค๋ณต ์์ ์ฐจ๋จ์ฉ
CREATE UNIQUE INDEX IF NOT EXISTS idx_billing_events_processor_event
ON public.billing_events(processor, processor_event_id)
WHERE processor_event_id IS NOT NULL;
ALTER TABLE public.billing_events ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can read own billing events" ON public.billing_events
FOR SELECT USING (auth.uid() = user_id);
-- updated_at ์๋ ๊ฐฑ์
CREATE OR REPLACE FUNCTION public.tg_subscriptions_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_subscriptions_updated_at ON public.subscriptions;
CREATE TRIGGER trg_subscriptions_updated_at
BEFORE UPDATE ON public.subscriptions
FOR EACH ROW EXECUTE FUNCTION public.tg_subscriptions_updated_at();
|