Files
aura-crm/production/backend/migrations/041_payroll.sql
T

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;