mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-29 06:49:25 +00:00
148 lines
5.2 KiB
Haskell
148 lines
5.2 KiB
Haskell
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
|
|
module BadgeService.Store.Postgres.Migrations (badgeServiceSchemaMigrations, servicePrefix, withPrefix) where
|
|
|
|
import Data.List (sortOn)
|
|
import Data.Text (Text)
|
|
import Simplex.Chat.Store.Postgres.Migrations.M20260915_user_badges (badgeSchema, badgeSchemaDown, withPrefix)
|
|
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
|
import Text.RawString.QQ (r)
|
|
|
|
badgeServiceSchemaMigrations :: [Migration]
|
|
badgeServiceSchemaMigrations = sortOn name $ map migration schemaMigrations
|
|
where
|
|
migration (name, up, down) = Migration {name, up, down}
|
|
|
|
schemaMigrations :: [(String, Text, Maybe Text)]
|
|
schemaMigrations =
|
|
[ ("20260915_badge_service_schema", m20260915_badge_service_schema, Just down_m20260915_badge_service_schema),
|
|
("20260918_badge_group_ops", m20260918_badge_group_ops, Just down_m20260918_badge_group_ops)
|
|
]
|
|
|
|
-- | The client tables share this database, so the service tables are the same names behind a prefix.
|
|
servicePrefix :: Text
|
|
servicePrefix = "sx_badge_service_"
|
|
|
|
m20260915_badge_service_schema :: Text
|
|
m20260915_badge_service_schema =
|
|
badgeSchema servicePrefix
|
|
<> withPrefix
|
|
servicePrefix
|
|
-- The payment columns are added to @payments, which badgeSchema owns. crypto_paid,
|
|
-- crypto_due and paid_in_full record the provider's own figures: it applies a payment
|
|
-- tolerance and adds a network fee after a partial payment, so what is owed and whether
|
|
-- an invoice is settled are its verdicts, not amounts recomputable from what we store.
|
|
[r|
|
|
ALTER TABLE @payments ADD COLUMN crypto_paid TEXT;
|
|
|
|
ALTER TABLE @payments ADD COLUMN crypto_due TEXT;
|
|
|
|
ALTER TABLE @payments ADD COLUMN paid_in_full SMALLINT NOT NULL DEFAULT 0;
|
|
|
|
CREATE TABLE @badge_codes(
|
|
badge_code_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
code_hash BYTEA NOT NULL,
|
|
badge_type TEXT NOT NULL,
|
|
months SMALLINT NOT NULL,
|
|
code_payment_status TEXT NOT NULL,
|
|
redeemed_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ,
|
|
revoked_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL,
|
|
UNIQUE(code_hash)
|
|
);
|
|
|
|
ALTER TABLE @badge_purchases ADD COLUMN badge_code_id BIGINT REFERENCES @badge_codes;
|
|
|
|
CREATE UNIQUE INDEX @idx_badge_purchases_code ON @badge_purchases(badge_code_id);
|
|
|
|
CREATE TABLE @badge_code_invoices(
|
|
invoice_id TEXT NOT NULL PRIMARY KEY REFERENCES @invoices ON DELETE CASCADE,
|
|
badge_code_id BIGINT NOT NULL REFERENCES @badge_codes,
|
|
price_id TEXT NOT NULL REFERENCES @badge_prices,
|
|
offer_id TEXT REFERENCES @badge_offers,
|
|
provider_ref TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL
|
|
);
|
|
|
|
CREATE INDEX @idx_badge_code_invoices_offer ON @badge_code_invoices(offer_id);
|
|
|
|
CREATE INDEX @idx_badge_code_invoices_price ON @badge_code_invoices(price_id);
|
|
|
|
-- provider_ref is the provider's own id for the invoice; unique so a webhook or poller read
|
|
-- resolves a payment to exactly one invoice.
|
|
CREATE UNIQUE INDEX @idx_badge_code_invoices_provider_ref ON @badge_code_invoices(provider_ref);
|
|
|]
|
|
-- Two filters run on every poller pass and neither may read the whole table, or the pass
|
|
-- lengthens for as long as the service keeps selling: the expiry sweep, on (status, expires_at),
|
|
-- and the read lane, which takes a window of created_at. Status leads the first because it is
|
|
-- matched by equality there; the second matches it with <>, which no index can seek, so it seeks
|
|
-- the window and filters what little that leaves.
|
|
<> withPrefix
|
|
servicePrefix
|
|
[r|
|
|
CREATE INDEX @idx_invoices_status_expires_at ON @invoices(status, expires_at);
|
|
|
|
CREATE INDEX @idx_invoices_created ON @invoices(created_at);
|
|
|]
|
|
|
|
down_m20260915_badge_service_schema :: Text
|
|
down_m20260915_badge_service_schema =
|
|
withPrefix
|
|
servicePrefix
|
|
[r|
|
|
DROP INDEX @idx_invoices_created;
|
|
DROP INDEX @idx_invoices_status_expires_at;
|
|
|
|
DROP INDEX @idx_badge_code_invoices_provider_ref;
|
|
DROP INDEX @idx_badge_code_invoices_offer;
|
|
DROP INDEX @idx_badge_code_invoices_price;
|
|
DROP TABLE @badge_code_invoices;
|
|
|
|
DROP INDEX @idx_badge_purchases_code;
|
|
|]
|
|
<> badgeSchemaDown servicePrefix
|
|
<> withPrefix
|
|
servicePrefix
|
|
[r|
|
|
DROP TABLE @badge_codes;
|
|
|]
|
|
|
|
m20260918_badge_group_ops :: Text
|
|
m20260918_badge_group_ops =
|
|
withPrefix
|
|
servicePrefix
|
|
[r|
|
|
ALTER TABLE @badge_codes ADD COLUMN redeem_limit INTEGER NOT NULL DEFAULT 1;
|
|
|
|
ALTER TABLE @badge_codes ADD COLUMN redeem_count INTEGER NOT NULL DEFAULT 0;
|
|
|
|
-- Redemptions made before this migration must count against the new limit, or every code
|
|
-- redeemed already would read as unspent and could be redeemed once more.
|
|
UPDATE @badge_codes SET redeem_count = 1 WHERE redeemed_at IS NOT NULL;
|
|
|
|
DROP INDEX @idx_badge_purchases_code;
|
|
|
|
CREATE INDEX @idx_badge_purchases_code ON @badge_purchases(badge_code_id);
|
|
|]
|
|
|
|
-- The index stays non-unique, since a multi-use code may already have several purchases.
|
|
down_m20260918_badge_group_ops :: Text
|
|
down_m20260918_badge_group_ops =
|
|
withPrefix
|
|
servicePrefix
|
|
[r|
|
|
ALTER TABLE @badge_codes DROP COLUMN redeem_count;
|
|
ALTER TABLE @badge_codes DROP COLUMN redeem_limit;
|
|
|]
|
|
|
|
{- TODO [badges] deferred with the draft in M20260915_user_badges, service only.
|
|
|
|
ALTER TABLE @payments ADD COLUMN receipt_hash BYTEA;
|
|
|
|
-- down
|
|
ALTER TABLE @payments DROP COLUMN receipt_hash;
|
|
-}
|