From 8fdff7750f564553b69491a7f83d21b16cf44f6d Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:28:52 +0000 Subject: [PATCH] core: client and service schema for badge purchases --- .../BadgeService/Store/Postgres/Migrations.hs | 26 +- .../BadgeService/Store/SQLite/Migrations.hs | 24 +- simplex-chat.cabal | 2 + .../Migrations/M20260731_user_badges.hs | 226 ++++++++++++++ .../Migrations/M20260731_user_badges.hs | 278 +++++++++++------- 5 files changed, 423 insertions(+), 133 deletions(-) create mode 100644 src/Simplex/Chat/Store/Postgres/Migrations/M20260731_user_badges.hs diff --git a/apps/simplex-badge-service/src/BadgeService/Store/Postgres/Migrations.hs b/apps/simplex-badge-service/src/BadgeService/Store/Postgres/Migrations.hs index a4bc3396bf..f1b6590b38 100644 --- a/apps/simplex-badge-service/src/BadgeService/Store/Postgres/Migrations.hs +++ b/apps/simplex-badge-service/src/BadgeService/Store/Postgres/Migrations.hs @@ -1,11 +1,12 @@ {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} module BadgeService.Store.Postgres.Migrations (badgeServiceSchemaMigrations) where import Data.List (sortOn) import Data.Text (Text) -import qualified Data.Text as T +import Simplex.Chat.Store.Postgres.Migrations.M20260731_user_badges (badgeSchema, badgeSchemaDown, withPrefix) import Simplex.Messaging.Agent.Store.Shared (Migration (..)) import Text.RawString.QQ (r) @@ -19,19 +20,18 @@ schemaMigrations = [ ("20260806_badge_service_schema", m20260806_badge_service_schema, Just down_m20260806_badge_service_schema) ] +-- the client tables are in the same database, so the service tables are the same names with this prefix +servicePrefix :: Text +servicePrefix = "sx_badge_service_" + m20260806_badge_service_schema :: Text m20260806_badge_service_schema = - T.pack - [r| -CREATE TABLE sx_badge_service_test( - test_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - created_at TIMESTAMPTZ NOT NULL DEFAULT (now()) -); - |] + badgeSchema servicePrefix + <> withPrefix + servicePrefix + [r| +ALTER TABLE @badge_purchases ADD COLUMN receipt_hash BYTEA; +|] down_m20260806_badge_service_schema :: Text -down_m20260806_badge_service_schema = - T.pack - [r| -DROP TABLE sx_badge_service_test; - |] +down_m20260806_badge_service_schema = badgeSchemaDown servicePrefix diff --git a/apps/simplex-badge-service/src/BadgeService/Store/SQLite/Migrations.hs b/apps/simplex-badge-service/src/BadgeService/Store/SQLite/Migrations.hs index 8404f135db..5584ab917f 100644 --- a/apps/simplex-badge-service/src/BadgeService/Store/SQLite/Migrations.hs +++ b/apps/simplex-badge-service/src/BadgeService/Store/SQLite/Migrations.hs @@ -1,11 +1,14 @@ {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} module BadgeService.Store.SQLite.Migrations (badgeServiceSchemaMigrations) where import Data.List (sortOn) +import Data.Text (Text) import Database.SQLite.Simple (Query (..)) import Database.SQLite.Simple.QQ (sql) +import Simplex.Chat.Store.SQLite.Migrations.M20260731_user_badges (badgeSchema, badgeSchemaDown, withPrefix) import Simplex.Messaging.Agent.Store.Shared (Migration (..)) badgeServiceSchemaMigrations :: [Migration] @@ -18,17 +21,18 @@ schemaMigrations = [ ("20260806_badge_service_schema", m20260806_badge_service_schema, Just down_m20260806_badge_service_schema) ] +-- the client tables are in the same database, so the service tables are the same names with this prefix +servicePrefix :: Text +servicePrefix = "sx_badge_service_" + m20260806_badge_service_schema :: Query m20260806_badge_service_schema = - [sql| -CREATE TABLE sx_badge_service_test( - test_id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at TEXT NOT NULL DEFAULT(datetime('now')) -); - |] + badgeSchema servicePrefix + <> withPrefix + servicePrefix + [sql| +ALTER TABLE @badge_purchases ADD COLUMN receipt_hash BLOB; +|] down_m20260806_badge_service_schema :: Query -down_m20260806_badge_service_schema = - [sql| -DROP TABLE sx_badge_service_test; - |] +down_m20260806_badge_service_schema = badgeSchemaDown servicePrefix diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 9f23bd9223..52cae7eb68 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -155,6 +155,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260716_signed_history Simplex.Chat.Store.Postgres.Migrations.M20260720_server_roles Simplex.Chat.Store.Postgres.Migrations.M20260723_contact_request_rejection + Simplex.Chat.Store.Postgres.Migrations.M20260731_user_badges else exposed-modules: Simplex.Chat.Archive @@ -325,6 +326,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260716_signed_history Simplex.Chat.Store.SQLite.Migrations.M20260720_server_roles Simplex.Chat.Store.SQLite.Migrations.M20260723_contact_request_rejection + Simplex.Chat.Store.SQLite.Migrations.M20260731_user_badges other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260731_user_badges.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260731_user_badges.hs new file mode 100644 index 0000000000..c541b22093 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260731_user_badges.hs @@ -0,0 +1,226 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260731_user_badges where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +-- the same tables in the client and in the badge service; '@' is replaced with the table name prefix +badgeSchema :: Text -> Text +badgeSchema pfx = withPrefix pfx badgeSchemaTables + +badgeSchemaDown :: Text -> Text +badgeSchemaDown pfx = withPrefix pfx badgeSchemaTablesDown + +withPrefix :: Text -> Text -> Text +withPrefix = T.replace "@" + +badgeSchemaTables :: Text +badgeSchemaTables = + [r| +CREATE TABLE @invoices( + invoice_id TEXT NOT NULL PRIMARY KEY, + 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_amount TEXT, + expires_at TIMESTAMPTZ NOT NULL, + status TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE @payments( + payment_id TEXT NOT NULL PRIMARY KEY, + invoice_id TEXT REFERENCES @invoices, + provider TEXT NOT NULL, + provider_ref TEXT, + amount BIGINT, + currency TEXT, + status TEXT NOT NULL, + exception TEXT, + renews_at TIMESTAMPTZ, + grace_until TIMESTAMPTZ, + cancelled SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX @idx_payments_provider_ref ON @payments(provider, provider_ref); + +CREATE INDEX @idx_payments_invoice ON @payments(invoice_id); + +CREATE TABLE @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 TIMESTAMPTZ NOT NULL, + period_end TIMESTAMPTZ NOT NULL, + amount BIGINT NOT NULL, + currency TEXT NOT NULL, + charged_at TIMESTAMPTZ NOT NULL, + UNIQUE(payment_id, provider_charge_ref) +); + +CREATE TABLE @badge_prices( + price_id TEXT NOT NULL PRIMARY KEY, + badge_type TEXT NOT NULL, + month_price BIGINT NOT NULL, + currency TEXT NOT NULL, + status TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE @badge_offers( + offer_id TEXT NOT NULL PRIMARY KEY, + price_id TEXT REFERENCES @badge_prices, + months SMALLINT NOT NULL, + free_months SMALLINT, + discount SMALLINT, + status TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE @badge_purchases( + badge_purchase_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + 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 REFERENCES @payments, + status TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + UNIQUE(purchase_key), + UNIQUE(payment_id) +); + +CREATE TABLE @badge_invoices( + invoice_id TEXT NOT NULL PRIMARY KEY REFERENCES @invoices ON DELETE CASCADE, + badge_purchase_id BIGINT NOT NULL REFERENCES @badge_purchases ON DELETE CASCADE, + price_id TEXT NOT NULL REFERENCES @badge_prices, + offer_id TEXT REFERENCES @badge_offers, + months SMALLINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX @idx_badge_invoices_purchase ON @badge_invoices(badge_purchase_id); + +CREATE TABLE @badge_subscription_changes( + change_id TEXT NOT NULL PRIMARY KEY, + badge_purchase_id BIGINT 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 TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX @idx_badge_subscription_changes_purchase ON @badge_subscription_changes(badge_purchase_id); + +CREATE TABLE @badge_ledger( + entry_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + entry_uuid TEXT NOT NULL, + badge_purchase_id BIGINT NOT NULL REFERENCES @badge_purchases ON DELETE CASCADE, + change_months SMALLINT NOT NULL, + balance_months SMALLINT NOT NULL, + balance_start_ts TIMESTAMPTZ NOT NULL, + balance_badge_type TEXT NOT NULL, + was_paused_since TIMESTAMPTZ, + service_created_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + entry_type TEXT NOT NULL, + entry_credit_type TEXT, + entry_debit_type TEXT, + payment_id TEXT REFERENCES @payments, + charge_id TEXT REFERENCES @charges, + from_purchase_id BIGINT REFERENCES @badge_purchases, + to_purchase_id BIGINT REFERENCES @badge_purchases +); + +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 TABLE @badge_issuances( + issuance_id TEXT NOT NULL PRIMARY KEY, + badge_purchase_id BIGINT NOT NULL REFERENCES @badge_purchases ON DELETE CASCADE, + entry_id BIGINT REFERENCES @badge_ledger, + badge_type TEXT NOT NULL, + period_start TIMESTAMPTZ NOT NULL, + period_end TIMESTAMPTZ NOT NULL, + expiry TIMESTAMPTZ NOT NULL, + credential BYTEA NOT NULL, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX @idx_badge_issuances_purchase ON @badge_issuances(badge_purchase_id, issuance_id); +|] + +badgeSchemaTablesDown :: Text +badgeSchemaTablesDown = + [r| +DROP TABLE @badge_issuances; +DROP TABLE @badge_ledger; +DROP TABLE @badge_subscription_changes; +DROP TABLE @badge_invoices; +DROP TABLE @badge_purchases; +DROP TABLE @charges; +DROP TABLE @payments; +DROP TABLE @invoices; +DROP TABLE @badge_offers; +DROP TABLE @badge_prices; +|] + +m20260731_user_badges :: Text +m20260731_user_badges = + badgeSchema "" + <> [r| +ALTER TABLE badge_purchases ADD COLUMN user_id BIGINT REFERENCES users ON DELETE CASCADE; + +ALTER TABLE badge_purchases ADD COLUMN purchase_priv_key BYTEA; + +ALTER TABLE badge_purchases ADD COLUMN receipt_code TEXT; + +ALTER TABLE badge_purchases ADD COLUMN alert_acked_kind TEXT; + +ALTER TABLE badge_purchases ADD COLUMN alert_acked_episode TEXT; + +ALTER TABLE badge_purchases ADD COLUMN alert_snooze_until TIMESTAMPTZ; + +ALTER TABLE payments ADD COLUMN evidence BYTEA; + +ALTER TABLE badge_ledger ADD COLUMN entry_type_unknown SMALLINT NOT NULL DEFAULT 0; + +ALTER TABLE badge_ledger ADD COLUMN entry_type_value TEXT; + +CREATE INDEX idx_badge_purchases_user ON badge_purchases(user_id); + +ALTER TABLE users ADD COLUMN shown_badge_id BIGINT REFERENCES badge_purchases ON DELETE SET NULL; +|] + +down_m20260731_user_badges :: Text +down_m20260731_user_badges = + [r| +ALTER TABLE users DROP COLUMN shown_badge_id; +|] + <> badgeSchemaDown "" diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260731_user_badges.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260731_user_badges.hs index 08db8a49ca..5e24d2d7d9 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20260731_user_badges.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260731_user_badges.hs @@ -1,52 +1,50 @@ +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} module Simplex.Chat.Store.SQLite.Migrations.M20260731_user_badges where -import Database.SQLite.Simple (Query) +import Data.Text (Text) +import qualified Data.Text as T +import Database.SQLite.Simple (Query (..)) import Database.SQLite.Simple.QQ (sql) -m20260731_user_badges :: Query -m20260731_user_badges = +-- the same tables in the client and in the badge service; '@' is replaced with the table name prefix +badgeSchema :: Text -> Query +badgeSchema pfx = withPrefix pfx badgeSchemaTables + +badgeSchemaDown :: Text -> Query +badgeSchemaDown pfx = withPrefix pfx badgeSchemaTablesDown + +withPrefix :: Text -> Query -> Query +withPrefix pfx = Query . T.replace "@" pfx . fromQuery + +badgeSchemaTables :: Query +badgeSchemaTables = [sql| -CREATE TABLE badge_prices( - price_id TEXT NOT NULL PRIMARY KEY, - badge_type TEXT NOT NULL, - month_price INTEGER NOT NULL, +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_amount TEXT, + expires_at TEXT NOT NULL, status TEXT NOT NULL, - created_at TEXT NOT NULL + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL ); -CREATE TABLE badge_offers( - offer_id TEXT NOT NULL PRIMARY KEY, - price_id TEXT REFERENCES badge_prices, - months INTEGER NOT NULL, - discount_type TEXT NOT NULL, - free_months INTEGER, - discount INTEGER, - status TEXT NOT NULL, - created_at TEXT NOT NULL -); - -CREATE TABLE payments( - payment_id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, - purchase_key BLOB NOT NULL, - badge_type TEXT NOT NULL, - price_id TEXT REFERENCES badge_prices, - offer_id TEXT REFERENCES badge_offers, - invoice_uuid TEXT, - months INTEGER, - amount INTEGER, - currency TEXT, +CREATE TABLE @payments( + payment_id TEXT NOT NULL PRIMARY KEY, + invoice_id TEXT REFERENCES @invoices, provider TEXT NOT NULL, provider_ref TEXT, - invoice_url TEXT, - invoice_address TEXT, - invoice_crypto_amount TEXT, - invoice_expires_at TEXT, - evidence BLOB, - receipt_code TEXT, + amount INTEGER, + currency TEXT, status TEXT NOT NULL, exception TEXT, renews_at TEXT, @@ -56,14 +54,13 @@ CREATE TABLE payments( updated_at TEXT NOT NULL ); -CREATE INDEX idx_payments_purchase_key ON payments(purchase_key); +CREATE INDEX @idx_payments_provider_ref ON @payments(provider, provider_ref); -CREATE UNIQUE INDEX idx_payments_invoice_uuid ON payments(invoice_uuid); +CREATE INDEX @idx_payments_invoice ON @payments(invoice_id); -CREATE TABLE charges( - charge_id INTEGER PRIMARY KEY, - payment_id INTEGER NOT NULL REFERENCES payments, - invoice_uuid TEXT NOT NULL, +CREATE TABLE @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, @@ -73,71 +70,70 @@ CREATE TABLE charges( UNIQUE(payment_id, provider_charge_ref) ); -CREATE TABLE badge_purchases( - badge_purchase_id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, - purchase_key BLOB NOT NULL, - purchase_priv_key BLOB NOT NULL, - master_key BLOB NOT NULL, +CREATE TABLE @badge_prices( + price_id TEXT NOT NULL PRIMARY KEY, badge_type TEXT NOT NULL, - price_id TEXT REFERENCES badge_prices, - offer_id TEXT REFERENCES badge_offers, - payment_id INTEGER NOT NULL REFERENCES payments, + month_price INTEGER NOT NULL, + currency TEXT NOT NULL, status TEXT NOT NULL, - badge_key_idx INTEGER, - badge_signature BLOB, - badge_expiry TEXT, - alert_acked_kind TEXT, - alert_acked_episode TEXT, - alert_snooze_until TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - UNIQUE(purchase_key) -); - -CREATE UNIQUE INDEX idx_badge_purchases_live ON badge_purchases(user_id, (badge_type = 'investor')) - WHERE status IN ('acquiring', 'issued'); - -ALTER TABLE users ADD COLUMN shown_badge_id INTEGER REFERENCES badge_purchases ON DELETE SET NULL; -|] - <> badgeLedgerTable - <> [sql| -ALTER TABLE badge_ledger ADD COLUMN entry_type_unknown INTEGER NOT NULL DEFAULT 0; - -ALTER TABLE badge_ledger ADD COLUMN entry_type_value TEXT; - -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_invoice ON badge_ledger(invoice_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 TABLE badge_issuances( - issuance_id INTEGER PRIMARY KEY, - badge_purchase_id INTEGER NOT NULL REFERENCES badge_purchases ON DELETE CASCADE, - period_start TEXT, - period_end TEXT, - expiry TEXT, - entry_id INTEGER REFERENCES badge_ledger, created_at TEXT NOT NULL ); -CREATE INDEX idx_badge_issuances_purchase ON badge_issuances(badge_purchase_id, issuance_id); -|] +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 +); -badgeLedgerTable :: Query -badgeLedgerTable = - [sql| -CREATE TABLE badge_ledger( +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, + UNIQUE(purchase_key), + UNIQUE(payment_id) +); + +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 +); + +CREATE INDEX @idx_badge_invoices_purchase ON @badge_invoices(badge_purchase_id); + +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 +); + +CREATE INDEX @idx_badge_subscription_changes_purchase ON @badge_subscription_changes(badge_purchase_id); + +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, + 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, @@ -148,22 +144,84 @@ CREATE TABLE badge_ledger( entry_type TEXT NOT NULL, entry_credit_type TEXT, entry_debit_type TEXT, - invoice_id INTEGER REFERENCES payments, - charge_id INTEGER REFERENCES charges, - from_purchase_id INTEGER REFERENCES badge_purchases, - to_purchase_id INTEGER REFERENCES badge_purchases + payment_id TEXT REFERENCES @payments, + charge_id TEXT REFERENCES @charges, + from_purchase_id INTEGER REFERENCES @badge_purchases, + to_purchase_id INTEGER REFERENCES @badge_purchases ); + +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 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 +); + +CREATE INDEX @idx_badge_issuances_purchase ON @badge_issuances(badge_purchase_id, issuance_id); +|] + +badgeSchemaTablesDown :: Query +badgeSchemaTablesDown = + [sql| +DROP TABLE @badge_issuances; +DROP TABLE @badge_ledger; +DROP TABLE @badge_subscription_changes; +DROP TABLE @badge_invoices; +DROP TABLE @badge_purchases; +DROP TABLE @charges; +DROP TABLE @payments; +DROP TABLE @invoices; +DROP TABLE @badge_offers; +DROP TABLE @badge_prices; +|] + +m20260731_user_badges :: Query +m20260731_user_badges = + badgeSchema "" + <> [sql| +ALTER TABLE badge_purchases ADD COLUMN user_id INTEGER REFERENCES users ON DELETE CASCADE; + +ALTER TABLE badge_purchases ADD COLUMN purchase_priv_key BLOB; + +ALTER TABLE badge_purchases ADD COLUMN receipt_code TEXT; + +ALTER TABLE badge_purchases ADD COLUMN alert_acked_kind TEXT; + +ALTER TABLE badge_purchases ADD COLUMN alert_acked_episode TEXT; + +ALTER TABLE badge_purchases ADD COLUMN alert_snooze_until TEXT; + +ALTER TABLE payments ADD COLUMN evidence BLOB; + +ALTER TABLE badge_ledger ADD COLUMN entry_type_unknown INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE badge_ledger ADD COLUMN entry_type_value TEXT; + +CREATE INDEX idx_badge_purchases_user ON badge_purchases(user_id); + +ALTER TABLE users ADD COLUMN shown_badge_id INTEGER REFERENCES badge_purchases ON DELETE SET NULL; |] down_m20260731_user_badges :: Query down_m20260731_user_badges = [sql| -DROP TABLE badge_issuances; -DROP TABLE badge_ledger; ALTER TABLE users DROP COLUMN shown_badge_id; -DROP TABLE badge_purchases; -DROP TABLE charges; -DROP TABLE payments; -DROP TABLE badge_offers; -DROP TABLE badge_prices; |] + <> badgeSchemaDown ""