-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path20260809001548_fix_reversal_business_dates.sql
More file actions
249 lines (227 loc) · 7.52 KB
/
Copy path20260809001548_fix_reversal_business_dates.sql
File metadata and controls
249 lines (227 loc) · 7.52 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
-- Palace Elite: use membership-local business dates for reversals and historical accounting.
--
-- Supabase/Postgres runs in UTC. A reversal created after 7 PM Central could
-- therefore receive tomorrow's `current_date`, while the user still considers
-- the activity part of the current business day. This migration gives each
-- membership a canonical IANA timezone, uses it for new reversal dates, and
-- interprets reversal/approval/void timestamps in that same timezone for
-- historical accounting snapshots.
--
-- Existing ledger rows remain physically unchanged. Pre-fix reversal rows are
-- interpreted by their membership-local creation date because the reversal RPC
-- has always meant "reverse now" and did not expose a caller-selected date.
alter table public.memberships
add column if not exists business_timezone text;
update public.memberships
set business_timezone = 'America/Chicago'
where business_timezone is null;
alter table public.memberships
alter column business_timezone set default 'America/Chicago',
alter column business_timezone set not null;
comment on column public.memberships.business_timezone is
'Canonical IANA timezone used to derive membership business dates from timestamps.';
create or replace function public.get_benefit_unit_balances_as_of(
p_membership_id uuid,
p_as_of date
)
returns table(
benefit_grant_id uuid,
benefit_name text,
pool public.benefit_pool,
quantity_kind public.quantity_kind,
ownership_unit_id uuid,
ownership_unit_name text,
allocation_percentage numeric,
allocated_quantity numeric,
ledger_delta numeric,
remaining_quantity numeric
)
language sql
stable
security definer
set search_path = pg_catalog, public
as $$
select
a.benefit_grant_id,
g.name,
g.pool,
g.quantity_kind,
a.ownership_unit_id,
ou.name,
a.allocation_percentage,
a.allocated_quantity,
coalesce(sum(t.quantity_delta), 0::numeric) as ledger_delta,
a.allocated_quantity + coalesce(sum(t.quantity_delta), 0::numeric) as remaining_quantity
from public.benefit_unit_allocations a
join public.benefit_grants g on g.id = a.benefit_grant_id
join public.ownership_units ou on ou.id = a.ownership_unit_id
join public.memberships m on m.id = a.membership_id
left join public.benefit_transactions t
on t.benefit_grant_id = a.benefit_grant_id
and t.ownership_unit_id = a.ownership_unit_id
and (
case
when t.transaction_type = 'reversal'
then timezone(m.business_timezone, t.created_at)::date
else t.effective_date
end
) <= p_as_of
and t.approved_at is not null
and timezone(m.business_timezone, t.approved_at)::date <= p_as_of
and (
t.voided_at is null
or timezone(m.business_timezone, t.voided_at)::date > p_as_of
)
where a.membership_id = p_membership_id
and g.archived_at is null
and public.user_has_membership_access(p_membership_id)
group by a.id, g.id, ou.id, m.id
order by g.pool, g.name, ou.name;
$$;
comment on function public.get_benefit_unit_balances_as_of(uuid, date) is
'Returns ownership-unit benefit positions as of a membership-local business date using append-only approved ledger activity.';
create or replace function public.create_benefit_reversal(
p_transaction_id uuid,
p_reason text,
p_source_reference text default null
)
returns void
language plpgsql
security definer
set search_path = pg_catalog, public
as $$
declare
v_source public.benefit_transactions%rowtype;
v_leg public.benefit_transactions%rowtype;
v_user uuid := auth.uid();
v_reason text := nullif(btrim(p_reason), '');
v_reversal_group uuid;
v_business_timezone text;
v_business_date date;
begin
if v_user is null then
raise exception 'Authentication is required.' using errcode = '42501';
end if;
if v_reason is null then
raise exception 'A reversal reason is required.' using errcode = '23514';
end if;
select *
into v_source
from public.benefit_transactions
where id = p_transaction_id
for update;
if not found then
raise exception 'Benefit transaction % was not found.', p_transaction_id
using errcode = 'P0002';
end if;
if not public.user_is_membership_admin(v_source.membership_id) then
raise exception 'Membership administrator access is required.' using errcode = '42501';
end if;
select m.business_timezone
into v_business_timezone
from public.memberships m
where m.id = v_source.membership_id;
if v_business_timezone is null then
raise exception 'Membership business timezone is not configured.' using errcode = '23514';
end if;
v_business_date := timezone(v_business_timezone, now())::date;
if v_source.status <> 'approved' or v_source.voided_at is not null then
raise exception 'Only an approved, non-voided transaction can be reversed.'
using errcode = '23514';
end if;
if v_source.transaction_type = 'reversal' then
raise exception 'A reversal cannot directly reverse another reversal.'
using errcode = '23514';
end if;
if v_source.transaction_type = 'transfer' then
if v_source.transaction_group_id is null then
raise exception 'Transfer transaction % is missing its transaction group.', p_transaction_id
using errcode = '23514';
end if;
v_reversal_group := gen_random_uuid();
for v_leg in
select *
from public.benefit_transactions
where transaction_group_id = v_source.transaction_group_id
order by id
for update
loop
if v_leg.status <> 'approved' or v_leg.voided_at is not null then
raise exception 'Every transfer leg must be approved and non-voided before reversal.'
using errcode = '23514';
end if;
insert into public.benefit_transactions (
membership_id,
ownership_unit_id,
benefit_grant_id,
reservation_id,
transaction_type,
quantity_delta,
effective_date,
face_value,
economic_value,
status,
notes,
source_reference,
related_transaction_id,
transaction_group_id,
created_by
) values (
v_leg.membership_id,
v_leg.ownership_unit_id,
v_leg.benefit_grant_id,
v_leg.reservation_id,
'reversal',
-v_leg.quantity_delta,
v_business_date,
v_leg.face_value,
v_leg.economic_value,
'submitted',
v_reason,
nullif(btrim(p_source_reference), ''),
v_leg.id,
v_reversal_group,
v_user
);
end loop;
else
insert into public.benefit_transactions (
membership_id,
ownership_unit_id,
benefit_grant_id,
reservation_id,
transaction_type,
quantity_delta,
effective_date,
face_value,
economic_value,
status,
notes,
source_reference,
related_transaction_id,
created_by
) values (
v_source.membership_id,
v_source.ownership_unit_id,
v_source.benefit_grant_id,
v_source.reservation_id,
'reversal',
-v_source.quantity_delta,
v_business_date,
v_source.face_value,
v_source.economic_value,
'submitted',
v_reason,
nullif(btrim(p_source_reference), ''),
v_source.id,
v_user
);
end if;
end;
$$;
revoke execute on function public.create_benefit_reversal(uuid, text, text)
from public, anon;
grant execute on function public.create_benefit_reversal(uuid, text, text)
to authenticated;
comment on function public.create_benefit_reversal(uuid, text, text) is
'Creates an append-only reversal using the membership business timezone for effective_date.';