-- «Мой налог» (НПД) export rail: the operator registers each rouble income with the tax service -- and annuls its receipt on a refund. Two new tables plus one nullable column — strictly additive, -- so it applies forward via goose with no data rewrite (no contour wipe) and an image rollback -- simply ignores it. The ledger itself is untouched: it is append-only, so the export's working -- state cannot live there. -- +goose Up -- The single stored «Мой налог» session and the rail's operating mode. The password is never -- persisted: it is exchanged for a refresh token at login, and only that token is kept, encrypted -- with BACKEND_MYNALOG_KEY. device_id pins the session to one synthetic "device" so repeated -- logins do not look like a new one every time. Dropping the row logs the rail out and, by -- design, also disarms the automatic mode. CREATE TABLE payments.mynalog_session ( only_row boolean DEFAULT true NOT NULL, inn text NOT NULL, device_id text NOT NULL, refresh_token_enc bytea NOT NULL, auto_enabled boolean DEFAULT false NOT NULL, paused_at timestamp with time zone, pause_reason text DEFAULT '' NOT NULL, last_ok_at timestamp with time zone, last_alert_at timestamp with time zone, last_alert_kind text DEFAULT '' NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT mynalog_session_pkey PRIMARY KEY (only_row), CONSTRAINT mynalog_session_single_row_chk CHECK (only_row) ); -- One row per income we act on, keyed on the ledger row it reports. The queue itself is NOT -- materialised here — it is derived by joining the ledger against this table — so a row appears -- only once we have decided something about that income. -- -- receipt_name is the exact services[].name we sent, frozen BEFORE the request: the tax API has no -- idempotency key, so after a timeout the only way to find out whether our receipt was created is -- to search the tax service's own income list by that string. It carries the order id for exactly -- that reason. CREATE TABLE payments.mynalog_receipt ( ledger_id uuid NOT NULL, order_id uuid NOT NULL, account_id uuid NOT NULL, status text NOT NULL, receipt_uuid text, receipt_name text NOT NULL, operation_time timestamp with time zone NOT NULL, amount_minor bigint NOT NULL, currency text NOT NULL, entered_manually boolean DEFAULT false NOT NULL, attempts integer DEFAULT 0 NOT NULL, last_error text DEFAULT '' NOT NULL, sent_at timestamp with time zone, receipt_mail_sent_at timestamp with time zone, cancel_status text DEFAULT 'none' NOT NULL, cancel_refund_ledger_id uuid, cancelled_at timestamp with time zone, cancel_mail_sent_at timestamp with time zone, created_at timestamp with time zone DEFAULT now() NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT mynalog_receipt_pkey PRIMARY KEY (ledger_id), CONSTRAINT mynalog_receipt_ledger_fkey FOREIGN KEY (ledger_id) REFERENCES payments.ledger (ledger_id), CONSTRAINT mynalog_receipt_order_fkey FOREIGN KEY (order_id) REFERENCES payments.orders (order_id), CONSTRAINT mynalog_receipt_refund_fkey FOREIGN KEY (cancel_refund_ledger_id) REFERENCES payments.ledger (ledger_id), CONSTRAINT mynalog_receipt_status_chk CHECK ((status = ANY (ARRAY['sending'::text, 'sent'::text, 'unknown'::text, 'failed'::text, 'not_required'::text]))), CONSTRAINT mynalog_receipt_cancel_status_chk CHECK ((cancel_status = ANY (ARRAY['none'::text, 'cancelled'::text, 'failed'::text]))), CONSTRAINT mynalog_receipt_amount_chk CHECK ((amount_minor >= 0)), CONSTRAINT mynalog_receipt_attempts_chk CHECK ((attempts >= 0)), -- A sent receipt must carry its uuid: without it the refund could never annul anything. CONSTRAINT mynalog_receipt_sent_uuid_chk CHECK ((status <> 'sent') OR (receipt_uuid IS NOT NULL AND receipt_uuid <> '')) ); CREATE INDEX mynalog_receipt_status_idx ON payments.mynalog_receipt (status); CREATE INDEX mynalog_receipt_cancel_idx ON payments.mynalog_receipt (cancel_status); CREATE INDEX mynalog_receipt_order_idx ON payments.mynalog_receipt (order_id); -- A second, independent cursor over the existing payment-event outbox: dispatched_at drives the -- in-app wallet-refresh push, mailed_at drives the buyer's purchase-confirmation letter. The -- column means "the mail decision has been made", not "a letter was sent" — an event that needs -- no letter (a store rail, or an account with no confirmed address) is stamped and skipped. ALTER TABLE payments.payment_events ADD COLUMN mailed_at timestamp with time zone; CREATE INDEX payment_events_mail_idx ON payments.payment_events (mailed_at) WHERE mailed_at IS NULL; -- +goose Down DROP INDEX payments.payment_events_mail_idx; ALTER TABLE payments.payment_events DROP COLUMN mailed_at; DROP TABLE payments.mynalog_receipt; DROP TABLE payments.mynalog_session;