diff --git a/plans/2026-08-21-badges-web-checkout.md b/plans/2026-08-21-badges-web-checkout.md index 6aef6530e0..101360669a 100644 --- a/plans/2026-08-21-badges-web-checkout.md +++ b/plans/2026-08-21-badges-web-checkout.md @@ -127,7 +127,7 @@ The two `-m` filters are needed because the badge tests live under two hspec pat | Step | Title | Deps | Status | |---|---|---|---| -| A1 | Register the client badge migration, add `STRICT` | — | ☐ | +| A1 | Register the client badge migration, add `STRICT` | — | ☑ | | A2 | JSON instances for badge protocol types, and the offer `total` | — | ☐ | | A3 | Service schema: `web_orders`, `codes`, `provider_events` | — | ☐ | | A4 | `Catalog.hs`: internal pricing, totals, seeding | A2, A5 | ☐ | diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index a8b4958de4..862a604da3 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -48,6 +48,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260720_server_roles import Simplex.Chat.Store.Postgres.Migrations.M20260723_contact_request_rejection import Simplex.Chat.Store.Postgres.Migrations.M20260813_auto_accept_group_invitations import Simplex.Chat.Store.Postgres.Migrations.M20260822_forward_link +import Simplex.Chat.Store.Postgres.Migrations.M20261001_user_badges import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -95,7 +96,8 @@ schemaMigrations = ("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles), ("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection), ("20260813_auto_accept_group_invitations", m20260813_auto_accept_group_invitations, Just down_m20260813_auto_accept_group_invitations), - ("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link) + ("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link), + ("20261001_user_badges", m20261001_user_badges, Just down_m20261001_user_badges) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index de5acc3c82..d420af7f4b 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -171,6 +171,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260720_server_roles import Simplex.Chat.Store.SQLite.Migrations.M20260723_contact_request_rejection import Simplex.Chat.Store.SQLite.Migrations.M20260813_auto_accept_group_invitations import Simplex.Chat.Store.SQLite.Migrations.M20260822_forward_link +import Simplex.Chat.Store.SQLite.Migrations.M20261001_user_badges import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -341,7 +342,8 @@ schemaMigrations = ("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles), ("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection), ("20260813_auto_accept_group_invitations", m20260813_auto_accept_group_invitations, Just down_m20260813_auto_accept_group_invitations), - ("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link) + ("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link), + ("20261001_user_badges", m20261001_user_badges, Just down_m20261001_user_badges) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20261001_user_badges.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20261001_user_badges.hs index f1015ae393..cc07e34608 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20261001_user_badges.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20261001_user_badges.hs @@ -37,7 +37,7 @@ CREATE TABLE @invoices( status TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL -); +) STRICT; CREATE TABLE @payments( payment_id TEXT NOT NULL PRIMARY KEY, @@ -53,7 +53,7 @@ CREATE TABLE @payments( cancelled INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL -); +) STRICT; CREATE INDEX @idx_payments_provider_ref ON @payments(provider, provider_ref); @@ -69,7 +69,7 @@ CREATE TABLE @subscription_charges( currency TEXT NOT NULL, charged_at TEXT NOT NULL, UNIQUE(payment_id, provider_charge_ref) -); +) STRICT; CREATE TABLE @badge_prices( price_id TEXT NOT NULL PRIMARY KEY, @@ -78,7 +78,7 @@ CREATE TABLE @badge_prices( currency TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL -); +) STRICT; CREATE TABLE @badge_offers( offer_id TEXT NOT NULL PRIMARY KEY, @@ -88,7 +88,7 @@ CREATE TABLE @badge_offers( discount INTEGER, status TEXT NOT NULL, created_at TEXT NOT NULL -); +) STRICT; CREATE TABLE @badge_purchases( badge_purchase_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -102,7 +102,7 @@ CREATE TABLE @badge_purchases( updated_at TEXT NOT NULL, UNIQUE(purchase_key), UNIQUE(payment_id) -); +) STRICT; CREATE TABLE @badge_invoices( invoice_id TEXT NOT NULL PRIMARY KEY REFERENCES @invoices ON DELETE CASCADE, @@ -111,7 +111,7 @@ CREATE TABLE @badge_invoices( offer_id TEXT REFERENCES @badge_offers, months INTEGER NOT NULL, created_at TEXT NOT NULL -); +) STRICT; CREATE INDEX @idx_badge_invoices_purchase ON @badge_invoices(badge_purchase_id); @@ -127,7 +127,7 @@ CREATE TABLE @badge_subscription_changes( effective_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL -); +) STRICT; CREATE INDEX @idx_badge_subscription_changes_purchase ON @badge_subscription_changes(badge_purchase_id); @@ -149,7 +149,7 @@ CREATE TABLE @badge_ledger( charge_id TEXT REFERENCES @subscription_charges, from_purchase_id INTEGER REFERENCES @badge_purchases, to_purchase_id INTEGER REFERENCES @badge_purchases -); +) STRICT; CREATE UNIQUE INDEX @idx_badge_ledger_uuid ON @badge_ledger(entry_uuid); @@ -173,7 +173,7 @@ CREATE TABLE @badge_issuances( expiry TEXT NOT NULL, credential BLOB NOT NULL, created_at TEXT NOT NULL -); +) STRICT; CREATE INDEX @idx_badge_issuances_purchase ON @badge_issuances(badge_purchase_id, issuance_id); |] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_lint.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_lint.sql index e69de29bb2..cb7fed2a07 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_lint.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_lint.sql @@ -0,0 +1,5 @@ +CREATE INDEX 'badge_invoices_offer_id' ON 'badge_invoices'('offer_id'); --> badge_offers(offer_id) +CREATE INDEX 'badge_invoices_price_id' ON 'badge_invoices'('price_id'); --> badge_prices(price_id) +CREATE INDEX 'badge_issuances_entry_id' ON 'badge_issuances'('entry_id'); --> badge_ledger(entry_id) +CREATE INDEX 'badge_offers_price_id' ON 'badge_offers'('price_id'); --> badge_prices(price_id) +CREATE INDEX 'users_shown_badge_id' ON 'users'('shown_badge_id'); --> badge_purchases(badge_purchase_id) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index a488d7da21..9fa0b693a2 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -54,6 +54,7 @@ CREATE TABLE users( auto_accept_member_contacts INTEGER NOT NULL DEFAULT 0, is_user_chat_relay INTEGER NOT NULL DEFAULT 0, client_service INTEGER NOT NULL DEFAULT 0, + shown_badge_id INTEGER REFERENCES badge_purchases ON DELETE SET NULL, auto_accept_group_invitations INTEGER NOT NULL DEFAULT 0, -- 1 for active user FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) @@ -851,6 +852,141 @@ CREATE TABLE rcv_roster_transfers( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ) STRICT; +CREATE TABLE invoices( + invoice_id TEXT NOT NULL PRIMARY KEY, + provider TEXT NOT NULL, + price INTEGER NOT NULL, + discount_amount INTEGER, + credit_amount INTEGER, + amount INTEGER NOT NULL, + currency TEXT NOT NULL, + payment_url TEXT, + payment_address TEXT, + payment_crypto_currency TEXT, + payment_crypto_amount TEXT, + expires_at TEXT NOT NULL, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +) STRICT; +CREATE TABLE payments( + payment_id TEXT NOT NULL PRIMARY KEY, + invoice_id TEXT REFERENCES invoices, + provider TEXT NOT NULL, + provider_ref TEXT, + amount INTEGER, + currency TEXT, + status TEXT NOT NULL, + exception TEXT, + subscription_renews_at TEXT, + grace_until TEXT, + cancelled INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + , + evidence BLOB, + receipt_code TEXT +) STRICT; +CREATE TABLE subscription_charges( + charge_id TEXT NOT NULL PRIMARY KEY, + payment_id TEXT NOT NULL REFERENCES payments ON DELETE CASCADE, + provider_charge_ref TEXT NOT NULL, + period_start TEXT NOT NULL, + period_end TEXT NOT NULL, + amount INTEGER NOT NULL, + currency TEXT NOT NULL, + charged_at TEXT NOT NULL, + UNIQUE(payment_id, provider_charge_ref) +) STRICT; +CREATE TABLE badge_prices( + price_id TEXT NOT NULL PRIMARY KEY, + badge_type TEXT NOT NULL, + month_price INTEGER NOT NULL, + currency TEXT NOT NULL, + status TEXT NOT NULL, + created_at TEXT NOT NULL +) STRICT; +CREATE TABLE badge_offers( + offer_id TEXT NOT NULL PRIMARY KEY, + price_id TEXT REFERENCES badge_prices, + months INTEGER NOT NULL, + free_months INTEGER, + discount INTEGER, + status TEXT NOT NULL, + created_at TEXT NOT NULL +) STRICT; +CREATE TABLE badge_purchases( + badge_purchase_id INTEGER PRIMARY KEY AUTOINCREMENT, + purchase_key BLOB NOT NULL, + master_key BLOB NOT NULL, + initial_badge_type TEXT NOT NULL, + current_badge_type TEXT NOT NULL, + payment_id TEXT REFERENCES payments, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + user_id INTEGER REFERENCES users ON DELETE CASCADE, + purchase_priv_key BLOB, + alert_acked_kind TEXT, + alert_acked_episode TEXT, + alert_snooze_until TEXT, + UNIQUE(purchase_key), + UNIQUE(payment_id) +) STRICT; +CREATE TABLE badge_invoices( + invoice_id TEXT NOT NULL PRIMARY KEY REFERENCES invoices ON DELETE CASCADE, + badge_purchase_id INTEGER NOT NULL REFERENCES badge_purchases ON DELETE CASCADE, + price_id TEXT NOT NULL REFERENCES badge_prices, + offer_id TEXT REFERENCES badge_offers, + months INTEGER NOT NULL, + created_at TEXT NOT NULL +) STRICT; +CREATE TABLE badge_subscription_changes( + change_id TEXT NOT NULL PRIMARY KEY, + badge_purchase_id INTEGER NOT NULL REFERENCES badge_purchases ON DELETE CASCADE, + 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 TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +) STRICT; +CREATE TABLE badge_ledger( + entry_id INTEGER PRIMARY KEY AUTOINCREMENT, + entry_uuid TEXT NOT NULL, + badge_purchase_id INTEGER NOT NULL REFERENCES badge_purchases ON DELETE CASCADE, + change_months INTEGER NOT NULL, + balance_months INTEGER NOT NULL, + balance_start_ts TEXT NOT NULL, + balance_badge_type TEXT NOT NULL, + was_paused_since TEXT, + service_created_at TEXT NOT NULL, + created_at TEXT NOT NULL, + entry_type TEXT NOT NULL, + entry_credit_type TEXT, + entry_debit_type TEXT, + payment_id TEXT REFERENCES payments, + charge_id TEXT REFERENCES subscription_charges, + from_purchase_id INTEGER REFERENCES badge_purchases, + to_purchase_id INTEGER REFERENCES badge_purchases + , + entry_type_unknown INTEGER NOT NULL DEFAULT 0, + entry_type_value TEXT +) STRICT; +CREATE TABLE badge_issuances( + issuance_id TEXT NOT NULL PRIMARY KEY, + badge_purchase_id INTEGER NOT NULL REFERENCES badge_purchases ON DELETE CASCADE, + entry_id INTEGER REFERENCES badge_ledger, + badge_type TEXT NOT NULL, + period_start TEXT NOT NULL, + period_end TEXT NOT NULL, + expiry TEXT NOT NULL, + credential BLOB NOT NULL, + created_at TEXT NOT NULL +) STRICT; CREATE INDEX contact_profiles_index ON contact_profiles( display_name, full_name @@ -1385,6 +1521,26 @@ CREATE INDEX idx_files_roster_transfer_id ON files(roster_transfer_id); CREATE INDEX idx_chat_items_item_signed_by_group_member_id ON chat_items( item_signed_by_group_member_id ); +CREATE INDEX idx_payments_provider_ref ON payments(provider, provider_ref); +CREATE INDEX idx_payments_invoice ON payments(invoice_id); +CREATE INDEX idx_badge_invoices_purchase ON badge_invoices(badge_purchase_id); +CREATE INDEX idx_badge_subscription_changes_purchase ON badge_subscription_changes( + badge_purchase_id +); +CREATE UNIQUE INDEX idx_badge_ledger_uuid ON badge_ledger(entry_uuid); +CREATE INDEX idx_badge_ledger_purchase ON badge_ledger( + badge_purchase_id, + entry_id +); +CREATE INDEX idx_badge_ledger_payment ON badge_ledger(payment_id); +CREATE INDEX idx_badge_ledger_charge ON badge_ledger(charge_id); +CREATE INDEX idx_badge_ledger_from_purchase ON badge_ledger(from_purchase_id); +CREATE INDEX idx_badge_ledger_to_purchase ON badge_ledger(to_purchase_id); +CREATE INDEX idx_badge_issuances_purchase ON badge_issuances( + badge_purchase_id, + issuance_id +); +CREATE INDEX idx_badge_purchases_user ON badge_purchases(user_id); CREATE TRIGGER on_group_members_insert_update_summary AFTER INSERT ON group_members FOR EACH ROW