-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgroup_transactions_setup.sql
More file actions
384 lines (357 loc) · 12 KB
/
Copy pathgroup_transactions_setup.sql
File metadata and controls
384 lines (357 loc) · 12 KB
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
-- Create group transactions table
CREATE TABLE IF NOT EXISTS public.group_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
group_id UUID NOT NULL REFERENCES public.groups(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
amount NUMERIC NOT NULL CHECK (amount > 0),
description TEXT NOT NULL,
date DATE NOT NULL,
is_expense BOOLEAN NOT NULL DEFAULT TRUE,
category TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Group transaction members table
CREATE TABLE IF NOT EXISTS public.group_transaction_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
transaction_id UUID NOT NULL REFERENCES public.group_transactions(id) ON DELETE CASCADE,
member_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
UNIQUE(transaction_id, member_id)
);
-- Add basic RLS policies
ALTER TABLE public.group_transactions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.group_transaction_members ENABLE ROW LEVEL SECURITY;
-- Clear existing policies
DROP POLICY IF EXISTS "Group members can view group transactions" ON public.group_transactions;
DROP POLICY IF EXISTS "Group members can view transaction participants" ON public.group_transaction_members;
DROP POLICY IF EXISTS "Group members can add transactions" ON public.group_transactions;
DROP POLICY IF EXISTS "Group members can add transaction participants" ON public.group_transaction_members;
DROP POLICY IF EXISTS "Group members can update their own transactions" ON public.group_transactions;
DROP POLICY IF EXISTS "Users can delete their own transactions or owners can delete all" ON public.group_transactions;
DROP POLICY IF EXISTS "Transaction owner or group owners can delete participants" ON public.group_transaction_members;
-- Group members can view transactions in their groups
CREATE POLICY "Group members can view group transactions"
ON public.group_transactions FOR SELECT
USING (EXISTS (
SELECT 1 FROM public.group_members
WHERE group_members.group_id = group_transactions.group_id
AND group_members.user_id = auth.uid()
));
-- Group members can view transaction participants
CREATE POLICY "Group members can view transaction participants"
ON public.group_transaction_members FOR SELECT
USING (EXISTS (
SELECT 1 FROM public.group_transactions
JOIN public.group_members
ON group_members.group_id = group_transactions.group_id
WHERE group_transactions.id = group_transaction_members.transaction_id
AND group_members.user_id = auth.uid()
));
-- Group members can add transactions to their groups
CREATE POLICY "Group members can add transactions"
ON public.group_transactions FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM public.group_members
WHERE group_members.group_id = group_transactions.group_id
AND group_members.user_id = auth.uid()
)
AND auth.uid() = user_id
);
-- Group members can add transaction participants
CREATE POLICY "Group members can add transaction participants"
ON public.group_transaction_members FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM public.group_transactions
JOIN public.group_members
ON group_members.group_id = group_transactions.group_id
WHERE group_transactions.id = group_transaction_members.transaction_id
AND group_members.user_id = auth.uid()
AND (group_transactions.user_id = auth.uid() OR group_members.role = 'owner')
)
);
-- Group members can update their own transactions
CREATE POLICY "Group members can update their own transactions"
ON public.group_transactions FOR UPDATE
USING (auth.uid() = user_id);
-- Users can delete their own transactions, or group owners can delete any
CREATE POLICY "Users can delete their own transactions or owners can delete all"
ON public.group_transactions FOR DELETE
USING (
auth.uid() = user_id
OR EXISTS (
SELECT 1 FROM public.group_members
WHERE group_members.group_id = group_transactions.group_id
AND group_members.user_id = auth.uid()
AND group_members.role = 'owner'
)
);
-- Transaction creators or group owners can delete participants
CREATE POLICY "Transaction owner or group owners can delete participants"
ON public.group_transaction_members FOR DELETE
USING (
EXISTS (
SELECT 1 FROM public.group_transactions
JOIN public.group_members
ON group_members.group_id = group_transactions.group_id
WHERE group_transactions.id = group_transaction_members.transaction_id
AND (
group_transactions.user_id = auth.uid() OR
(group_members.user_id = auth.uid() AND group_members.role = 'owner')
)
)
);
-- RPC Functions
-- Clear existing functions
DROP FUNCTION IF EXISTS public.get_group_transactions(UUID);
DROP FUNCTION IF EXISTS public.get_transaction_members(UUID);
DROP FUNCTION IF EXISTS public.add_group_transaction(UUID, UUID, NUMERIC, TEXT, DATE, BOOLEAN, TEXT, UUID[]);
-- Function to get group transactions
CREATE OR REPLACE FUNCTION public.get_group_transactions(group_id_param UUID)
RETURNS SETOF public.group_transactions
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- Check if calling user has access to this group
IF NOT EXISTS (
SELECT 1 FROM public.group_members
WHERE group_members.group_id = group_id_param
AND group_members.user_id = auth.uid()
) THEN
RAISE EXCEPTION 'You do not have access to this group';
END IF;
-- Return transactions
RETURN QUERY
SELECT * FROM public.group_transactions
WHERE group_id = group_id_param
ORDER BY date DESC, created_at DESC;
END;
$$;
-- Function to get transaction members
CREATE OR REPLACE FUNCTION public.get_transaction_members(transaction_id_param UUID)
RETURNS TABLE(
id UUID,
transaction_id UUID,
member_id UUID,
created_at TIMESTAMPTZ,
first_name TEXT,
last_name TEXT
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- Check if calling user has access to this transaction
IF NOT EXISTS (
SELECT 1
FROM public.group_transactions gt
JOIN public.group_members gm ON gm.group_id = gt.group_id
WHERE gt.id = transaction_id_param
AND gm.user_id = auth.uid()
) THEN
RAISE EXCEPTION 'You do not have access to this transaction';
END IF;
-- Return participants and profile information
RETURN QUERY
SELECT
gtm.id,
gtm.transaction_id,
gtm.member_id,
gtm.created_at,
p.first_name,
p.last_name
FROM
public.group_transaction_members gtm
LEFT JOIN
public.profiles p ON p.id = gtm.member_id
WHERE
gtm.transaction_id = transaction_id_param;
END;
$$;
-- Secure function to add a group transaction
CREATE OR REPLACE FUNCTION public.add_group_transaction(
p_group_id UUID,
p_user_id UUID,
p_amount NUMERIC,
p_description TEXT,
p_date DATE,
p_is_expense BOOLEAN DEFAULT TRUE,
p_category TEXT DEFAULT NULL,
p_member_ids UUID[] DEFAULT NULL
)
RETURNS public.group_transactions
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_new_transaction public.group_transactions;
v_member_id UUID;
BEGIN
-- Verify user is creating transaction for themselves
IF p_user_id != auth.uid() THEN
RAISE EXCEPTION 'You can only add transactions for yourself';
END IF;
-- Check that user is a member of the group
IF NOT EXISTS (
SELECT 1 FROM public.group_members
WHERE group_members.group_id = p_group_id
AND group_members.user_id = auth.uid()
) THEN
RAISE EXCEPTION 'You must be a member of this group';
END IF;
-- Validate amount
IF p_amount <= 0 THEN
RAISE EXCEPTION 'Transaction amount must be greater than zero';
END IF;
-- Add the transaction
INSERT INTO public.group_transactions (
group_id,
user_id,
amount,
description,
date,
is_expense,
category
) VALUES (
p_group_id,
p_user_id,
p_amount,
p_description,
p_date,
p_is_expense,
p_category
)
RETURNING * INTO v_new_transaction;
-- If participants specified, add them
IF p_member_ids IS NOT NULL AND array_length(p_member_ids, 1) > 0 THEN
FOREACH v_member_id IN ARRAY p_member_ids
LOOP
-- Check that member is in the group
IF EXISTS (
SELECT 1 FROM public.group_members
WHERE group_members.group_id = p_group_id
AND group_members.user_id = v_member_id
) THEN
-- Insert the member
INSERT INTO public.group_transaction_members (transaction_id, member_id)
VALUES (v_new_transaction.id, v_member_id);
ELSE
RAISE WARNING 'Member % is not in the group, skipping', v_member_id;
END IF;
END LOOP;
END IF;
RETURN v_new_transaction;
END;
$$;
-- Function to calculate group settlements (who owes whom)
CREATE OR REPLACE FUNCTION public.calculate_group_settlement(group_id_param UUID)
RETURNS JSON
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_result JSON;
BEGIN
-- Çağıran kullanıcının bu gruba erişimi olup olmadığını kontrol edin
IF NOT EXISTS (
SELECT 1 FROM public.group_members
WHERE group_members.group_id = group_id_param
AND group_members.user_id = auth.uid()
) THEN
RAISE EXCEPTION 'Bu gruba erişiminiz yok';
END IF;
-- Basit debug için log ekleyin
RAISE NOTICE 'Grup hesaplaşması hesaplanıyor: %', group_id_param;
-- Güncellenmiş hesaplaşma sorgusu - daha basitleştirilmiş
WITH expense_payments AS (
-- Her bir harcama için kim ödedi
SELECT
gt.user_id AS payer_id,
gt.amount,
gt.id AS transaction_id
FROM
public.group_transactions gt
WHERE
gt.group_id = group_id_param
AND gt.is_expense = TRUE
),
expense_shares AS (
-- Her bir harcamanın kimlere paylaştırıldığı
SELECT
ep.transaction_id,
ep.payer_id,
gtm.member_id AS beneficiary_id,
ep.amount,
ep.amount / COUNT(*) OVER (PARTITION BY ep.transaction_id) AS share_amount
FROM
expense_payments ep
JOIN
public.group_transaction_members gtm ON ep.transaction_id = gtm.transaction_id
),
user_balances AS (
-- Kullanıcı bazında ödenen ve borçlu olunan miktarlar
SELECT
u.user_id,
COALESCE(SUM(CASE WHEN u.user_id = es.payer_id THEN es.amount ELSE 0 END), 0) AS paid_total,
COALESCE(SUM(CASE WHEN u.user_id = es.beneficiary_id THEN es.share_amount ELSE 0 END), 0) AS owed_total
FROM
(SELECT DISTINCT user_id FROM public.group_members WHERE group_id = group_id_param) u
LEFT JOIN
expense_shares es ON (u.user_id = es.payer_id OR u.user_id = es.beneficiary_id)
GROUP BY
u.user_id
),
net_balances AS (
-- Net bakiyeler (pozitif: alacaklı, negatif: borçlu)
SELECT
user_id,
paid_total - owed_total AS balance
FROM
user_balances
),
debts AS (
-- Kimin kime ne kadar ödeyeceği
SELECT
debtors.user_id AS from_user_id,
creditors.user_id AS to_user_id,
LEAST(ABS(debtors.balance), creditors.balance) AS amount
FROM
(SELECT user_id, balance FROM net_balances WHERE balance < 0) debtors
CROSS JOIN
(SELECT user_id, balance FROM net_balances WHERE balance > 0) creditors
WHERE
ABS(debtors.balance) > 0 AND creditors.balance > 0
ORDER BY
ABS(debtors.balance) DESC, creditors.balance DESC
)
-- JSON sonucunu formatlama
SELECT
COALESCE(
json_agg(
json_build_object(
'from_user_id', d.from_user_id,
'to_user_id', d.to_user_id,
'amount', ROUND(d.amount::numeric, 2),
'from_user_name', COALESCE(p1.first_name || ' ' || p1.last_name, 'Kullanıcı ' || d.from_user_id),
'to_user_name', COALESCE(p2.first_name || ' ' || p2.last_name, 'Kullanıcı ' || d.to_user_id)
)
),
'[]'::JSON
) INTO v_result
FROM
debts d
LEFT JOIN
profiles p1 ON d.from_user_id = p1.id
LEFT JOIN
profiles p2 ON d.to_user_id = p2.id;
-- Debug için sonucu kaydedin
RAISE NOTICE 'Hesaplaşma sonucu: %', v_result;
RETURN COALESCE(v_result, '[]'::JSON);
END;
$$;