diff --git a/plans/2026-08-21-badges-web-checkout.md b/plans/2026-08-21-badges-web-checkout.md index 4786e58e0d..f5e39b0db2 100644 --- a/plans/2026-08-21-badges-web-checkout.md +++ b/plans/2026-08-21-badges-web-checkout.md @@ -1336,7 +1336,10 @@ No step in this plan waits on an answer. This section lists only the values an o Append here when a step contradicts this plan: the step id, what was wrong, and what was done instead. - **A1 — the migration entry is inserted chronologically, not appended.** The step said to append after `20260723_contact_request_rejection`, "whose current last entry is" that migration. Merging `master` into this branch added `20260813_auto_accept_group_invitations` after it, so `20260731_user_badges` now goes *between* the two, in both lists. Read the lists before editing rather than trusting a stated last entry; the same applies to any later step that appends to a shared list. -- **A1 — `Store/Postgres/Migrations/chat_schema.sql` is NOT regenerated. OPEN, and a release blocker.** The step's Verify requires committing it. The development environment has no PostgreSQL binaries and no container runtime, so the `client_postgres` suite cannot run: it builds and links, then fails at `Connection refused` on port 5432. Nothing catches this automatically — `build-linux-postgres` in `.github/workflows/build.yml:389` is gated on `startsWith(github.ref, 'refs/tags/v')` and only builds the library; it never runs the Postgres schema-dump spec. **Regenerate and commit that dump on a machine with PostgreSQL before this branch merges.** A3 has the same Postgres verification leg and will hit the same wall. +- **A1 — `Store/Postgres/Migrations/chat_schema.sql` regenerated in a later commit, and hand-filtered.** The step's Verify requires committing it, but PostgreSQL was absent when A1 landed. Once installed, the dump was regenerated and the badge tables, their constraints and indexes, and `users.shown_badge_id` with its foreign key all appear correctly. Two categories of environment noise were removed by hand before committing, and the result was checked to be a pure-addition diff containing nothing but badge objects: + - `\restrict`/`\unrestrict` directives, which `pg_dump` emits since the August 2025 security patch. Their token is **randomised per run**, so committing them would make the file differ from itself on every regeneration. + - `relay_request_execute_at`'s default, which the dump renders in the server's timezone: `'1970-01-01 01:00:00+01'` in the committed file versus `'1970-01-01 00:00:00+00'` here. The same instant; the committed rendering was kept to avoid a spurious flip. +- **The Postgres schema-dump spec cannot pass on Linux as written. Pre-existing, unrelated to badges.** Two independent causes, and together they explain why this dump had drifted. First, `tests/PostgresSchemaDump.hs:71` selects `sed -i ''` — BSD/macOS syntax — unless `envCI` is true, and `envCI` is `lookupEnv "CI" == Just "true"` (`tests/ChatTests/Utils.hs:117`). A developer on Linux running locally, without `CI=true`, gets the macOS branch and the spec dies on `sed: can't read /^--/d`. The flag conflates "running in CI" with "has GNU sed". Second, even with `CI=true`, the spec compares a freshly dumped schema against the committed file without stripping `\restrict`/`\unrestrict`, so any patched `pg_dump` fails the comparison non-deterministically. Fixing either is outside this plan; note both before relying on that spec. - **A2 — three more `Int64` id fields need the same correction the step already mandates.** The step lists `BadgePurchase.paymentId`, `BadgePayment.paymentId` and `BadgeIssuance.issuanceId`. An audit of `badges-rpc.schema.json` against all four modules found the same defect in three further places, all `TEXT` columns typed as `Int64`: `StatementCreditType.SCCharge {chargeId}` (`Badges/Service.hs:163`, against `subscription_charges.charge_id TEXT NOT NULL PRIMARY KEY`), and `BadgeCharge.chargeId` and `BadgeCharge.paymentId` (`Badges/Types.hs:163-164`). `SCCharge` is the load-bearing one: it is a wire type whose `taggedObjectJSON` instance A2 writes, the schema declares `chargeId` as `string` (`badges-rpc.schema.json:230`), and Aeson would encode an `Int64` as a JSON number — so leaving it ships a payload that fails its own schema. Its sibling `SCPayment` already carries `Maybe InvoiceId`, a newtype over `Text`. A2 corrects all six. - **`LedgerCreditType.CTPayment {invoiceId :: Int64}` and `CTCharge {chargeId :: Int64}` are wrong against their columns but are marked `-- confirmed`. OPEN — needs a decision, not a mechanical fix.** `invoices.invoice_id` and `subscription_charges.charge_id` are both `TEXT`. These are the DB-side twins of the wire types above, and `CTTransferIn {fromPurchaseId :: Maybe Int64}` beside them is correct because `from_purchase_id` really is `INTEGER`. A2 does not touch them: they sit outside its tagged-sum list, and altering a type someone marked confirmed is above a mechanical step. C1's `insertLedgerEntries` is the first code that would persist them, so this must be settled before C1. - **A1 — `chat_lint.sql` gains 5 fkey-index advisories, left unfixed by design.** The badge migration introduces unindexed foreign keys: `badge_invoices.offer_id`, `badge_invoices.price_id`, `badge_offers.price_id`, `badge_issuances.entry_id`, `users.shown_badge_id`. The lint output is committed literally rather than adding indexes, since index design is outside A1's scope and the repo has precedent for this (`9e000d6bc`). The first three point at rarely-mutated reference tables. The last two are the ones likely to matter under load — `badge_issuances.entry_id` for issuance lookup by ledger entry, and `users.shown_badge_id` for per-user badge display (C1's `getShownPurchase`). Decide on indexes for those two before release. diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index ab6384cb29..48c19466df 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -1,5 +1,6 @@ + SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; @@ -168,6 +169,135 @@ CREATE TABLE test_chat_schema.app_settings ( +CREATE TABLE test_chat_schema.badge_invoices ( + invoice_id text NOT NULL, + badge_purchase_id bigint NOT NULL, + price_id text NOT NULL, + offer_id text, + months smallint NOT NULL, + created_at timestamp with time zone NOT NULL +); + + + +CREATE TABLE test_chat_schema.badge_issuances ( + issuance_id text NOT NULL, + badge_purchase_id bigint NOT NULL, + entry_id bigint, + badge_type text NOT NULL, + period_start timestamp with time zone NOT NULL, + period_end timestamp with time zone NOT NULL, + expiry timestamp with time zone NOT NULL, + credential bytea NOT NULL, + created_at timestamp with time zone NOT NULL +); + + + +CREATE TABLE test_chat_schema.badge_ledger ( + entry_id bigint NOT NULL, + entry_uuid text NOT NULL, + badge_purchase_id bigint NOT NULL, + change_months smallint NOT NULL, + balance_months smallint NOT NULL, + balance_start_ts timestamp with time zone NOT NULL, + balance_badge_type text NOT NULL, + was_paused_since timestamp with time zone, + service_created_at timestamp with time zone NOT NULL, + created_at timestamp with time zone NOT NULL, + entry_type text NOT NULL, + entry_credit_type text, + entry_debit_type text, + payment_id text, + charge_id text, + from_purchase_id bigint, + to_purchase_id bigint, + entry_type_unknown smallint DEFAULT 0 NOT NULL, + entry_type_value text +); + + + +ALTER TABLE test_chat_schema.badge_ledger ALTER COLUMN entry_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME test_chat_schema.badge_ledger_entry_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + + +CREATE TABLE test_chat_schema.badge_offers ( + offer_id text NOT NULL, + price_id text, + months smallint NOT NULL, + free_months smallint, + discount smallint, + status text NOT NULL, + created_at timestamp with time zone NOT NULL +); + + + +CREATE TABLE test_chat_schema.badge_prices ( + price_id text NOT NULL, + badge_type text NOT NULL, + month_price bigint NOT NULL, + currency text NOT NULL, + status text NOT NULL, + created_at timestamp with time zone NOT NULL +); + + + +CREATE TABLE test_chat_schema.badge_purchases ( + badge_purchase_id bigint NOT NULL, + purchase_key bytea NOT NULL, + master_key bytea NOT NULL, + initial_badge_type text NOT NULL, + current_badge_type text NOT NULL, + payment_id text, + status text NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + user_id bigint, + purchase_priv_key bytea, + alert_acked_kind text, + alert_acked_episode text, + alert_snooze_until timestamp with time zone +); + + + +ALTER TABLE test_chat_schema.badge_purchases ALTER COLUMN badge_purchase_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME test_chat_schema.badge_purchases_badge_purchase_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + + +CREATE TABLE test_chat_schema.badge_subscription_changes ( + change_id text NOT NULL, + badge_purchase_id bigint NOT NULL, + from_badge_type text NOT NULL, + to_badge_type text NOT NULL, + from_provider_ref text, + to_provider_ref text, + effective text NOT NULL, + status text NOT NULL, + effective_at timestamp with time zone, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + + + CREATE TABLE test_chat_schema.calls ( call_id bigint NOT NULL, contact_id bigint NOT NULL, @@ -1027,6 +1157,26 @@ ALTER TABLE test_chat_schema.groups ALTER COLUMN group_id ADD GENERATED ALWAYS A +CREATE TABLE test_chat_schema.invoices ( + invoice_id text NOT NULL, + provider text NOT NULL, + price bigint NOT NULL, + discount_amount bigint, + credit_amount bigint, + amount bigint NOT NULL, + currency text NOT NULL, + payment_url text, + payment_address text, + payment_crypto_currency text, + payment_crypto_amount text, + expires_at timestamp with time zone NOT NULL, + status text NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + + + CREATE TABLE test_chat_schema.known_servers ( server_id bigint NOT NULL, host text NOT NULL, @@ -1160,6 +1310,26 @@ ALTER TABLE test_chat_schema.operator_usage_conditions ALTER COLUMN operator_usa +CREATE TABLE test_chat_schema.payments ( + payment_id text NOT NULL, + invoice_id text, + provider text NOT NULL, + provider_ref text, + amount bigint, + currency text, + status text NOT NULL, + exception text, + subscription_renews_at timestamp with time zone, + grace_until timestamp with time zone, + cancelled smallint DEFAULT 0 NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + evidence bytea, + receipt_code text +); + + + CREATE TABLE test_chat_schema.pending_group_messages ( pending_group_message_id bigint NOT NULL, group_member_id bigint NOT NULL, @@ -1454,6 +1624,19 @@ CREATE TABLE test_chat_schema.snd_files ( +CREATE TABLE test_chat_schema.subscription_charges ( + charge_id text NOT NULL, + payment_id text NOT NULL, + provider_charge_ref text NOT NULL, + period_start timestamp with time zone NOT NULL, + period_end timestamp with time zone NOT NULL, + amount bigint NOT NULL, + currency text NOT NULL, + charged_at timestamp with time zone NOT NULL +); + + + CREATE TABLE test_chat_schema.usage_conditions ( usage_conditions_id bigint NOT NULL, conditions_commit text NOT NULL, @@ -1516,6 +1699,7 @@ CREATE TABLE test_chat_schema.users ( auto_accept_member_contacts smallint DEFAULT 0 NOT NULL, is_user_chat_relay smallint DEFAULT 0 NOT NULL, client_service smallint DEFAULT 0 NOT NULL, + shown_badge_id bigint, auto_accept_group_invitations smallint DEFAULT 0 NOT NULL ); @@ -1555,6 +1739,51 @@ ALTER TABLE test_chat_schema.xftp_file_descriptions ALTER COLUMN file_descr_id A +ALTER TABLE ONLY test_chat_schema.badge_invoices + ADD CONSTRAINT badge_invoices_pkey PRIMARY KEY (invoice_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_issuances + ADD CONSTRAINT badge_issuances_pkey PRIMARY KEY (issuance_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_ledger + ADD CONSTRAINT badge_ledger_pkey PRIMARY KEY (entry_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_offers + ADD CONSTRAINT badge_offers_pkey PRIMARY KEY (offer_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_prices + ADD CONSTRAINT badge_prices_pkey PRIMARY KEY (price_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_purchases + ADD CONSTRAINT badge_purchases_payment_id_key UNIQUE (payment_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_purchases + ADD CONSTRAINT badge_purchases_pkey PRIMARY KEY (badge_purchase_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_purchases + ADD CONSTRAINT badge_purchases_purchase_key_key UNIQUE (purchase_key); + + + +ALTER TABLE ONLY test_chat_schema.badge_subscription_changes + ADD CONSTRAINT badge_subscription_changes_pkey PRIMARY KEY (change_id); + + + ALTER TABLE ONLY test_chat_schema.calls ADD CONSTRAINT calls_pkey PRIMARY KEY (call_id); @@ -1745,6 +1974,11 @@ ALTER TABLE ONLY test_chat_schema.groups +ALTER TABLE ONLY test_chat_schema.invoices + ADD CONSTRAINT invoices_pkey PRIMARY KEY (invoice_id); + + + ALTER TABLE ONLY test_chat_schema.known_servers ADD CONSTRAINT known_servers_pkey PRIMARY KEY (server_id); @@ -1780,6 +2014,11 @@ ALTER TABLE ONLY test_chat_schema.operator_usage_conditions +ALTER TABLE ONLY test_chat_schema.payments + ADD CONSTRAINT payments_pkey PRIMARY KEY (payment_id); + + + ALTER TABLE ONLY test_chat_schema.pending_group_messages ADD CONSTRAINT pending_group_messages_pkey PRIMARY KEY (pending_group_message_id); @@ -1855,6 +2094,16 @@ ALTER TABLE ONLY test_chat_schema.snd_files +ALTER TABLE ONLY test_chat_schema.subscription_charges + ADD CONSTRAINT subscription_charges_payment_id_provider_charge_ref_key UNIQUE (payment_id, provider_charge_ref); + + + +ALTER TABLE ONLY test_chat_schema.subscription_charges + ADD CONSTRAINT subscription_charges_pkey PRIMARY KEY (charge_id); + + + ALTER TABLE ONLY test_chat_schema.usage_conditions ADD CONSTRAINT usage_conditions_conditions_commit_key UNIQUE (conditions_commit); @@ -1907,6 +2156,46 @@ CREATE INDEX files_note_folder_id ON test_chat_schema.files USING btree (note_fo +CREATE INDEX idx_badge_invoices_purchase ON test_chat_schema.badge_invoices USING btree (badge_purchase_id); + + + +CREATE INDEX idx_badge_issuances_purchase ON test_chat_schema.badge_issuances USING btree (badge_purchase_id, issuance_id); + + + +CREATE INDEX idx_badge_ledger_charge ON test_chat_schema.badge_ledger USING btree (charge_id); + + + +CREATE INDEX idx_badge_ledger_from_purchase ON test_chat_schema.badge_ledger USING btree (from_purchase_id); + + + +CREATE INDEX idx_badge_ledger_payment ON test_chat_schema.badge_ledger USING btree (payment_id); + + + +CREATE INDEX idx_badge_ledger_purchase ON test_chat_schema.badge_ledger USING btree (badge_purchase_id, entry_id); + + + +CREATE INDEX idx_badge_ledger_to_purchase ON test_chat_schema.badge_ledger USING btree (to_purchase_id); + + + +CREATE UNIQUE INDEX idx_badge_ledger_uuid ON test_chat_schema.badge_ledger USING btree (entry_uuid); + + + +CREATE INDEX idx_badge_purchases_user ON test_chat_schema.badge_purchases USING btree (user_id); + + + +CREATE INDEX idx_badge_subscription_changes_purchase ON test_chat_schema.badge_subscription_changes USING btree (badge_purchase_id); + + + CREATE INDEX idx_calls_chat_item_id ON test_chat_schema.calls USING btree (chat_item_id); @@ -2511,6 +2800,14 @@ CREATE INDEX idx_operator_usage_conditions_server_operator_id ON test_chat_schem +CREATE INDEX idx_payments_invoice ON test_chat_schema.payments USING btree (invoice_id); + + + +CREATE INDEX idx_payments_provider_ref ON test_chat_schema.payments USING btree (provider, provider_ref); + + + CREATE INDEX idx_pending_group_messages_group_member_id ON test_chat_schema.pending_group_messages USING btree (group_member_id); @@ -2659,6 +2956,81 @@ CREATE TRIGGER tr_group_members_update_update_summary AFTER UPDATE ON test_chat_ +ALTER TABLE ONLY test_chat_schema.badge_invoices + ADD CONSTRAINT badge_invoices_badge_purchase_id_fkey FOREIGN KEY (badge_purchase_id) REFERENCES test_chat_schema.badge_purchases(badge_purchase_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.badge_invoices + ADD CONSTRAINT badge_invoices_invoice_id_fkey FOREIGN KEY (invoice_id) REFERENCES test_chat_schema.invoices(invoice_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.badge_invoices + ADD CONSTRAINT badge_invoices_offer_id_fkey FOREIGN KEY (offer_id) REFERENCES test_chat_schema.badge_offers(offer_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_invoices + ADD CONSTRAINT badge_invoices_price_id_fkey FOREIGN KEY (price_id) REFERENCES test_chat_schema.badge_prices(price_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_issuances + ADD CONSTRAINT badge_issuances_badge_purchase_id_fkey FOREIGN KEY (badge_purchase_id) REFERENCES test_chat_schema.badge_purchases(badge_purchase_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.badge_issuances + ADD CONSTRAINT badge_issuances_entry_id_fkey FOREIGN KEY (entry_id) REFERENCES test_chat_schema.badge_ledger(entry_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_ledger + ADD CONSTRAINT badge_ledger_badge_purchase_id_fkey FOREIGN KEY (badge_purchase_id) REFERENCES test_chat_schema.badge_purchases(badge_purchase_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.badge_ledger + ADD CONSTRAINT badge_ledger_charge_id_fkey FOREIGN KEY (charge_id) REFERENCES test_chat_schema.subscription_charges(charge_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_ledger + ADD CONSTRAINT badge_ledger_from_purchase_id_fkey FOREIGN KEY (from_purchase_id) REFERENCES test_chat_schema.badge_purchases(badge_purchase_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_ledger + ADD CONSTRAINT badge_ledger_payment_id_fkey FOREIGN KEY (payment_id) REFERENCES test_chat_schema.payments(payment_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_ledger + ADD CONSTRAINT badge_ledger_to_purchase_id_fkey FOREIGN KEY (to_purchase_id) REFERENCES test_chat_schema.badge_purchases(badge_purchase_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_offers + ADD CONSTRAINT badge_offers_price_id_fkey FOREIGN KEY (price_id) REFERENCES test_chat_schema.badge_prices(price_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_purchases + ADD CONSTRAINT badge_purchases_payment_id_fkey FOREIGN KEY (payment_id) REFERENCES test_chat_schema.payments(payment_id); + + + +ALTER TABLE ONLY test_chat_schema.badge_purchases + ADD CONSTRAINT badge_purchases_user_id_fkey FOREIGN KEY (user_id) REFERENCES test_chat_schema.users(user_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.badge_subscription_changes + ADD CONSTRAINT badge_subscription_changes_badge_purchase_id_fkey FOREIGN KEY (badge_purchase_id) REFERENCES test_chat_schema.badge_purchases(badge_purchase_id) ON DELETE CASCADE; + + + ALTER TABLE ONLY test_chat_schema.calls ADD CONSTRAINT calls_chat_item_id_fkey FOREIGN KEY (chat_item_id) REFERENCES test_chat_schema.chat_items(chat_item_id) ON DELETE CASCADE; @@ -3199,6 +3571,11 @@ ALTER TABLE ONLY test_chat_schema.operator_usage_conditions +ALTER TABLE ONLY test_chat_schema.payments + ADD CONSTRAINT payments_invoice_id_fkey FOREIGN KEY (invoice_id) REFERENCES test_chat_schema.invoices(invoice_id); + + + ALTER TABLE ONLY test_chat_schema.pending_group_messages ADD CONSTRAINT pending_group_messages_group_member_id_fkey FOREIGN KEY (group_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE CASCADE; @@ -3304,6 +3681,11 @@ ALTER TABLE ONLY test_chat_schema.snd_files +ALTER TABLE ONLY test_chat_schema.subscription_charges + ADD CONSTRAINT subscription_charges_payment_id_fkey FOREIGN KEY (payment_id) REFERENCES test_chat_schema.payments(payment_id) ON DELETE CASCADE; + + + ALTER TABLE ONLY test_chat_schema.user_contact_links ADD CONSTRAINT user_contact_links_group_id_fkey FOREIGN KEY (group_id) REFERENCES test_chat_schema.groups(group_id) ON DELETE CASCADE; @@ -3314,8 +3696,14 @@ ALTER TABLE ONLY test_chat_schema.user_contact_links +ALTER TABLE ONLY test_chat_schema.users + ADD CONSTRAINT users_shown_badge_id_fkey FOREIGN KEY (shown_badge_id) REFERENCES test_chat_schema.badge_purchases(badge_purchase_id) ON DELETE SET NULL; + + + ALTER TABLE ONLY test_chat_schema.xftp_file_descriptions ADD CONSTRAINT xftp_file_descriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES test_chat_schema.users(user_id) ON DELETE CASCADE; +