105 lines
5.4 KiB
SQL
105 lines
5.4 KiB
SQL
-- +goose Up
|
|
|
|
-- Per-staff payroll rate configuration — owner sets these once, applied to
|
|
-- every future calculation until changed. staff_id/staff_name follow the
|
|
-- same denormalized-pointer pattern as orders.assigned_master_id/_name:
|
|
-- staff accounts live in core, production never queries that table
|
|
-- directly, so there is no FK here on purpose.
|
|
CREATE TABLE payroll_rates (
|
|
staff_id UUID PRIMARY KEY,
|
|
staff_name TEXT NOT NULL,
|
|
shift_rate NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
|
order_profit_percent NUMERIC(5, 2) NOT NULL DEFAULT 0 CHECK (order_profit_percent BETWEEN 0 AND 100),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Piece rate per cartridge model ("сумма за заправку этой модели"), not
|
|
-- per-staff — the same rate applies to whoever refilled it. RESTRICT so a
|
|
-- model with a configured rate can't be deleted out from under it.
|
|
CREATE TABLE cartridge_payroll_rates (
|
|
cartridge_model_id UUID PRIMARY KEY REFERENCES cartridge_models(id) ON DELETE RESTRICT,
|
|
rate NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- One row per "начисление" (payroll run) for one staff member over one
|
|
-- period. Deliberately a snapshot, not a live query: orders/cartridges are
|
|
-- mutable (an order's final_price can still be edited after completion),
|
|
-- so a run locks in the numbers at calculation time — a past payslip must
|
|
-- never silently reshape itself because someone edited an old order.
|
|
-- rate columns are copied from payroll_rates at calc time for the same
|
|
-- reason: a later rate change must not retroactively rewrite history.
|
|
CREATE TABLE payroll_runs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
staff_id UUID NOT NULL,
|
|
staff_name TEXT NOT NULL,
|
|
period_from DATE NOT NULL,
|
|
period_to DATE NOT NULL, -- exclusive
|
|
shifts_count INT NOT NULL DEFAULT 0 CHECK (shifts_count >= 0),
|
|
shift_rate NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
|
shift_total NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
|
order_profit_percent NUMERIC(5, 2) NOT NULL DEFAULT 0,
|
|
orders_profit_total NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
|
orders_commission_total NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
|
cartridge_total NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
|
adjustment_total NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
|
grand_total NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'paid')),
|
|
paid_at TIMESTAMPTZ,
|
|
cash_transaction_id UUID REFERENCES cash_transactions(id) ON DELETE SET NULL,
|
|
created_by_staff_id UUID NOT NULL,
|
|
created_by_staff_name TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CHECK (period_from < period_to)
|
|
);
|
|
CREATE INDEX payroll_runs_staff_id_idx ON payroll_runs (staff_id, period_from);
|
|
|
|
-- Detail rows behind orders_commission_total — auditable, not a black-box
|
|
-- total. The unique index on order_id is the real guard: once an order has
|
|
-- been counted into any run (any staff, any period), it can never be
|
|
-- pulled into a second one, which is what actually prevents double-paying
|
|
-- commission on the same job.
|
|
CREATE TABLE payroll_run_orders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
run_id UUID NOT NULL REFERENCES payroll_runs(id) ON DELETE CASCADE,
|
|
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE RESTRICT,
|
|
order_number TEXT,
|
|
final_price NUMERIC(10, 2) NOT NULL,
|
|
parts_cost NUMERIC(10, 2) NOT NULL,
|
|
profit NUMERIC(10, 2) NOT NULL,
|
|
commission NUMERIC(10, 2) NOT NULL
|
|
);
|
|
CREATE INDEX payroll_run_orders_run_id_idx ON payroll_run_orders (run_id);
|
|
CREATE UNIQUE INDEX payroll_run_orders_order_id_idx ON payroll_run_orders (order_id);
|
|
|
|
-- Same shape/guard as payroll_run_orders, one row per cartridge_item.
|
|
CREATE TABLE payroll_run_cartridges (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
run_id UUID NOT NULL REFERENCES payroll_runs(id) ON DELETE CASCADE,
|
|
cartridge_item_id UUID NOT NULL REFERENCES cartridge_items(id) ON DELETE RESTRICT,
|
|
model TEXT NOT NULL,
|
|
rate NUMERIC(10, 2) NOT NULL
|
|
);
|
|
CREATE INDEX payroll_run_cartridges_run_id_idx ON payroll_run_cartridges (run_id);
|
|
CREATE UNIQUE INDEX payroll_run_cartridges_item_id_idx ON payroll_run_cartridges (cartridge_item_id);
|
|
|
|
-- Manual bonus/penalty lines on a run, kept as separate rows (not folded
|
|
-- into one +/- number on payroll_runs) so each carries its own reason text
|
|
-- and can be added/removed individually while the run is still a draft.
|
|
CREATE TABLE payroll_adjustments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
run_id UUID NOT NULL REFERENCES payroll_runs(id) ON DELETE CASCADE,
|
|
amount NUMERIC(10, 2) NOT NULL, -- positive = bonus, negative = deduction
|
|
reason TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE INDEX payroll_adjustments_run_id_idx ON payroll_adjustments (run_id);
|
|
|
|
-- +goose Down
|
|
DROP TABLE payroll_adjustments;
|
|
DROP TABLE payroll_run_cartridges;
|
|
DROP TABLE payroll_run_orders;
|
|
DROP TABLE payroll_runs;
|
|
DROP TABLE cartridge_payroll_rates;
|
|
DROP TABLE payroll_rates;
|