844 lines
30 KiB
PL/PgSQL
844 lines
30 KiB
PL/PgSQL
begin;
|
|
|
|
-- Preserve in-flight legacy reservations before the conversational flow starts
|
|
-- using the shared usage ledger. Existing rows were always credit-funded.
|
|
insert into public.usage_reservations (
|
|
id, user_id, request_id, feature_key, source, credit_amount, status,
|
|
reserved_at, completed_at
|
|
)
|
|
select
|
|
b.reservation_id,
|
|
b.user_id,
|
|
'rectification:' || b.case_id::text,
|
|
'rectification',
|
|
'credits',
|
|
b.price,
|
|
case when b.state = 'charged' then 'completed' else 'reserved' end,
|
|
coalesce(b.reserved_at, b.created_at),
|
|
case when b.state = 'charged' then coalesce(b.charged_at, b.updated_at) end
|
|
from public.birth_time_rectification_billing b
|
|
where b.state in ('reserved', 'charged')
|
|
and b.reservation_id is not null
|
|
on conflict (user_id, request_id) do nothing;
|
|
|
|
insert into public.usage_ledger (
|
|
reservation_id, user_id, request_id, feature_key, source, metadata, created_at
|
|
)
|
|
select
|
|
r.id, r.user_id, r.request_id, r.feature_key, r.source,
|
|
'{"legacyConversationalBilling":true}'::jsonb,
|
|
coalesce(r.completed_at, r.reserved_at)
|
|
from public.usage_reservations r
|
|
where r.feature_key = 'rectification'
|
|
and r.request_id like 'rectification:%'
|
|
and r.status = 'completed'
|
|
on conflict (reservation_id) do nothing;
|
|
|
|
create or replace function public.recover_conversational_rectification_orphan_reservations(
|
|
p_user_id uuid,
|
|
p_excluded_case_id uuid default null
|
|
)
|
|
returns integer
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_orphan public.birth_time_rectification_billing%rowtype;
|
|
v_balance integer;
|
|
v_recovery_action_id uuid;
|
|
v_recovery_fingerprint text;
|
|
v_recovery_response jsonb;
|
|
v_release_success boolean;
|
|
v_release_error text;
|
|
begin
|
|
if p_user_id is null then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
perform pg_catalog.pg_advisory_xact_lock(
|
|
pg_catalog.hashtextextended(
|
|
p_user_id::text || ':conversational-rectification-case',
|
|
0
|
|
)
|
|
);
|
|
select profile.credits into v_balance
|
|
from public.profiles profile
|
|
where profile.id = p_user_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
for v_orphan in
|
|
select orphan_billing.*
|
|
from public.birth_time_rectification_billing orphan_billing
|
|
left join public.birth_time_rectification_cases orphan_case
|
|
on orphan_case.id = orphan_billing.case_id
|
|
where orphan_billing.user_id = p_user_id
|
|
and (
|
|
p_excluded_case_id is null
|
|
or orphan_billing.case_id <> p_excluded_case_id
|
|
)
|
|
and orphan_billing.state = 'reserved'
|
|
and orphan_case.id is null
|
|
order by orphan_billing.reserved_at, orphan_billing.case_id
|
|
for update of orphan_billing
|
|
loop
|
|
v_recovery_action_id :=
|
|
public.conversational_rectification_billing_receipt_action_id(
|
|
v_orphan.reserve_action_id,
|
|
'recover_fee'
|
|
);
|
|
v_recovery_fingerprint := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
|
|
pg_catalog.jsonb_build_object(
|
|
'kind', 'recover_fee', 'userId', p_user_id, 'caseId', v_orphan.case_id,
|
|
'expectedVersion', 0, 'actionId', v_recovery_action_id,
|
|
'reserveActionId', v_orphan.reserve_action_id
|
|
)::text,
|
|
'UTF8'
|
|
)), 'hex');
|
|
|
|
select released.success, released.credits, released.error_code
|
|
into v_release_success, v_balance, v_release_error
|
|
from public.release_usage(
|
|
p_user_id,
|
|
'rectification:' || v_orphan.case_id::text,
|
|
'orphaned conversational rectification reservation'
|
|
) released;
|
|
if v_release_success is distinct from true or v_release_error is not null then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
update public.birth_time_rectification_billing orphan_billing
|
|
set state = 'released',
|
|
release_action_id = v_recovery_action_id,
|
|
balance_after = v_balance,
|
|
released_at = pg_catalog.now(),
|
|
updated_at = pg_catalog.now()
|
|
where orphan_billing.case_id = v_orphan.case_id
|
|
and orphan_billing.user_id = p_user_id
|
|
and orphan_billing.state = 'reserved';
|
|
if not found then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
v_recovery_response := pg_catalog.jsonb_build_object(
|
|
'success', true, 'credits', v_balance,
|
|
'billing_state', 'released', 'error_code', null
|
|
);
|
|
insert into public.birth_time_rectification_action_receipts (
|
|
case_id, action_id, user_id, action_kind, expected_turn_version,
|
|
result_turn_version, request_fingerprint, request, response
|
|
) values (
|
|
v_orphan.case_id, v_recovery_action_id, p_user_id, 'recover_fee', 0,
|
|
0, v_recovery_fingerprint,
|
|
public.conversational_rectification_action_request(
|
|
'recover_fee', p_user_id, v_orphan.case_id, 0,
|
|
v_recovery_action_id, v_recovery_fingerprint
|
|
),
|
|
v_recovery_response
|
|
);
|
|
end loop;
|
|
return v_balance;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.reserve_conversational_rectification_fee(
|
|
p_user_id uuid,
|
|
p_case_id uuid,
|
|
p_expected_version bigint,
|
|
p_action_id uuid,
|
|
p_price integer
|
|
)
|
|
returns table (
|
|
success boolean,
|
|
credits integer,
|
|
billing_state text,
|
|
error_code text
|
|
)
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_receipt public.birth_time_rectification_action_receipts%rowtype;
|
|
v_billing public.birth_time_rectification_billing%rowtype;
|
|
v_receipt_action_id uuid :=
|
|
public.conversational_rectification_billing_receipt_action_id(
|
|
p_action_id,
|
|
'reserve_fee'
|
|
);
|
|
v_balance integer;
|
|
v_usage_success boolean;
|
|
v_usage_reservation_id uuid;
|
|
v_usage_reason text;
|
|
v_response jsonb;
|
|
v_fingerprint text := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
|
|
pg_catalog.jsonb_build_object(
|
|
'kind', 'reserve_fee', 'userId', p_user_id, 'caseId', p_case_id,
|
|
'expectedVersion', p_expected_version, 'actionId', p_action_id,
|
|
'price', p_price
|
|
)::text,
|
|
'UTF8'
|
|
)), 'hex');
|
|
begin
|
|
if p_user_id is null or p_case_id is null or p_action_id is null
|
|
or p_expected_version is distinct from 0
|
|
or p_price is null or not (p_price between 1 and 1000000) then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
if p_case_id is distinct from p_action_id then
|
|
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
|
end if;
|
|
|
|
-- The account lock prevents two different start actions from reserving two
|
|
-- fees before either action has created its case. The action lock preserves
|
|
-- exact replay for the same request.
|
|
perform pg_catalog.pg_advisory_xact_lock(
|
|
pg_catalog.hashtextextended(
|
|
p_user_id::text || ':conversational-rectification-case',
|
|
0
|
|
)
|
|
);
|
|
perform pg_catalog.pg_advisory_xact_lock(
|
|
pg_catalog.hashtextextended(p_user_id::text || ':' || p_action_id::text, 0)
|
|
);
|
|
|
|
select r.* into v_receipt
|
|
from public.birth_time_rectification_action_receipts r
|
|
where r.case_id = p_case_id and r.action_id = v_receipt_action_id
|
|
for update;
|
|
if found then
|
|
if v_receipt.user_id is distinct from p_user_id
|
|
or v_receipt.action_kind is distinct from 'reserve_fee'
|
|
or v_receipt.expected_turn_version is distinct from p_expected_version
|
|
or v_receipt.request_fingerprint is distinct from v_fingerprint then
|
|
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
|
end if;
|
|
return query select
|
|
(v_receipt.response ->> 'success')::boolean,
|
|
nullif(v_receipt.response ->> 'credits', '')::integer,
|
|
nullif(v_receipt.response ->> 'billing_state', ''),
|
|
nullif(v_receipt.response ->> 'error_code', '');
|
|
return;
|
|
end if;
|
|
|
|
-- A public action identifies one start attempt even if a buggy caller loses
|
|
-- its case id. Reusing that action for another case is a conflict, never a
|
|
-- second debit.
|
|
select r.* into v_receipt
|
|
from public.birth_time_rectification_action_receipts r
|
|
where r.user_id = p_user_id
|
|
and r.action_id = v_receipt_action_id
|
|
and r.action_kind = 'reserve_fee'
|
|
for update;
|
|
if found then
|
|
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
|
end if;
|
|
|
|
perform 1
|
|
from public.birth_time_rectification_cases active_case
|
|
where active_case.user_id = p_user_id
|
|
and active_case.id <> p_case_id
|
|
and active_case.journey_protocol = 'conversational-evidence-v3'
|
|
and active_case.status in ('starting', 'active', 'paused', 'confirming')
|
|
for update;
|
|
if found then
|
|
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
|
end if;
|
|
|
|
-- Reservation intentionally precedes the external first-turn calculation,
|
|
-- so it cannot share a transaction with case creation. A process/device
|
|
-- loss in that gap leaves no case for the account resume RPC to expose.
|
|
-- A fresh account-scoped start deterministically releases every such orphan
|
|
-- under the same account/profile locks before it attempts another debit.
|
|
v_balance := public.recover_conversational_rectification_orphan_reservations(
|
|
p_user_id,
|
|
p_case_id
|
|
);
|
|
|
|
select b.* into v_billing
|
|
from public.birth_time_rectification_billing b
|
|
where b.case_id = p_case_id
|
|
for update;
|
|
if found then
|
|
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select usage.success, usage.reservation_id, usage.credits, usage.reason
|
|
into v_usage_success, v_usage_reservation_id, v_balance, v_usage_reason
|
|
from public.authorize_usage(
|
|
p_user_id,
|
|
'rectification',
|
|
null,
|
|
'rectification:' || p_case_id::text,
|
|
p_price
|
|
) usage;
|
|
if v_usage_success is distinct from true or v_usage_reservation_id is null then
|
|
v_usage_reason := coalesce(v_usage_reason, 'billing_failed');
|
|
v_response := pg_catalog.jsonb_build_object(
|
|
'success', false, 'credits', v_balance,
|
|
'billing_state', null, 'error_code', v_usage_reason
|
|
);
|
|
insert into public.birth_time_rectification_action_receipts (
|
|
case_id, action_id, user_id, action_kind, expected_turn_version,
|
|
result_turn_version, request_fingerprint, request, response
|
|
) values (
|
|
p_case_id, v_receipt_action_id, p_user_id, 'reserve_fee', 0,
|
|
0, v_fingerprint,
|
|
public.conversational_rectification_action_request(
|
|
'reserve_fee', p_user_id, p_case_id, 0, p_action_id, v_fingerprint
|
|
),
|
|
v_response
|
|
);
|
|
return query select false, v_balance, null::text, v_usage_reason;
|
|
return;
|
|
end if;
|
|
|
|
insert into public.birth_time_rectification_billing (
|
|
case_id, user_id, price, state, reservation_id, reserve_action_id,
|
|
balance_after, reserved_at
|
|
) values (
|
|
p_case_id, p_user_id, p_price, 'reserved',
|
|
v_usage_reservation_id, p_action_id, v_balance, pg_catalog.now()
|
|
);
|
|
|
|
v_response := pg_catalog.jsonb_build_object(
|
|
'success', true, 'credits', v_balance,
|
|
'billing_state', 'reserved', 'error_code', null
|
|
);
|
|
insert into public.birth_time_rectification_action_receipts (
|
|
case_id, action_id, user_id, action_kind, expected_turn_version,
|
|
result_turn_version, request_fingerprint, request, response
|
|
) values (
|
|
p_case_id, v_receipt_action_id, p_user_id, 'reserve_fee', 0,
|
|
0, v_fingerprint,
|
|
public.conversational_rectification_action_request(
|
|
'reserve_fee', p_user_id, p_case_id, 0, p_action_id, v_fingerprint
|
|
),
|
|
v_response
|
|
);
|
|
|
|
return query select true, v_balance, 'reserved'::text, null::text;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.complete_conversational_rectification_fee(
|
|
p_user_id uuid,
|
|
p_case_id uuid,
|
|
p_expected_version bigint,
|
|
p_action_id uuid
|
|
)
|
|
returns table (
|
|
success boolean,
|
|
credits integer,
|
|
billing_state text,
|
|
error_code text
|
|
)
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_case public.birth_time_rectification_cases%rowtype;
|
|
v_receipt public.birth_time_rectification_action_receipts%rowtype;
|
|
v_billing public.birth_time_rectification_billing%rowtype;
|
|
v_receipt_action_id uuid :=
|
|
public.conversational_rectification_billing_receipt_action_id(
|
|
p_action_id,
|
|
'complete_fee'
|
|
);
|
|
v_balance integer;
|
|
v_usage_success boolean;
|
|
v_usage_error text;
|
|
v_response jsonb;
|
|
v_success boolean;
|
|
v_error_code text;
|
|
v_state text;
|
|
v_fingerprint text := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
|
|
pg_catalog.jsonb_build_object(
|
|
'kind', 'complete_fee', 'userId', p_user_id, 'caseId', p_case_id,
|
|
'expectedVersion', p_expected_version, 'actionId', p_action_id
|
|
)::text,
|
|
'UTF8'
|
|
)), 'hex');
|
|
begin
|
|
if p_user_id is null or p_case_id is null or p_action_id is null
|
|
or p_expected_version is null or p_expected_version < 0 then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
if p_case_id is distinct from p_action_id then
|
|
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
|
end if;
|
|
|
|
perform pg_catalog.pg_advisory_xact_lock(
|
|
pg_catalog.hashtextextended(p_user_id::text || ':' || p_action_id::text, 0)
|
|
);
|
|
select c.* into v_case
|
|
from public.birth_time_rectification_cases c
|
|
where c.id = p_case_id and c.user_id = p_user_id
|
|
for update;
|
|
if not found or v_case.journey_protocol is distinct from 'conversational-evidence-v3' then
|
|
raise exception 'conversational_case_not_found' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select r.* into v_receipt
|
|
from public.birth_time_rectification_action_receipts r
|
|
where r.case_id = p_case_id and r.action_id = v_receipt_action_id
|
|
for update;
|
|
if found then
|
|
if v_receipt.user_id is distinct from p_user_id
|
|
or v_receipt.action_kind is distinct from 'complete_fee'
|
|
or v_receipt.expected_turn_version is distinct from p_expected_version
|
|
or v_receipt.request_fingerprint is distinct from v_fingerprint then
|
|
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
|
end if;
|
|
return query select
|
|
(v_receipt.response ->> 'success')::boolean,
|
|
nullif(v_receipt.response ->> 'credits', '')::integer,
|
|
nullif(v_receipt.response ->> 'billing_state', ''),
|
|
nullif(v_receipt.response ->> 'error_code', '');
|
|
return;
|
|
end if;
|
|
if v_case.turn_version is distinct from p_expected_version then
|
|
raise exception 'conversational_stale_turn' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select profile.credits into v_balance
|
|
from public.profiles profile
|
|
where profile.id = p_user_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
select b.* into v_billing
|
|
from public.birth_time_rectification_billing b
|
|
where b.case_id = p_case_id and b.user_id = p_user_id
|
|
for update;
|
|
|
|
if not found then
|
|
v_success := false;
|
|
v_state := null;
|
|
v_error_code := 'billing_missing';
|
|
elsif v_billing.state = 'charged' then
|
|
v_success := true;
|
|
v_state := 'charged';
|
|
v_error_code := null;
|
|
elsif v_billing.state = 'released' then
|
|
v_success := false;
|
|
v_state := 'released';
|
|
v_error_code := 'reservation_released';
|
|
elsif v_billing.state = 'migration_waived' then
|
|
v_success := true;
|
|
v_state := 'migration_waived';
|
|
v_error_code := null;
|
|
else
|
|
select completed.success, completed.credits, completed.error_code
|
|
into v_usage_success, v_balance, v_usage_error
|
|
from public.complete_usage(
|
|
p_user_id,
|
|
'rectification:' || p_case_id::text,
|
|
'{"metadata":{"flow":"conversational_rectification"}}'::jsonb
|
|
) completed;
|
|
if v_usage_success is distinct from true or v_usage_error is not null then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
update public.birth_time_rectification_billing
|
|
set state = 'charged',
|
|
billing_receipt_id = pg_catalog.gen_random_uuid(),
|
|
complete_action_id = p_action_id,
|
|
balance_after = v_balance,
|
|
charged_at = pg_catalog.now(),
|
|
updated_at = pg_catalog.now()
|
|
where case_id = p_case_id and user_id = p_user_id and state = 'reserved';
|
|
if not found then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
v_success := true;
|
|
v_state := 'charged';
|
|
v_error_code := null;
|
|
end if;
|
|
|
|
v_response := pg_catalog.jsonb_build_object(
|
|
'success', v_success, 'credits', v_balance,
|
|
'billing_state', v_state, 'error_code', v_error_code
|
|
);
|
|
insert into public.birth_time_rectification_action_receipts (
|
|
case_id, action_id, user_id, action_kind, expected_turn_version,
|
|
result_turn_version, request_fingerprint, request, response
|
|
) values (
|
|
p_case_id, v_receipt_action_id, p_user_id, 'complete_fee', p_expected_version,
|
|
v_case.turn_version, v_fingerprint,
|
|
public.conversational_rectification_action_request(
|
|
'complete_fee', p_user_id, p_case_id, p_expected_version,
|
|
p_action_id, v_fingerprint
|
|
),
|
|
v_response
|
|
);
|
|
return query select v_success, v_balance, v_state, v_error_code;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.release_conversational_rectification_fee(
|
|
p_user_id uuid,
|
|
p_case_id uuid,
|
|
p_expected_version bigint,
|
|
p_action_id uuid,
|
|
p_price integer
|
|
)
|
|
returns table (
|
|
success boolean,
|
|
credits integer,
|
|
billing_state text,
|
|
error_code text
|
|
)
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_case public.birth_time_rectification_cases%rowtype;
|
|
v_receipt public.birth_time_rectification_action_receipts%rowtype;
|
|
v_billing public.birth_time_rectification_billing%rowtype;
|
|
v_receipt_action_id uuid :=
|
|
public.conversational_rectification_billing_receipt_action_id(
|
|
p_action_id,
|
|
'release_fee'
|
|
);
|
|
v_balance integer;
|
|
v_result_version bigint := p_expected_version;
|
|
v_response jsonb;
|
|
v_usage_success boolean;
|
|
v_usage_error text;
|
|
v_success boolean := true;
|
|
v_error_code text;
|
|
v_state text := 'released';
|
|
v_fingerprint text := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
|
|
pg_catalog.jsonb_build_object(
|
|
'kind', 'release_fee', 'userId', p_user_id, 'caseId', p_case_id,
|
|
'expectedVersion', p_expected_version, 'actionId', p_action_id,
|
|
'price', p_price
|
|
)::text,
|
|
'UTF8'
|
|
)), 'hex');
|
|
begin
|
|
if p_user_id is null or p_case_id is null or p_action_id is null
|
|
or p_expected_version is null or p_expected_version < 0
|
|
or p_price is null or not (p_price between 1 and 1000000) then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
if p_case_id is distinct from p_action_id then
|
|
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
|
end if;
|
|
|
|
perform pg_catalog.pg_advisory_xact_lock(
|
|
pg_catalog.hashtextextended(p_user_id::text || ':' || p_action_id::text, 0)
|
|
);
|
|
|
|
select r.* into v_receipt
|
|
from public.birth_time_rectification_action_receipts r
|
|
where r.case_id = p_case_id and r.action_id = v_receipt_action_id
|
|
for update;
|
|
if found then
|
|
if v_receipt.user_id is distinct from p_user_id
|
|
or v_receipt.action_kind is distinct from 'release_fee'
|
|
or v_receipt.expected_turn_version is distinct from p_expected_version
|
|
or v_receipt.request_fingerprint is distinct from v_fingerprint then
|
|
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
|
end if;
|
|
return query select
|
|
(v_receipt.response ->> 'success')::boolean,
|
|
nullif(v_receipt.response ->> 'credits', '')::integer,
|
|
nullif(v_receipt.response ->> 'billing_state', ''),
|
|
nullif(v_receipt.response ->> 'error_code', '');
|
|
return;
|
|
end if;
|
|
|
|
select c.* into v_case
|
|
from public.birth_time_rectification_cases c
|
|
where c.id = p_case_id
|
|
for update;
|
|
if found then
|
|
if v_case.user_id is distinct from p_user_id
|
|
or v_case.journey_protocol is distinct from 'conversational-evidence-v3' then
|
|
raise exception 'conversational_case_not_found' using errcode = 'P0001';
|
|
end if;
|
|
if v_case.turn_version is distinct from p_expected_version then
|
|
raise exception 'conversational_stale_turn' using errcode = 'P0001';
|
|
end if;
|
|
v_result_version := v_case.turn_version;
|
|
elsif p_expected_version is distinct from 0 then
|
|
raise exception 'conversational_stale_turn' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select profile.credits into v_balance
|
|
from public.profiles profile
|
|
where profile.id = p_user_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
select b.* into v_billing
|
|
from public.birth_time_rectification_billing b
|
|
where b.case_id = p_case_id
|
|
for update;
|
|
|
|
if not found then
|
|
insert into public.birth_time_rectification_billing (
|
|
case_id, user_id, price, state, release_action_id,
|
|
balance_after, released_at
|
|
) values (
|
|
p_case_id, p_user_id, p_price, 'released', p_action_id,
|
|
v_balance, pg_catalog.now()
|
|
);
|
|
elsif v_billing.user_id is distinct from p_user_id then
|
|
raise exception 'conversational_case_not_found' using errcode = 'P0001';
|
|
elsif v_billing.state = 'released' then
|
|
v_state := 'released';
|
|
elsif v_billing.state = 'charged' then
|
|
v_success := false;
|
|
v_state := 'charged';
|
|
v_error_code := 'already_charged';
|
|
elsif v_billing.state = 'migration_waived' then
|
|
v_state := 'migration_waived';
|
|
else
|
|
select released.success, released.credits, released.error_code
|
|
into v_usage_success, v_balance, v_usage_error
|
|
from public.release_usage(
|
|
p_user_id,
|
|
'rectification:' || p_case_id::text,
|
|
'conversational rectification start failed'
|
|
) released;
|
|
if v_usage_success is distinct from true or v_usage_error is not null then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
update public.birth_time_rectification_billing
|
|
set state = 'released',
|
|
release_action_id = p_action_id,
|
|
balance_after = v_balance,
|
|
released_at = pg_catalog.now(),
|
|
updated_at = pg_catalog.now()
|
|
where case_id = p_case_id and user_id = p_user_id and state = 'reserved';
|
|
if not found then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
-- Creation and fee settlement are separate calls. If settlement fails
|
|
-- after the case was created, releasing the reservation must also close
|
|
-- that unfinished case or it would block every later account-level start.
|
|
if v_case.id is not null
|
|
and v_case.status in ('starting', 'active', 'paused', 'confirming') then
|
|
update public.birth_time_rectification_cases c
|
|
set status = 'abandoned',
|
|
turn_state = case
|
|
when pg_catalog.jsonb_typeof(c.turn_state) = 'object'
|
|
then pg_catalog.jsonb_set(
|
|
c.turn_state, '{status}', pg_catalog.to_jsonb('abandoned'::text), true
|
|
)
|
|
else c.turn_state
|
|
end,
|
|
journey_snapshot = case
|
|
when pg_catalog.jsonb_typeof(c.journey_snapshot) = 'object'
|
|
then pg_catalog.jsonb_set(
|
|
c.journey_snapshot, '{status}', pg_catalog.to_jsonb('abandoned'::text), true
|
|
)
|
|
else c.journey_snapshot
|
|
end,
|
|
updated_at = pg_catalog.now()
|
|
where c.id = p_case_id
|
|
and c.user_id = p_user_id
|
|
and c.journey_protocol = 'conversational-evidence-v3'
|
|
and c.turn_version = p_expected_version
|
|
and c.status in ('starting', 'active', 'paused', 'confirming');
|
|
if not found then
|
|
raise exception 'conversational_stale_turn' using errcode = 'P0001';
|
|
end if;
|
|
end if;
|
|
end if;
|
|
|
|
v_response := pg_catalog.jsonb_build_object(
|
|
'success', v_success, 'credits', v_balance,
|
|
'billing_state', v_state, 'error_code', v_error_code
|
|
);
|
|
insert into public.birth_time_rectification_action_receipts (
|
|
case_id, action_id, user_id, action_kind, expected_turn_version,
|
|
result_turn_version, request_fingerprint, request, response
|
|
) values (
|
|
p_case_id, v_receipt_action_id, p_user_id, 'release_fee', p_expected_version,
|
|
v_result_version, v_fingerprint,
|
|
public.conversational_rectification_action_request(
|
|
'release_fee', p_user_id, p_case_id, p_expected_version,
|
|
p_action_id, v_fingerprint
|
|
),
|
|
v_response
|
|
);
|
|
return query select v_success, v_balance, v_state, v_error_code;
|
|
end;
|
|
$$;
|
|
|
|
|
|
create or replace function public.conversational_rectification_refund_completed_usage(
|
|
p_user_id uuid,
|
|
p_case_id uuid,
|
|
p_reason text
|
|
)
|
|
returns integer
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_request_id text := 'rectification:' || p_case_id::text;
|
|
v_reservation public.usage_reservations%rowtype;
|
|
v_balance integer;
|
|
begin
|
|
perform pg_catalog.pg_advisory_xact_lock(
|
|
pg_catalog.hashtextextended(p_user_id::text || ':' || v_request_id, 0)
|
|
);
|
|
select r.* into v_reservation
|
|
from public.usage_reservations r
|
|
where r.user_id = p_user_id and r.request_id = v_request_id
|
|
for update;
|
|
if not found or v_reservation.feature_key is distinct from 'rectification' then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select profile.credits into v_balance
|
|
from public.profiles profile
|
|
where profile.id = p_user_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
if v_reservation.status = 'released' then
|
|
return v_balance;
|
|
end if;
|
|
if v_reservation.status is distinct from 'completed' then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
if v_reservation.source = 'credits' and v_reservation.credit_amount > 0 then
|
|
update public.profiles profile
|
|
set credits = profile.credits + v_reservation.credit_amount,
|
|
updated_at = pg_catalog.clock_timestamp()
|
|
where profile.id = p_user_id
|
|
returning profile.credits into v_balance;
|
|
insert into public.credit_transactions (
|
|
user_id, transaction_type, amount, balance_after, request_id, model
|
|
) values (
|
|
p_user_id, 'refund', v_reservation.credit_amount, v_balance,
|
|
v_request_id, v_reservation.requested_model_id
|
|
) on conflict (user_id, transaction_type, request_id) do nothing;
|
|
end if;
|
|
|
|
update public.usage_reservations
|
|
set status = 'released',
|
|
released_at = pg_catalog.clock_timestamp(),
|
|
release_reason = left(coalesce(p_reason, 'unconfirmed rectification'), 500)
|
|
where id = v_reservation.id and status = 'completed';
|
|
if not found then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
return v_balance;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function public.conversational_rectification_refund_unconfirmed_case(
|
|
p_user_id uuid,
|
|
p_case_id uuid,
|
|
p_action_id uuid
|
|
)
|
|
returns jsonb
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_case public.birth_time_rectification_cases%rowtype;
|
|
v_billing public.birth_time_rectification_billing%rowtype;
|
|
v_balance integer;
|
|
begin
|
|
select c.* into v_case
|
|
from public.birth_time_rectification_cases c
|
|
where c.id = p_case_id and c.user_id = p_user_id
|
|
for update;
|
|
if not found
|
|
or v_case.journey_protocol is distinct from 'conversational-evidence-v3'
|
|
or v_case.status not in ('completed', 'abandoned')
|
|
or v_case.turn_state #>> '{candidate,status}' = 'confirmed' then
|
|
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select profile.credits into v_balance
|
|
from public.profiles profile
|
|
where profile.id = p_user_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select b.* into v_billing
|
|
from public.birth_time_rectification_billing b
|
|
where b.case_id = p_case_id and b.user_id = p_user_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
if v_billing.state = 'charged' then
|
|
v_balance := public.conversational_rectification_refund_completed_usage(
|
|
p_user_id,
|
|
p_case_id,
|
|
'unconfirmed conversational rectification'
|
|
);
|
|
update public.birth_time_rectification_billing
|
|
set state = 'released',
|
|
release_action_id = p_action_id,
|
|
balance_after = v_balance,
|
|
released_at = pg_catalog.now(),
|
|
updated_at = pg_catalog.now()
|
|
where case_id = p_case_id and user_id = p_user_id and state = 'charged';
|
|
if not found then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
elsif v_billing.state not in ('released', 'migration_waived') then
|
|
raise exception 'conversational_billing_failed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
return public.conversational_rectification_case_projection(p_user_id, p_case_id);
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.conversational_rectification_refund_completed_usage(
|
|
uuid, uuid, text
|
|
) from public, anon, authenticated, service_role;
|
|
revoke all on function public.conversational_rectification_refund_unconfirmed_case(
|
|
uuid, uuid, uuid
|
|
) from public, anon, authenticated;
|
|
|
|
revoke all on function public.reserve_conversational_rectification_fee(
|
|
uuid, uuid, bigint, uuid, integer
|
|
) from public, anon, authenticated;
|
|
revoke all on function public.recover_conversational_rectification_orphan_reservations(
|
|
uuid, uuid
|
|
) from public, anon, authenticated, service_role;
|
|
revoke all on function public.complete_conversational_rectification_fee(
|
|
uuid, uuid, bigint, uuid
|
|
) from public, anon, authenticated;
|
|
revoke all on function public.release_conversational_rectification_fee(
|
|
uuid, uuid, bigint, uuid, integer
|
|
) from public, anon, authenticated;
|
|
|
|
grant execute on function public.reserve_conversational_rectification_fee(
|
|
uuid, uuid, bigint, uuid, integer
|
|
) to service_role;
|
|
grant execute on function public.complete_conversational_rectification_fee(
|
|
uuid, uuid, bigint, uuid
|
|
) to service_role;
|
|
grant execute on function public.release_conversational_rectification_fee(
|
|
uuid, uuid, bigint, uuid, integer
|
|
) to service_role;
|
|
|
|
commit;
|