Add cash management schema and immediate variance alerts
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
-- =====================================================================
|
||||
-- Migration 0005 — Inventory and e-float (roadmap Step 6).
|
||||
--
|
||||
-- Two parallel stock systems for a cell shop:
|
||||
--
|
||||
-- 1. Physical inventory: scratch cards (with serials), SIMs, phones,
|
||||
-- accessories. Voucher serials track per-card lifecycle so the
|
||||
-- same card can never be sold twice and "lost" cards are visible.
|
||||
--
|
||||
-- 2. Electronic float: OMT cash float, Alfa/touch e-recharge wallet,
|
||||
-- whish, etc. Every recharge or transfer must move e-float in
|
||||
-- lockstep with cash, otherwise reconciliation fails.
|
||||
--
|
||||
-- Threat-model rows addressed: 3, 4, 5, 13, 21, 22.
|
||||
-- =====================================================================
|
||||
|
||||
-- =====================================================================
|
||||
-- Items and physical stock
|
||||
-- =====================================================================
|
||||
do $$ begin
|
||||
create type app.item_type as enum (
|
||||
'scratch_card', 'sim', 'phone', 'accessory', 'consumable'
|
||||
);
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type app.stock_movement_type as enum (
|
||||
'purchase_in', -- received from distributor
|
||||
'sale_out', -- linked to a transaction
|
||||
'return_in', -- customer return
|
||||
'damaged_out', -- write-off (manager approval)
|
||||
'lost_out', -- write-off (manager approval)
|
||||
'transfer_in', -- between shops
|
||||
'transfer_out',
|
||||
'adjustment_in', -- audited correction
|
||||
'adjustment_out'
|
||||
);
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type app.voucher_status as enum (
|
||||
'in_stock', 'sold', 'damaged', 'lost', 'returned'
|
||||
);
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
create table if not exists app.items (
|
||||
sku text primary key,
|
||||
name text not null,
|
||||
type app.item_type not null,
|
||||
operator text, -- 'ALFA','TOUCH', null for non-recharge
|
||||
face_value_usd numeric(14,2), -- recharge denomination if applicable
|
||||
cost_usd numeric(14,2) not null check (cost_usd >= 0),
|
||||
price_usd numeric(14,2) not null check (price_usd >= 0),
|
||||
is_active boolean not null default true,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- Per-shop stock-on-hand counter (denormalized, kept in sync by trigger).
|
||||
create table if not exists app.stock_on_hand (
|
||||
sku text not null references app.items(sku),
|
||||
shop_id uuid not null references app.shops(id),
|
||||
qty integer not null default 0 check (qty >= 0),
|
||||
primary key (sku, shop_id)
|
||||
);
|
||||
|
||||
create table if not exists app.stock_lots (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
sku text not null references app.items(sku),
|
||||
shop_id uuid not null references app.shops(id),
|
||||
received_at timestamptz not null default now(),
|
||||
qty_received integer not null check (qty_received > 0),
|
||||
unit_cost_usd numeric(14,2) not null check (unit_cost_usd >= 0),
|
||||
supplier text,
|
||||
invoice_no text,
|
||||
received_by uuid not null references auth.users(id) default auth.uid(),
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index if not exists idx_stock_lots_sku_shop on app.stock_lots(sku, shop_id);
|
||||
|
||||
-- Append-only stock movements ledger -----------------------------------
|
||||
create table if not exists app.stock_movements (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
sku text not null references app.items(sku),
|
||||
shop_id uuid not null references app.shops(id),
|
||||
shift_id uuid references app.shifts(id),
|
||||
type app.stock_movement_type not null,
|
||||
-- Signed: positive = +stock (purchase_in, return_in, transfer_in, adjustment_in)
|
||||
-- negative = -stock (sale_out, damaged_out, lost_out, transfer_out, adjustment_out)
|
||||
qty_delta integer not null check (qty_delta <> 0),
|
||||
ref_txn_id uuid references app.transactions(id),
|
||||
ref_lot_id uuid references app.stock_lots(id),
|
||||
approved_by uuid references auth.users(id), -- required for damaged/lost/adjustment
|
||||
reason text,
|
||||
created_at timestamptz not null default now(),
|
||||
created_by uuid not null references auth.users(id) default auth.uid()
|
||||
);
|
||||
create index if not exists idx_stock_mov_sku_shop on app.stock_movements(sku, shop_id, created_at desc);
|
||||
create index if not exists idx_stock_mov_txn on app.stock_movements(ref_txn_id);
|
||||
|
||||
-- =====================================================================
|
||||
-- Voucher inventory (per-serial lifecycle)
|
||||
-- =====================================================================
|
||||
create table if not exists app.voucher_inventory (
|
||||
serial text primary key,
|
||||
sku text not null references app.items(sku),
|
||||
shop_id uuid not null references app.shops(id),
|
||||
lot_id uuid references app.stock_lots(id),
|
||||
status app.voucher_status not null default 'in_stock',
|
||||
received_at timestamptz not null default now(),
|
||||
sold_txn_id uuid references app.transactions(id),
|
||||
sold_at timestamptz,
|
||||
status_changed_by uuid references auth.users(id),
|
||||
status_change_reason text,
|
||||
constraint voucher_status_consistency check (
|
||||
(status = 'in_stock' and sold_txn_id is null and sold_at is null)
|
||||
or (status = 'sold' and sold_txn_id is not null and sold_at is not null)
|
||||
or (status in ('damaged','lost','returned')
|
||||
and sold_txn_id is null and sold_at is null)
|
||||
)
|
||||
);
|
||||
create index if not exists idx_voucher_status on app.voucher_inventory(status);
|
||||
create index if not exists idx_voucher_sku_shop on app.voucher_inventory(sku, shop_id);
|
||||
|
||||
-- =====================================================================
|
||||
-- E-float (OMT cash float, Alfa e-recharge wallet, etc.)
|
||||
-- =====================================================================
|
||||
do $$ begin
|
||||
create type app.float_provider as enum (
|
||||
'OMT_CASH', 'OMT_DIGITAL', 'ALFA_ERECHARGE', 'TOUCH_ERECHARGE',
|
||||
'OGERO_ERECHARGE', 'WHISH', 'CARD_TERMINAL', 'BANK'
|
||||
);
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
create table if not exists app.floats (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
shop_id uuid not null references app.shops(id) on delete restrict,
|
||||
provider app.float_provider not null,
|
||||
currency app.currency_code not null,
|
||||
is_active boolean not null default true,
|
||||
created_at timestamptz not null default now(),
|
||||
unique (shop_id, provider, currency)
|
||||
);
|
||||
|
||||
-- Cached balance per float, kept in sync by the movements trigger.
|
||||
create table if not exists app.float_balances (
|
||||
float_id uuid primary key references app.floats(id) on delete cascade,
|
||||
balance numeric(20,2) not null default 0,
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists app.float_movements (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
float_id uuid not null references app.floats(id) on delete restrict,
|
||||
shift_id uuid references app.shifts(id),
|
||||
occurred_at timestamptz not null default now(),
|
||||
-- Signed: + adds to e-float, - removes from it.
|
||||
amount numeric(20,2) not null check (amount <> 0),
|
||||
ref_txn_id uuid references app.transactions(id),
|
||||
ref_settlement_id uuid, -- FK added in 0007
|
||||
reason text,
|
||||
created_at timestamptz not null default now(),
|
||||
created_by uuid not null references auth.users(id) default auth.uid()
|
||||
);
|
||||
create index if not exists idx_float_mov_float on app.float_movements(float_id, occurred_at);
|
||||
create index if not exists idx_float_mov_txn on app.float_movements(ref_txn_id);
|
||||
|
||||
-- =====================================================================
|
||||
-- Triggers — append-only, balance maintenance, no negative stock
|
||||
-- =====================================================================
|
||||
|
||||
-- Stock movements: append-only.
|
||||
create or replace function app._stock_mov_no_update_delete()
|
||||
returns trigger language plpgsql as $$
|
||||
begin raise exception 'stock_movements is append-only'; end;
|
||||
$$;
|
||||
drop trigger if exists trg_stock_mov_freeze on app.stock_movements;
|
||||
create trigger trg_stock_mov_freeze before update or delete on app.stock_movements
|
||||
for each row execute function app._stock_mov_no_update_delete();
|
||||
|
||||
-- Stock movements: server-stamped, sign matches type, optional manager
|
||||
-- approval enforced for write-offs.
|
||||
create or replace function app._stock_mov_before_insert()
|
||||
returns trigger language plpgsql as $$
|
||||
begin
|
||||
new.created_at := now();
|
||||
new.created_by := auth.uid();
|
||||
|
||||
-- Sign / type consistency.
|
||||
if new.type in ('purchase_in','return_in','transfer_in','adjustment_in')
|
||||
and new.qty_delta <= 0 then
|
||||
raise exception '% must have qty_delta > 0', new.type;
|
||||
end if;
|
||||
if new.type in ('sale_out','damaged_out','lost_out','transfer_out','adjustment_out')
|
||||
and new.qty_delta >= 0 then
|
||||
raise exception '% must have qty_delta < 0', new.type;
|
||||
end if;
|
||||
|
||||
-- Write-offs and adjustments need manager approval.
|
||||
if new.type in ('damaged_out','lost_out','adjustment_in','adjustment_out')
|
||||
and new.approved_by is null then
|
||||
raise exception '% requires manager approval (approved_by)', new.type;
|
||||
end if;
|
||||
|
||||
-- sale_out must reference a real, completed sale of the same shop.
|
||||
if new.type = 'sale_out' then
|
||||
if new.ref_txn_id is null then
|
||||
raise exception 'sale_out requires ref_txn_id';
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from app.transactions
|
||||
where id = new.ref_txn_id and shop_id = new.shop_id and status = 'completed'
|
||||
) then
|
||||
raise exception 'sale_out must reference a completed txn in the same shop';
|
||||
end if;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
drop trigger if exists trg_stock_mov_before_insert on app.stock_movements;
|
||||
create trigger trg_stock_mov_before_insert before insert on app.stock_movements
|
||||
for each row execute function app._stock_mov_before_insert();
|
||||
|
||||
-- Maintain stock_on_hand. No negative balance allowed.
|
||||
create or replace function app._stock_on_hand_apply()
|
||||
returns trigger language plpgsql as $$
|
||||
begin
|
||||
insert into app.stock_on_hand(sku, shop_id, qty)
|
||||
values (new.sku, new.shop_id, new.qty_delta)
|
||||
on conflict (sku, shop_id) do update
|
||||
set qty = app.stock_on_hand.qty + new.qty_delta;
|
||||
-- Re-check; the CHECK on the table will already reject negatives but
|
||||
-- give a clearer error here.
|
||||
if (select qty from app.stock_on_hand
|
||||
where sku = new.sku and shop_id = new.shop_id) < 0 then
|
||||
raise exception 'stock would go negative for sku=% shop=%', new.sku, new.shop_id;
|
||||
end if;
|
||||
return null;
|
||||
end;
|
||||
$$;
|
||||
drop trigger if exists trg_stock_on_hand_apply on app.stock_movements;
|
||||
create trigger trg_stock_on_hand_apply after insert on app.stock_movements
|
||||
for each row execute function app._stock_on_hand_apply();
|
||||
|
||||
-- Stock_lots: receiving stock auto-creates a purchase_in movement.
|
||||
create or replace function app._stock_lot_after_insert()
|
||||
returns trigger language plpgsql as $$
|
||||
begin
|
||||
insert into app.stock_movements(sku, shop_id, type, qty_delta, ref_lot_id, reason)
|
||||
values (new.sku, new.shop_id, 'purchase_in', new.qty_received, new.id,
|
||||
coalesce('lot ' || new.invoice_no, 'lot received'));
|
||||
return null;
|
||||
end;
|
||||
$$;
|
||||
drop trigger if exists trg_stock_lot_after_insert on app.stock_lots;
|
||||
create trigger trg_stock_lot_after_insert after insert on app.stock_lots
|
||||
for each row execute function app._stock_lot_after_insert();
|
||||
|
||||
-- Float movements: append-only + balance.
|
||||
create or replace function app._float_mov_no_update_delete()
|
||||
returns trigger language plpgsql as $$
|
||||
begin raise exception 'float_movements is append-only'; end;
|
||||
$$;
|
||||
drop trigger if exists trg_float_mov_freeze on app.float_movements;
|
||||
create trigger trg_float_mov_freeze before update or delete on app.float_movements
|
||||
for each row execute function app._float_mov_no_update_delete();
|
||||
|
||||
create or replace function app._float_balance_apply()
|
||||
returns trigger language plpgsql as $$
|
||||
begin
|
||||
insert into app.float_balances(float_id, balance, updated_at)
|
||||
values (new.float_id, new.amount, now())
|
||||
on conflict (float_id) do update
|
||||
set balance = app.float_balances.balance + new.amount,
|
||||
updated_at = now();
|
||||
if (select balance from app.float_balances where float_id = new.float_id) < 0 then
|
||||
raise exception 'float would go negative for float_id=%', new.float_id;
|
||||
end if;
|
||||
return null;
|
||||
end;
|
||||
$$;
|
||||
drop trigger if exists trg_float_balance_apply on app.float_movements;
|
||||
create trigger trg_float_balance_apply after insert on app.float_movements
|
||||
for each row execute function app._float_balance_apply();
|
||||
|
||||
-- =====================================================================
|
||||
-- Recharge ↔ stock/float coupling
|
||||
-- A recharge_details row MUST move either physical stock (voucher) or
|
||||
-- e-float, otherwise it is a free recharge — exactly the fraud we want
|
||||
-- to make impossible (vectors #3, #21).
|
||||
-- Implemented as a deferred constraint trigger so the client can write
|
||||
-- the recharge row first, then the movement, in a single transaction.
|
||||
-- =====================================================================
|
||||
create or replace function app._recharge_require_movement()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
has_voucher_movement boolean;
|
||||
has_float_movement boolean;
|
||||
v_provider app.float_provider;
|
||||
begin
|
||||
if new.voucher_serial is not null then
|
||||
-- The voucher must be marked sold and tied to this txn.
|
||||
select exists(
|
||||
select 1 from app.voucher_inventory
|
||||
where serial = new.voucher_serial
|
||||
and status = 'sold'
|
||||
and sold_txn_id = new.txn_id
|
||||
) into has_voucher_movement;
|
||||
if not has_voucher_movement then
|
||||
raise exception
|
||||
'recharge with voucher_serial=% must be paired with a sold voucher',
|
||||
new.voucher_serial;
|
||||
end if;
|
||||
else
|
||||
-- E-recharge: an e-float debit must exist for this txn against the
|
||||
-- matching operator's e-float account.
|
||||
v_provider := case new.operator
|
||||
when 'ALFA' then 'ALFA_ERECHARGE'::app.float_provider
|
||||
when 'TOUCH' then 'TOUCH_ERECHARGE'::app.float_provider
|
||||
when 'OGERO' then 'OGERO_ERECHARGE'::app.float_provider
|
||||
else null
|
||||
end;
|
||||
if v_provider is null then
|
||||
-- Unmapped operator (IDM, CYBERIA, TERRANET): require any negative
|
||||
-- float movement for this txn.
|
||||
select exists(
|
||||
select 1 from app.float_movements
|
||||
where ref_txn_id = new.txn_id and amount < 0
|
||||
) into has_float_movement;
|
||||
else
|
||||
select exists(
|
||||
select 1
|
||||
from app.float_movements fm
|
||||
join app.floats f on f.id = fm.float_id
|
||||
join app.transactions t on t.id = fm.ref_txn_id
|
||||
where fm.ref_txn_id = new.txn_id
|
||||
and fm.amount < 0
|
||||
and f.provider = v_provider
|
||||
and f.shop_id = t.shop_id
|
||||
) into has_float_movement;
|
||||
end if;
|
||||
if not has_float_movement then
|
||||
raise exception
|
||||
'e-recharge txn % must be paired with a negative e-float movement',
|
||||
new.txn_id;
|
||||
end if;
|
||||
end if;
|
||||
return null;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_recharge_require_movement on app.recharge_details;
|
||||
create constraint trigger trg_recharge_require_movement
|
||||
after insert on app.recharge_details
|
||||
deferrable initially deferred
|
||||
for each row execute function app._recharge_require_movement();
|
||||
|
||||
-- Goods sale ↔ stock_movement coupling (same idea).
|
||||
create or replace function app._goods_sale_require_movement()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare ok boolean;
|
||||
begin
|
||||
select exists(
|
||||
select 1 from app.stock_movements sm
|
||||
where sm.ref_txn_id = new.txn_id
|
||||
and sm.sku = new.sku
|
||||
and sm.type = 'sale_out'
|
||||
and -sm.qty_delta = new.qty
|
||||
) into ok;
|
||||
if not ok then
|
||||
raise exception 'goods sale txn % must be paired with a sale_out stock movement', new.txn_id;
|
||||
end if;
|
||||
return null;
|
||||
end;
|
||||
$$;
|
||||
drop trigger if exists trg_goods_sale_require_movement on app.goods_sale_details;
|
||||
create constraint trigger trg_goods_sale_require_movement
|
||||
after insert on app.goods_sale_details
|
||||
deferrable initially deferred
|
||||
for each row execute function app._goods_sale_require_movement();
|
||||
|
||||
-- =====================================================================
|
||||
-- SECURITY DEFINER helpers used by the cashier UI
|
||||
-- =====================================================================
|
||||
|
||||
-- Sell a scratch card: marks the voucher sold + creates the stock_out.
|
||||
-- Called inside the same transaction as inserting the txn + recharge_details.
|
||||
create or replace function app.sell_voucher(
|
||||
p_txn_id uuid,
|
||||
p_serial text
|
||||
) returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
as $$
|
||||
declare v app.voucher_inventory%rowtype;
|
||||
t app.transactions%rowtype;
|
||||
begin
|
||||
select * into t from app.transactions where id = p_txn_id;
|
||||
if t.id is null then raise exception 'txn not found'; end if;
|
||||
if t.user_id <> auth.uid() then
|
||||
raise exception 'only the txn owner may sell a voucher against it';
|
||||
end if;
|
||||
|
||||
select * into v from app.voucher_inventory where serial = p_serial for update;
|
||||
if v.serial is null then raise exception 'voucher % not found', p_serial; end if;
|
||||
if v.shop_id <> t.shop_id then
|
||||
raise exception 'voucher belongs to a different shop';
|
||||
end if;
|
||||
if v.status <> 'in_stock' then
|
||||
raise exception 'voucher % is not in_stock (status=%)', p_serial, v.status;
|
||||
end if;
|
||||
|
||||
update app.voucher_inventory
|
||||
set status = 'sold', sold_txn_id = p_txn_id, sold_at = now(),
|
||||
status_changed_by = auth.uid()
|
||||
where serial = p_serial;
|
||||
|
||||
insert into app.stock_movements(sku, shop_id, shift_id, type, qty_delta, ref_txn_id, reason)
|
||||
values (v.sku, v.shop_id, t.shift_id, 'sale_out', -1, p_txn_id, 'voucher ' || p_serial);
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Mark a voucher damaged or lost (manager only, with PIN).
|
||||
create or replace function app.write_off_voucher(
|
||||
p_serial text,
|
||||
p_status app.voucher_status,
|
||||
p_reason text,
|
||||
p_manager_pin text
|
||||
) returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = app, public
|
||||
as $$
|
||||
declare v app.voucher_inventory%rowtype;
|
||||
begin
|
||||
if p_status not in ('damaged','lost') then
|
||||
raise exception 'only damaged/lost are valid write-off statuses';
|
||||
end if;
|
||||
if p_reason is null or length(btrim(p_reason)) < 5 then
|
||||
raise exception 'reason >= 5 chars required';
|
||||
end if;
|
||||
select * into v from app.voucher_inventory where serial = p_serial for update;
|
||||
if v.serial is null then raise exception 'voucher not found'; end if;
|
||||
if v.status <> 'in_stock' then
|
||||
raise exception 'voucher must be in_stock to write off (was %)', v.status;
|
||||
end if;
|
||||
if not app.has_role_in_shop(v.shop_id, 'manager') then
|
||||
raise exception 'manager role required';
|
||||
end if;
|
||||
if not app.verify_my_pin(p_manager_pin) then
|
||||
raise exception 'invalid manager PIN';
|
||||
end if;
|
||||
|
||||
update app.voucher_inventory
|
||||
set status = p_status, status_changed_by = auth.uid(),
|
||||
status_change_reason = p_reason
|
||||
where serial = p_serial;
|
||||
|
||||
insert into app.stock_movements(sku, shop_id, type, qty_delta, approved_by, reason)
|
||||
values (v.sku, v.shop_id,
|
||||
case p_status when 'damaged' then 'damaged_out'::app.stock_movement_type
|
||||
when 'lost' then 'lost_out'::app.stock_movement_type end,
|
||||
-1, auth.uid(), p_reason);
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function app.sell_voucher(uuid, text) from public;
|
||||
revoke all on function app.write_off_voucher(text, app.voucher_status, text, text) from public;
|
||||
grant execute on function app.sell_voucher(uuid, text) to authenticated;
|
||||
grant execute on function app.write_off_voucher(text, app.voucher_status, text, text) to authenticated;
|
||||
|
||||
-- =====================================================================
|
||||
-- RLS
|
||||
-- =====================================================================
|
||||
alter table app.items enable row level security;
|
||||
alter table app.stock_on_hand enable row level security;
|
||||
alter table app.stock_lots enable row level security;
|
||||
alter table app.stock_movements enable row level security;
|
||||
alter table app.voucher_inventory enable row level security;
|
||||
alter table app.floats enable row level security;
|
||||
alter table app.float_balances enable row level security;
|
||||
alter table app.float_movements enable row level security;
|
||||
|
||||
alter table app.items force row level security;
|
||||
alter table app.stock_on_hand force row level security;
|
||||
alter table app.stock_lots force row level security;
|
||||
alter table app.stock_movements force row level security;
|
||||
alter table app.voucher_inventory force row level security;
|
||||
alter table app.floats force row level security;
|
||||
alter table app.float_balances force row level security;
|
||||
alter table app.float_movements force row level security;
|
||||
|
||||
-- Block direct UPDATE/DELETE on append-only tables.
|
||||
revoke update, delete on app.stock_movements from authenticated;
|
||||
revoke update, delete on app.float_movements from authenticated;
|
||||
revoke update, delete on app.voucher_inventory from authenticated;
|
||||
revoke update, delete on app.stock_on_hand from authenticated;
|
||||
revoke update, delete on app.float_balances from authenticated;
|
||||
-- Items are reference data: only owners may modify (handled by policy).
|
||||
|
||||
-- Items: readable by everyone authenticated; writes for owners only.
|
||||
drop policy if exists items_select on app.items;
|
||||
create policy items_select on app.items for select to authenticated using (true);
|
||||
drop policy if exists items_write_owner on app.items;
|
||||
create policy items_write_owner on app.items
|
||||
for all to authenticated
|
||||
using (app.is_owner_anywhere())
|
||||
with check (app.is_owner_anywhere());
|
||||
|
||||
-- Shop-scoped tables: readable to anyone assigned to the shop.
|
||||
drop policy if exists soh_select on app.stock_on_hand;
|
||||
create policy soh_select on app.stock_on_hand
|
||||
for select to authenticated
|
||||
using (
|
||||
app.has_any_role_in_shop(shop_id,
|
||||
array['owner','manager','cashier','auditor']::app.business_role[])
|
||||
);
|
||||
|
||||
drop policy if exists lots_select on app.stock_lots;
|
||||
create policy lots_select on app.stock_lots
|
||||
for select to authenticated
|
||||
using (
|
||||
app.has_any_role_in_shop(shop_id,
|
||||
array['owner','manager','auditor']::app.business_role[])
|
||||
);
|
||||
drop policy if exists lots_insert on app.stock_lots;
|
||||
create policy lots_insert on app.stock_lots
|
||||
for insert to authenticated
|
||||
with check (app.has_any_role_in_shop(shop_id, array['owner','manager']::app.business_role[]));
|
||||
grant select, insert on app.stock_lots to authenticated;
|
||||
|
||||
drop policy if exists smov_select on app.stock_movements;
|
||||
create policy smov_select on app.stock_movements
|
||||
for select to authenticated
|
||||
using (
|
||||
app.has_any_role_in_shop(shop_id,
|
||||
array['owner','manager','cashier','auditor']::app.business_role[])
|
||||
);
|
||||
drop policy if exists smov_insert on app.stock_movements;
|
||||
create policy smov_insert on app.stock_movements
|
||||
for insert to authenticated
|
||||
with check (
|
||||
app.has_any_role_in_shop(shop_id,
|
||||
array['owner','manager','cashier']::app.business_role[])
|
||||
);
|
||||
grant select, insert on app.stock_movements to authenticated;
|
||||
|
||||
drop policy if exists vouch_select on app.voucher_inventory;
|
||||
create policy vouch_select on app.voucher_inventory
|
||||
for select to authenticated
|
||||
using (
|
||||
app.has_any_role_in_shop(shop_id,
|
||||
array['owner','manager','cashier','auditor']::app.business_role[])
|
||||
);
|
||||
drop policy if exists vouch_insert on app.voucher_inventory;
|
||||
create policy vouch_insert on app.voucher_inventory
|
||||
for insert to authenticated
|
||||
with check (app.has_any_role_in_shop(shop_id, array['owner','manager']::app.business_role[]));
|
||||
grant select, insert on app.voucher_inventory to authenticated;
|
||||
-- Voucher status changes go through SECURITY DEFINER functions only.
|
||||
|
||||
drop policy if exists floats_select on app.floats;
|
||||
create policy floats_select on app.floats
|
||||
for select to authenticated
|
||||
using (
|
||||
app.has_any_role_in_shop(shop_id,
|
||||
array['owner','manager','cashier','auditor']::app.business_role[])
|
||||
);
|
||||
drop policy if exists floats_write_owner on app.floats;
|
||||
create policy floats_write_owner on app.floats
|
||||
for all to authenticated
|
||||
using (app.has_role_in_shop(shop_id, 'owner'))
|
||||
with check (app.has_role_in_shop(shop_id, 'owner'));
|
||||
grant select, insert, update on app.floats to authenticated;
|
||||
|
||||
drop policy if exists fbal_select on app.float_balances;
|
||||
create policy fbal_select on app.float_balances
|
||||
for select to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from app.floats f
|
||||
where f.id = float_balances.float_id
|
||||
and app.has_any_role_in_shop(f.shop_id,
|
||||
array['owner','manager','cashier','auditor']::app.business_role[])
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists fmov_select on app.float_movements;
|
||||
create policy fmov_select on app.float_movements
|
||||
for select to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from app.floats f
|
||||
where f.id = float_movements.float_id
|
||||
and app.has_any_role_in_shop(f.shop_id,
|
||||
array['owner','manager','cashier','auditor']::app.business_role[])
|
||||
)
|
||||
);
|
||||
drop policy if exists fmov_insert on app.float_movements;
|
||||
create policy fmov_insert on app.float_movements
|
||||
for insert to authenticated
|
||||
with check (
|
||||
exists (
|
||||
select 1 from app.floats f
|
||||
where f.id = float_movements.float_id
|
||||
and app.has_any_role_in_shop(f.shop_id,
|
||||
array['owner','manager','cashier']::app.business_role[])
|
||||
)
|
||||
);
|
||||
grant select, insert on app.float_movements to authenticated;
|
||||
|
||||
grant select on app.items, app.stock_on_hand, app.float_balances to authenticated;
|
||||
|
||||
-- End migration 0005 ----------------------------------------------------
|
||||
Reference in New Issue
Block a user