File size: 4,543 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
97
-- ์นดํŽ˜ ์Œ์„ฑ ์„ ๋ฌผ์ฟ ํฐ MVP โ€” gift_orders (2026-05-28, P1a Phase B)
--
-- ์ฃผ๋ฌธ = ์นดํŽ˜ยท์ƒํ’ˆ + ์Œ์„ฑํŽธ์ง€ + ๋ฐ›๋Š”์ด ์ด๋ฆ„ + ๊ตฌ๋งค์ž ์‹๋ณ„.
-- ์š”์ฒญ์„œ: doc/runbook/req_cafe_gift_mvp.md ยง1
-- ์„ค๊ณ„: doc/runbook/cafe_gift_p1a.md ยง2 sql/048
--
-- Stateless ๊ตฌ๋งค์ž (์‚ฌ์šฉ์ž ๊ฒฐ์ • #1):
--   - buyer_user_id ๋Š” NULL (Supabase anon ๋ฏธ์‚ฌ์šฉ)
--   - ์˜์ˆ˜์ฆ/์žฌ์ ‘๊ทผ = reveal_token + buyer_email
--
-- ๊ฒฐ์ œ mock (P1a):
--   - status 'mock_paid' ๋งŒ coupon ๋ฐœ๊ธ‰. P1b ์—์„œ 'paid' enum ์ถ”๊ฐ€.
--   - mock-pay endpoint ๊ฐ€ ์‹ค PG ์ฝœ๋ฐฑ์„ ๊ฐ€์žฅ (์›Œํ„ฐ๋งˆํฌ๋กœ UI ๋…ธ์ถœ)
--
-- ์œ ํšจ๊ธฐ๊ฐ„ 5๋…„ (๊ณต์ •์œ„ ํ‘œ์ค€์•ฝ๊ด€):
--   - ๋ฐฑ์—”๋“œ๊ฐ€ ๋ช…์‹œ set (DB default X) โ†’ ์ •์ฑ… ๋ณ€๊ฒฝ ์ถ”์  ๊ฐ€๋Šฅ
--   - gift_coupons.expires_at ๋„ ์ด ๊ฐ’์„ ๋ณต์‚ฌ
--
-- โ›” ํšŒ๊ท€ ๊ฐ€๋“œ: ์‹ ๊ทœ ํ…Œ์ด๋ธ”, CHECK ๋ชจ๋‘ ์‹ ๊ทœ ์ปฌ๋Ÿผ๋งŒ. ๊ฒฐ์ œ mock ๋ช…์‹œ ๋ถ„๋ฆฌ ('mock_paid' vs 'paid').

CREATE TABLE IF NOT EXISTS public.gift_orders (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    cafe_id             UUID NOT NULL REFERENCES public.cafe_merchants(id) ON DELETE RESTRICT,
    product_id          UUID NOT NULL REFERENCES public.gift_products(id) ON DELETE RESTRICT,
    voice_letter_id     UUID REFERENCES public.voice_letters(id) ON DELETE SET NULL,

    -- Stateless ๊ตฌ๋งค์ž ์‹๋ณ„ (์‚ฌ์šฉ์ž ๊ฒฐ์ • #1)
    buyer_user_id       UUID REFERENCES auth.users(id) ON DELETE SET NULL,  -- P1a ๋Š” ํ•ญ์ƒ NULL
    buyer_name          TEXT NOT NULL DEFAULT '',
    buyer_phone_e164    TEXT,                        -- ์˜์ˆ˜์ฆ/ํ™˜๋ถˆ์šฉ (์„ ํƒ)
    buyer_email         TEXT,                        -- ์˜์ˆ˜์ฆ ๋ฐœ์†ก (์„ ํƒ)
    recipient_name      TEXT NOT NULL DEFAULT '',

    amount_krw          INT NOT NULL CHECK (amount_krw > 0),

    -- P1a ๊ฒฐ์ œ mock โ€” 'mock_paid' ๋งŒ coupon ๋ฐœ๊ธ‰. P1b ์—์„œ 'paid' enum ์ •์ƒํ™”.
    status              TEXT NOT NULL DEFAULT 'pending'
                        CHECK (status IN ('pending','letter_pending','payable','mock_paid','paid','refunded','canceled')),
    pg_tx_id            TEXT,                        -- P1b ์—์„œ ์‹ค PG ๊ฑฐ๋ž˜ ID

    -- ์œ ํšจ๊ธฐ๊ฐ„ 5๋…„ (๋ฐฑ์—”๋“œ๊ฐ€ now + 5y ๋ช…์‹œ ์„ธํŒ…, DB default X)
    expires_at          TIMESTAMPTZ,

    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_gift_orders_cafe_status
    ON public.gift_orders (cafe_id, status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gift_orders_buyer_user
    ON public.gift_orders (buyer_user_id)
    WHERE buyer_user_id IS NOT NULL;

-- voice_letters โ†’ orders FK ์–‘๋ฐฉํ–ฅ ๋ณด๊ฐ• (sql/047 ์—์„œ๋Š” column ๋งŒ ๋งŒ๋“ค๊ณ  FK ๋Š” ์—ฌ๊ธฐ)
ALTER TABLE public.voice_letters
    DROP CONSTRAINT IF EXISTS voice_letters_order_id_fk;
ALTER TABLE public.voice_letters
    ADD CONSTRAINT voice_letters_order_id_fk
    FOREIGN KEY (order_id) REFERENCES public.gift_orders(id) ON DELETE SET NULL;

ALTER TABLE public.gift_orders ENABLE ROW LEVEL SECURITY;

-- ์‚ฌ์žฅ: ๋ณธ์ธ ์นดํŽ˜ ์ฃผ๋ฌธ SELECT (๋Œ€์‹œ๋ณด๋“œ์šฉ, Phase E)
DROP POLICY IF EXISTS gift_orders_cafe_read ON public.gift_orders;
CREATE POLICY gift_orders_cafe_read ON public.gift_orders FOR SELECT
    USING (cafe_id IN (
        SELECT id FROM public.cafe_merchants WHERE owner_user_id = auth.uid()
    ));

-- ๊ตฌ๋งค์ž ๋ณธ์ธ (๋กœ๊ทธ์ธ ์ผ€์ด์Šค โ€” P1a ๋Š” NULL ์ด๋ผ ๋น„ํ™œ์„ฑ, P1b ํ˜ธํ™˜)
DROP POLICY IF EXISTS gift_orders_buyer_read ON public.gift_orders;
CREATE POLICY gift_orders_buyer_read ON public.gift_orders FOR SELECT
    USING (buyer_user_id = auth.uid());

-- INSERT/UPDATE ๋Š” ๋ฐฑ์—”๋“œ service-role ๋งŒ (๋น„๋กœ๊ทธ์ธ ์†๋‹˜ ํ๋ฆ„)

CREATE OR REPLACE FUNCTION public._gift_orders_touch_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS gift_orders_touch_updated_at ON public.gift_orders;
CREATE TRIGGER gift_orders_touch_updated_at
    BEFORE UPDATE ON public.gift_orders
    FOR EACH ROW EXECUTE FUNCTION public._gift_orders_touch_updated_at();

COMMENT ON TABLE public.gift_orders IS
    '์นดํŽ˜ ์„ ๋ฌผ ์ฃผ๋ฌธ. P1a stateless (buyer_user_id NULL), mock_paid ๋งŒ coupon ๋ฐœ๊ธ‰. 5๋…„ ๋งŒ๋ฃŒ.';
COMMENT ON COLUMN public.gift_orders.status IS
    'pending โ†’ letter_pending โ†’ payable โ†’ mock_paid (P1a) | paid (P1b) โ†’ refunded/canceled.';
COMMENT ON COLUMN public.gift_orders.expires_at IS
    '๊ตฌ๋งค์ผ + 5๋…„ (๊ณต์ •์œ„ ํ‘œ์ค€์•ฝ๊ด€). ๋ฐฑ์—”๋“œ๊ฐ€ ๋ช…์‹œ ์„ธํŒ… (DB default X).';