feat(payments): add the payments schema, currency domain and money type
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m45s

Stand up the payments data foundation: a self-contained `payments` Postgres
schema for the in-game currency, wallets, benefits, catalog, orders and the
append-only operations ledger, behind a domain package — nothing wired to real
money yet.

- Migration 00010: the `payments` schema and a NOLOGIN confinement role (ALL on
  payments.*, nothing on backend); the ledger with a BEFORE UPDATE/DELETE
  append-only trigger and a partial idempotency index; the materialised
  balances/benefits; the catalog (atoms seeded) + products + per-method prices;
  the typed single-row config; orders and payment_events. There is no
  cross-schema foreign key — account_id is a plain uuid kept consistent in code,
  which keeps the domain extractable. Expand-contract and reversible.
- Money is a bigint in the currency's minor units carried by a `Money` value
  type (exact, math/big): no float ever touches an amount, and a whole-unit
  currency cannot hold a fraction.
- Extend jetgen to generate the payments schema; construct the service in the
  composition root behind a narrow interface with a boot reachability check.
- Tests: integration (role confinement via SET ROLE, the append-only trigger,
  CHECK constraints, the idempotency index, and a forward+backward migration),
  Money unit tests, and an import-boundary test keeping the payments jet code
  private to the domain.
- Docs: PLAN.md, docs/PAYMENTS.md (+ _ru mirror) updated to the built model.
This commit is contained in:
Ilia Denisov
2026-07-08 01:07:56 +02:00
parent d9bc7596c6
commit ce8b5026c1
34 changed files with 2243 additions and 78 deletions
@@ -0,0 +1,20 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
"time"
)
type Balances struct {
AccountID uuid.UUID `sql:"primary_key"`
Source string `sql:"primary_key"`
Chips int32
UpdatedAt time.Time
}
@@ -0,0 +1,22 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
"time"
)
type Benefits struct {
AccountID uuid.UUID `sql:"primary_key"`
Origin string `sql:"primary_key"`
AdsPaidUntil *time.Time
AdsForever bool
Hints int32
UpdatedAt time.Time
}
@@ -0,0 +1,12 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
type CatalogAtom struct {
AtomType string `sql:"primary_key"`
}
@@ -0,0 +1,17 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
type Config struct {
OnlyRow bool `sql:"primary_key"`
RewardedPayoutChips int32
CooldownGlobalSeconds int32
CooldownVsAiSeconds int32
CooldownHintSeconds int32
OrderTTLSeconds int32
}
@@ -0,0 +1,28 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
"time"
)
type Ledger struct {
LedgerID uuid.UUID `sql:"primary_key"`
AccountID uuid.UUID
Kind string
Source *string
Origin *string
ChipsDelta int32
ProductID *uuid.UUID
OrderID *uuid.UUID
Provider *string
ProviderPaymentID *string
Snapshot *string
CreatedAt time.Time
}
@@ -0,0 +1,28 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
"time"
)
type Orders struct {
OrderID uuid.UUID `sql:"primary_key"`
AccountID uuid.UUID
Platform string
ProductID uuid.UUID
ExpectedAmount int64
Currency string
Origin string
Status string
Provider *string
ProviderPaymentID *string
CreatedAt time.Time
UpdatedAt time.Time
}
@@ -0,0 +1,23 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
"time"
)
type PaymentEvents struct {
EventID uuid.UUID `sql:"primary_key"`
AccountID uuid.UUID
OrderID *uuid.UUID
Type string
Payload *string
CreatedAt time.Time
DispatchedAt *time.Time
}
@@ -0,0 +1,21 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
"time"
)
type Product struct {
ProductID uuid.UUID `sql:"primary_key"`
Title string
Active bool
CreatedAt time.Time
UpdatedAt time.Time
}
@@ -0,0 +1,18 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type ProductItem struct {
ProductID uuid.UUID `sql:"primary_key"`
AtomType string `sql:"primary_key"`
Quantity int32
}
@@ -0,0 +1,19 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type ProductPrice struct {
ProductID uuid.UUID
Method *string
Currency string
Amount int64
}
@@ -0,0 +1,87 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Balances = newBalancesTable("payments", "balances", "")
type balancesTable struct {
postgres.Table
// Columns
AccountID postgres.ColumnString
Source postgres.ColumnString
Chips postgres.ColumnInteger
UpdatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type BalancesTable struct {
balancesTable
EXCLUDED balancesTable
}
// AS creates new BalancesTable with assigned alias
func (a BalancesTable) AS(alias string) *BalancesTable {
return newBalancesTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new BalancesTable with assigned schema name
func (a BalancesTable) FromSchema(schemaName string) *BalancesTable {
return newBalancesTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new BalancesTable with assigned table prefix
func (a BalancesTable) WithPrefix(prefix string) *BalancesTable {
return newBalancesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new BalancesTable with assigned table suffix
func (a BalancesTable) WithSuffix(suffix string) *BalancesTable {
return newBalancesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newBalancesTable(schemaName, tableName, alias string) *BalancesTable {
return &BalancesTable{
balancesTable: newBalancesTableImpl(schemaName, tableName, alias),
EXCLUDED: newBalancesTableImpl("", "excluded", ""),
}
}
func newBalancesTableImpl(schemaName, tableName, alias string) balancesTable {
var (
AccountIDColumn = postgres.StringColumn("account_id")
SourceColumn = postgres.StringColumn("source")
ChipsColumn = postgres.IntegerColumn("chips")
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
allColumns = postgres.ColumnList{AccountIDColumn, SourceColumn, ChipsColumn, UpdatedAtColumn}
mutableColumns = postgres.ColumnList{ChipsColumn, UpdatedAtColumn}
defaultColumns = postgres.ColumnList{ChipsColumn, UpdatedAtColumn}
)
return balancesTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
AccountID: AccountIDColumn,
Source: SourceColumn,
Chips: ChipsColumn,
UpdatedAt: UpdatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -0,0 +1,93 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Benefits = newBenefitsTable("payments", "benefits", "")
type benefitsTable struct {
postgres.Table
// Columns
AccountID postgres.ColumnString
Origin postgres.ColumnString
AdsPaidUntil postgres.ColumnTimestampz
AdsForever postgres.ColumnBool
Hints postgres.ColumnInteger
UpdatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type BenefitsTable struct {
benefitsTable
EXCLUDED benefitsTable
}
// AS creates new BenefitsTable with assigned alias
func (a BenefitsTable) AS(alias string) *BenefitsTable {
return newBenefitsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new BenefitsTable with assigned schema name
func (a BenefitsTable) FromSchema(schemaName string) *BenefitsTable {
return newBenefitsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new BenefitsTable with assigned table prefix
func (a BenefitsTable) WithPrefix(prefix string) *BenefitsTable {
return newBenefitsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new BenefitsTable with assigned table suffix
func (a BenefitsTable) WithSuffix(suffix string) *BenefitsTable {
return newBenefitsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newBenefitsTable(schemaName, tableName, alias string) *BenefitsTable {
return &BenefitsTable{
benefitsTable: newBenefitsTableImpl(schemaName, tableName, alias),
EXCLUDED: newBenefitsTableImpl("", "excluded", ""),
}
}
func newBenefitsTableImpl(schemaName, tableName, alias string) benefitsTable {
var (
AccountIDColumn = postgres.StringColumn("account_id")
OriginColumn = postgres.StringColumn("origin")
AdsPaidUntilColumn = postgres.TimestampzColumn("ads_paid_until")
AdsForeverColumn = postgres.BoolColumn("ads_forever")
HintsColumn = postgres.IntegerColumn("hints")
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
allColumns = postgres.ColumnList{AccountIDColumn, OriginColumn, AdsPaidUntilColumn, AdsForeverColumn, HintsColumn, UpdatedAtColumn}
mutableColumns = postgres.ColumnList{AdsPaidUntilColumn, AdsForeverColumn, HintsColumn, UpdatedAtColumn}
defaultColumns = postgres.ColumnList{AdsForeverColumn, HintsColumn, UpdatedAtColumn}
)
return benefitsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
AccountID: AccountIDColumn,
Origin: OriginColumn,
AdsPaidUntil: AdsPaidUntilColumn,
AdsForever: AdsForeverColumn,
Hints: HintsColumn,
UpdatedAt: UpdatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -0,0 +1,78 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var CatalogAtom = newCatalogAtomTable("payments", "catalog_atom", "")
type catalogAtomTable struct {
postgres.Table
// Columns
AtomType postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type CatalogAtomTable struct {
catalogAtomTable
EXCLUDED catalogAtomTable
}
// AS creates new CatalogAtomTable with assigned alias
func (a CatalogAtomTable) AS(alias string) *CatalogAtomTable {
return newCatalogAtomTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new CatalogAtomTable with assigned schema name
func (a CatalogAtomTable) FromSchema(schemaName string) *CatalogAtomTable {
return newCatalogAtomTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new CatalogAtomTable with assigned table prefix
func (a CatalogAtomTable) WithPrefix(prefix string) *CatalogAtomTable {
return newCatalogAtomTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new CatalogAtomTable with assigned table suffix
func (a CatalogAtomTable) WithSuffix(suffix string) *CatalogAtomTable {
return newCatalogAtomTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newCatalogAtomTable(schemaName, tableName, alias string) *CatalogAtomTable {
return &CatalogAtomTable{
catalogAtomTable: newCatalogAtomTableImpl(schemaName, tableName, alias),
EXCLUDED: newCatalogAtomTableImpl("", "excluded", ""),
}
}
func newCatalogAtomTableImpl(schemaName, tableName, alias string) catalogAtomTable {
var (
AtomTypeColumn = postgres.StringColumn("atom_type")
allColumns = postgres.ColumnList{AtomTypeColumn}
mutableColumns = postgres.ColumnList{}
defaultColumns = postgres.ColumnList{}
)
return catalogAtomTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
AtomType: AtomTypeColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -0,0 +1,93 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Config = newConfigTable("payments", "config", "")
type configTable struct {
postgres.Table
// Columns
OnlyRow postgres.ColumnBool
RewardedPayoutChips postgres.ColumnInteger
CooldownGlobalSeconds postgres.ColumnInteger
CooldownVsAiSeconds postgres.ColumnInteger
CooldownHintSeconds postgres.ColumnInteger
OrderTTLSeconds postgres.ColumnInteger
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type ConfigTable struct {
configTable
EXCLUDED configTable
}
// AS creates new ConfigTable with assigned alias
func (a ConfigTable) AS(alias string) *ConfigTable {
return newConfigTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ConfigTable with assigned schema name
func (a ConfigTable) FromSchema(schemaName string) *ConfigTable {
return newConfigTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ConfigTable with assigned table prefix
func (a ConfigTable) WithPrefix(prefix string) *ConfigTable {
return newConfigTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ConfigTable with assigned table suffix
func (a ConfigTable) WithSuffix(suffix string) *ConfigTable {
return newConfigTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newConfigTable(schemaName, tableName, alias string) *ConfigTable {
return &ConfigTable{
configTable: newConfigTableImpl(schemaName, tableName, alias),
EXCLUDED: newConfigTableImpl("", "excluded", ""),
}
}
func newConfigTableImpl(schemaName, tableName, alias string) configTable {
var (
OnlyRowColumn = postgres.BoolColumn("only_row")
RewardedPayoutChipsColumn = postgres.IntegerColumn("rewarded_payout_chips")
CooldownGlobalSecondsColumn = postgres.IntegerColumn("cooldown_global_seconds")
CooldownVsAiSecondsColumn = postgres.IntegerColumn("cooldown_vs_ai_seconds")
CooldownHintSecondsColumn = postgres.IntegerColumn("cooldown_hint_seconds")
OrderTTLSecondsColumn = postgres.IntegerColumn("order_ttl_seconds")
allColumns = postgres.ColumnList{OnlyRowColumn, RewardedPayoutChipsColumn, CooldownGlobalSecondsColumn, CooldownVsAiSecondsColumn, CooldownHintSecondsColumn, OrderTTLSecondsColumn}
mutableColumns = postgres.ColumnList{RewardedPayoutChipsColumn, CooldownGlobalSecondsColumn, CooldownVsAiSecondsColumn, CooldownHintSecondsColumn, OrderTTLSecondsColumn}
defaultColumns = postgres.ColumnList{OnlyRowColumn, RewardedPayoutChipsColumn, CooldownGlobalSecondsColumn, CooldownVsAiSecondsColumn, CooldownHintSecondsColumn, OrderTTLSecondsColumn}
)
return configTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
OnlyRow: OnlyRowColumn,
RewardedPayoutChips: RewardedPayoutChipsColumn,
CooldownGlobalSeconds: CooldownGlobalSecondsColumn,
CooldownVsAiSeconds: CooldownVsAiSecondsColumn,
CooldownHintSeconds: CooldownHintSecondsColumn,
OrderTTLSeconds: OrderTTLSecondsColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -0,0 +1,111 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Ledger = newLedgerTable("payments", "ledger", "")
type ledgerTable struct {
postgres.Table
// Columns
LedgerID postgres.ColumnString
AccountID postgres.ColumnString
Kind postgres.ColumnString
Source postgres.ColumnString
Origin postgres.ColumnString
ChipsDelta postgres.ColumnInteger
ProductID postgres.ColumnString
OrderID postgres.ColumnString
Provider postgres.ColumnString
ProviderPaymentID postgres.ColumnString
Snapshot postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type LedgerTable struct {
ledgerTable
EXCLUDED ledgerTable
}
// AS creates new LedgerTable with assigned alias
func (a LedgerTable) AS(alias string) *LedgerTable {
return newLedgerTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new LedgerTable with assigned schema name
func (a LedgerTable) FromSchema(schemaName string) *LedgerTable {
return newLedgerTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new LedgerTable with assigned table prefix
func (a LedgerTable) WithPrefix(prefix string) *LedgerTable {
return newLedgerTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new LedgerTable with assigned table suffix
func (a LedgerTable) WithSuffix(suffix string) *LedgerTable {
return newLedgerTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newLedgerTable(schemaName, tableName, alias string) *LedgerTable {
return &LedgerTable{
ledgerTable: newLedgerTableImpl(schemaName, tableName, alias),
EXCLUDED: newLedgerTableImpl("", "excluded", ""),
}
}
func newLedgerTableImpl(schemaName, tableName, alias string) ledgerTable {
var (
LedgerIDColumn = postgres.StringColumn("ledger_id")
AccountIDColumn = postgres.StringColumn("account_id")
KindColumn = postgres.StringColumn("kind")
SourceColumn = postgres.StringColumn("source")
OriginColumn = postgres.StringColumn("origin")
ChipsDeltaColumn = postgres.IntegerColumn("chips_delta")
ProductIDColumn = postgres.StringColumn("product_id")
OrderIDColumn = postgres.StringColumn("order_id")
ProviderColumn = postgres.StringColumn("provider")
ProviderPaymentIDColumn = postgres.StringColumn("provider_payment_id")
SnapshotColumn = postgres.StringColumn("snapshot")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{LedgerIDColumn, AccountIDColumn, KindColumn, SourceColumn, OriginColumn, ChipsDeltaColumn, ProductIDColumn, OrderIDColumn, ProviderColumn, ProviderPaymentIDColumn, SnapshotColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{AccountIDColumn, KindColumn, SourceColumn, OriginColumn, ChipsDeltaColumn, ProductIDColumn, OrderIDColumn, ProviderColumn, ProviderPaymentIDColumn, SnapshotColumn, CreatedAtColumn}
defaultColumns = postgres.ColumnList{ChipsDeltaColumn, CreatedAtColumn}
)
return ledgerTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
LedgerID: LedgerIDColumn,
AccountID: AccountIDColumn,
Kind: KindColumn,
Source: SourceColumn,
Origin: OriginColumn,
ChipsDelta: ChipsDeltaColumn,
ProductID: ProductIDColumn,
OrderID: OrderIDColumn,
Provider: ProviderColumn,
ProviderPaymentID: ProviderPaymentIDColumn,
Snapshot: SnapshotColumn,
CreatedAt: CreatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -0,0 +1,111 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Orders = newOrdersTable("payments", "orders", "")
type ordersTable struct {
postgres.Table
// Columns
OrderID postgres.ColumnString
AccountID postgres.ColumnString
Platform postgres.ColumnString
ProductID postgres.ColumnString
ExpectedAmount postgres.ColumnInteger
Currency postgres.ColumnString
Origin postgres.ColumnString
Status postgres.ColumnString
Provider postgres.ColumnString
ProviderPaymentID postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
UpdatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type OrdersTable struct {
ordersTable
EXCLUDED ordersTable
}
// AS creates new OrdersTable with assigned alias
func (a OrdersTable) AS(alias string) *OrdersTable {
return newOrdersTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new OrdersTable with assigned schema name
func (a OrdersTable) FromSchema(schemaName string) *OrdersTable {
return newOrdersTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new OrdersTable with assigned table prefix
func (a OrdersTable) WithPrefix(prefix string) *OrdersTable {
return newOrdersTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new OrdersTable with assigned table suffix
func (a OrdersTable) WithSuffix(suffix string) *OrdersTable {
return newOrdersTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newOrdersTable(schemaName, tableName, alias string) *OrdersTable {
return &OrdersTable{
ordersTable: newOrdersTableImpl(schemaName, tableName, alias),
EXCLUDED: newOrdersTableImpl("", "excluded", ""),
}
}
func newOrdersTableImpl(schemaName, tableName, alias string) ordersTable {
var (
OrderIDColumn = postgres.StringColumn("order_id")
AccountIDColumn = postgres.StringColumn("account_id")
PlatformColumn = postgres.StringColumn("platform")
ProductIDColumn = postgres.StringColumn("product_id")
ExpectedAmountColumn = postgres.IntegerColumn("expected_amount")
CurrencyColumn = postgres.StringColumn("currency")
OriginColumn = postgres.StringColumn("origin")
StatusColumn = postgres.StringColumn("status")
ProviderColumn = postgres.StringColumn("provider")
ProviderPaymentIDColumn = postgres.StringColumn("provider_payment_id")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
allColumns = postgres.ColumnList{OrderIDColumn, AccountIDColumn, PlatformColumn, ProductIDColumn, ExpectedAmountColumn, CurrencyColumn, OriginColumn, StatusColumn, ProviderColumn, ProviderPaymentIDColumn, CreatedAtColumn, UpdatedAtColumn}
mutableColumns = postgres.ColumnList{AccountIDColumn, PlatformColumn, ProductIDColumn, ExpectedAmountColumn, CurrencyColumn, OriginColumn, StatusColumn, ProviderColumn, ProviderPaymentIDColumn, CreatedAtColumn, UpdatedAtColumn}
defaultColumns = postgres.ColumnList{StatusColumn, CreatedAtColumn, UpdatedAtColumn}
)
return ordersTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
OrderID: OrderIDColumn,
AccountID: AccountIDColumn,
Platform: PlatformColumn,
ProductID: ProductIDColumn,
ExpectedAmount: ExpectedAmountColumn,
Currency: CurrencyColumn,
Origin: OriginColumn,
Status: StatusColumn,
Provider: ProviderColumn,
ProviderPaymentID: ProviderPaymentIDColumn,
CreatedAt: CreatedAtColumn,
UpdatedAt: UpdatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -0,0 +1,96 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var PaymentEvents = newPaymentEventsTable("payments", "payment_events", "")
type paymentEventsTable struct {
postgres.Table
// Columns
EventID postgres.ColumnString
AccountID postgres.ColumnString
OrderID postgres.ColumnString
Type postgres.ColumnString
Payload postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
DispatchedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type PaymentEventsTable struct {
paymentEventsTable
EXCLUDED paymentEventsTable
}
// AS creates new PaymentEventsTable with assigned alias
func (a PaymentEventsTable) AS(alias string) *PaymentEventsTable {
return newPaymentEventsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new PaymentEventsTable with assigned schema name
func (a PaymentEventsTable) FromSchema(schemaName string) *PaymentEventsTable {
return newPaymentEventsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new PaymentEventsTable with assigned table prefix
func (a PaymentEventsTable) WithPrefix(prefix string) *PaymentEventsTable {
return newPaymentEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new PaymentEventsTable with assigned table suffix
func (a PaymentEventsTable) WithSuffix(suffix string) *PaymentEventsTable {
return newPaymentEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newPaymentEventsTable(schemaName, tableName, alias string) *PaymentEventsTable {
return &PaymentEventsTable{
paymentEventsTable: newPaymentEventsTableImpl(schemaName, tableName, alias),
EXCLUDED: newPaymentEventsTableImpl("", "excluded", ""),
}
}
func newPaymentEventsTableImpl(schemaName, tableName, alias string) paymentEventsTable {
var (
EventIDColumn = postgres.StringColumn("event_id")
AccountIDColumn = postgres.StringColumn("account_id")
OrderIDColumn = postgres.StringColumn("order_id")
TypeColumn = postgres.StringColumn("type")
PayloadColumn = postgres.StringColumn("payload")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
DispatchedAtColumn = postgres.TimestampzColumn("dispatched_at")
allColumns = postgres.ColumnList{EventIDColumn, AccountIDColumn, OrderIDColumn, TypeColumn, PayloadColumn, CreatedAtColumn, DispatchedAtColumn}
mutableColumns = postgres.ColumnList{AccountIDColumn, OrderIDColumn, TypeColumn, PayloadColumn, CreatedAtColumn, DispatchedAtColumn}
defaultColumns = postgres.ColumnList{CreatedAtColumn}
)
return paymentEventsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
EventID: EventIDColumn,
AccountID: AccountIDColumn,
OrderID: OrderIDColumn,
Type: TypeColumn,
Payload: PayloadColumn,
CreatedAt: CreatedAtColumn,
DispatchedAt: DispatchedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -0,0 +1,90 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Product = newProductTable("payments", "product", "")
type productTable struct {
postgres.Table
// Columns
ProductID postgres.ColumnString
Title postgres.ColumnString
Active postgres.ColumnBool
CreatedAt postgres.ColumnTimestampz
UpdatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type ProductTable struct {
productTable
EXCLUDED productTable
}
// AS creates new ProductTable with assigned alias
func (a ProductTable) AS(alias string) *ProductTable {
return newProductTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ProductTable with assigned schema name
func (a ProductTable) FromSchema(schemaName string) *ProductTable {
return newProductTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ProductTable with assigned table prefix
func (a ProductTable) WithPrefix(prefix string) *ProductTable {
return newProductTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ProductTable with assigned table suffix
func (a ProductTable) WithSuffix(suffix string) *ProductTable {
return newProductTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newProductTable(schemaName, tableName, alias string) *ProductTable {
return &ProductTable{
productTable: newProductTableImpl(schemaName, tableName, alias),
EXCLUDED: newProductTableImpl("", "excluded", ""),
}
}
func newProductTableImpl(schemaName, tableName, alias string) productTable {
var (
ProductIDColumn = postgres.StringColumn("product_id")
TitleColumn = postgres.StringColumn("title")
ActiveColumn = postgres.BoolColumn("active")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
allColumns = postgres.ColumnList{ProductIDColumn, TitleColumn, ActiveColumn, CreatedAtColumn, UpdatedAtColumn}
mutableColumns = postgres.ColumnList{TitleColumn, ActiveColumn, CreatedAtColumn, UpdatedAtColumn}
defaultColumns = postgres.ColumnList{TitleColumn, ActiveColumn, CreatedAtColumn, UpdatedAtColumn}
)
return productTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ProductID: ProductIDColumn,
Title: TitleColumn,
Active: ActiveColumn,
CreatedAt: CreatedAtColumn,
UpdatedAt: UpdatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -0,0 +1,84 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var ProductItem = newProductItemTable("payments", "product_item", "")
type productItemTable struct {
postgres.Table
// Columns
ProductID postgres.ColumnString
AtomType postgres.ColumnString
Quantity postgres.ColumnInteger
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type ProductItemTable struct {
productItemTable
EXCLUDED productItemTable
}
// AS creates new ProductItemTable with assigned alias
func (a ProductItemTable) AS(alias string) *ProductItemTable {
return newProductItemTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ProductItemTable with assigned schema name
func (a ProductItemTable) FromSchema(schemaName string) *ProductItemTable {
return newProductItemTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ProductItemTable with assigned table prefix
func (a ProductItemTable) WithPrefix(prefix string) *ProductItemTable {
return newProductItemTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ProductItemTable with assigned table suffix
func (a ProductItemTable) WithSuffix(suffix string) *ProductItemTable {
return newProductItemTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newProductItemTable(schemaName, tableName, alias string) *ProductItemTable {
return &ProductItemTable{
productItemTable: newProductItemTableImpl(schemaName, tableName, alias),
EXCLUDED: newProductItemTableImpl("", "excluded", ""),
}
}
func newProductItemTableImpl(schemaName, tableName, alias string) productItemTable {
var (
ProductIDColumn = postgres.StringColumn("product_id")
AtomTypeColumn = postgres.StringColumn("atom_type")
QuantityColumn = postgres.IntegerColumn("quantity")
allColumns = postgres.ColumnList{ProductIDColumn, AtomTypeColumn, QuantityColumn}
mutableColumns = postgres.ColumnList{QuantityColumn}
defaultColumns = postgres.ColumnList{}
)
return productItemTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ProductID: ProductIDColumn,
AtomType: AtomTypeColumn,
Quantity: QuantityColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -0,0 +1,87 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var ProductPrice = newProductPriceTable("payments", "product_price", "")
type productPriceTable struct {
postgres.Table
// Columns
ProductID postgres.ColumnString
Method postgres.ColumnString
Currency postgres.ColumnString
Amount postgres.ColumnInteger
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type ProductPriceTable struct {
productPriceTable
EXCLUDED productPriceTable
}
// AS creates new ProductPriceTable with assigned alias
func (a ProductPriceTable) AS(alias string) *ProductPriceTable {
return newProductPriceTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ProductPriceTable with assigned schema name
func (a ProductPriceTable) FromSchema(schemaName string) *ProductPriceTable {
return newProductPriceTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ProductPriceTable with assigned table prefix
func (a ProductPriceTable) WithPrefix(prefix string) *ProductPriceTable {
return newProductPriceTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ProductPriceTable with assigned table suffix
func (a ProductPriceTable) WithSuffix(suffix string) *ProductPriceTable {
return newProductPriceTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newProductPriceTable(schemaName, tableName, alias string) *ProductPriceTable {
return &ProductPriceTable{
productPriceTable: newProductPriceTableImpl(schemaName, tableName, alias),
EXCLUDED: newProductPriceTableImpl("", "excluded", ""),
}
}
func newProductPriceTableImpl(schemaName, tableName, alias string) productPriceTable {
var (
ProductIDColumn = postgres.StringColumn("product_id")
MethodColumn = postgres.StringColumn("method")
CurrencyColumn = postgres.StringColumn("currency")
AmountColumn = postgres.IntegerColumn("amount")
allColumns = postgres.ColumnList{ProductIDColumn, MethodColumn, CurrencyColumn, AmountColumn}
mutableColumns = postgres.ColumnList{ProductIDColumn, MethodColumn, CurrencyColumn, AmountColumn}
defaultColumns = postgres.ColumnList{}
)
return productPriceTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ProductID: ProductIDColumn,
Method: MethodColumn,
Currency: CurrencyColumn,
Amount: AmountColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -0,0 +1,23 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
// UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke
// this method only once at the beginning of the program.
func UseSchema(schema string) {
Balances = Balances.FromSchema(schema)
Benefits = Benefits.FromSchema(schema)
CatalogAtom = CatalogAtom.FromSchema(schema)
Config = Config.FromSchema(schema)
Ledger = Ledger.FromSchema(schema)
Orders = Orders.FromSchema(schema)
PaymentEvents = PaymentEvents.FromSchema(schema)
Product = Product.FromSchema(schema)
ProductItem = ProductItem.FromSchema(schema)
ProductPrice = ProductPrice.FromSchema(schema)
}
@@ -0,0 +1,266 @@
-- Payments data foundation: a self-contained `payments` schema for the in-game
-- currency, wallets, benefits, catalog, orders and the append-only operations
-- ledger. Nothing is wired to real money yet — this is the substrate the rest of
-- the payments domain builds on. Mechanics live in docs/PAYMENTS.md.
--
-- Isolation. The schema is confined to a dedicated NOLOGIN role that holds ALL
-- privileges on `payments.*` and none on `backend.*`; the backend application
-- keeps its single (superuser) pool, so the DB grants are a stepping-stone to a
-- future separate login/process rather than a runtime wall. The runtime wall is
-- code-level: only the payments package reaches these tables (an import-boundary
-- test guards it). There is deliberately NO cross-schema foreign key to
-- `backend.accounts`: `account_id` is a plain uuid, kept referentially honest in
-- code and joined to the tombstoned account / retained-identities dossier by the
-- stable account id — which keeps the domain extractable into its own database.
--
-- The ledger is append-only, enforced by a trigger (a superuser bypasses table
-- privileges, so a REVOKE would not bind the application). Money is stored as an
-- integer amount in the currency's minor units (RUB kopecks; Votes/Stars/chips
-- are whole units, scale 1), never a float — integer-currency integrality is
-- therefore structural.
--
-- Legacy note: backend.accounts.hint_balance and backend.accounts.paid_account
-- are superseded by the segmented model here and are DEPRECATED. They are NOT
-- dropped in this expand phase — reads still use them until the currency core
-- flips them, and the DROP is a later contract-phase migration (image rollback
-- must stay DB-safe). Neither was ever set in production.
--
-- Expand-contract: everything here is additive in its own schema, so a backend
-- image rollback stays DB-safe (older code simply ignores the new schema).
-- +goose Up
CREATE SCHEMA IF NOT EXISTS payments;
-- +goose StatementBegin
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'payments') THEN
CREATE ROLE payments NOLOGIN;
END IF;
END
$$;
-- +goose StatementEnd
GRANT USAGE ON SCHEMA payments TO payments;
-- Base value types (atoms) a product is composed of. A fixed, code-known set.
CREATE TABLE payments.catalog_atom (
atom_type text NOT NULL,
CONSTRAINT catalog_atom_pkey PRIMARY KEY (atom_type),
CONSTRAINT catalog_atom_type_chk
CHECK ((atom_type = ANY (ARRAY['chips'::text, 'hints'::text, 'noads_days'::text, 'tournament'::text])))
);
INSERT INTO payments.catalog_atom (atom_type) VALUES
('chips'), ('hints'), ('noads_days'), ('tournament');
-- A sellable product: a set of atoms at a price. Soft-deleted (deactivated),
-- never removed, so historical orders/ledger keep resolving it.
CREATE TABLE payments.product (
product_id uuid NOT NULL,
title text DEFAULT ''::text NOT NULL,
active boolean DEFAULT true NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT product_pkey PRIMARY KEY (product_id)
);
-- The atom composition of a product (e.g. 250 hints + 30 no-ads days).
CREATE TABLE payments.product_item (
product_id uuid NOT NULL,
atom_type text NOT NULL,
quantity integer NOT NULL,
CONSTRAINT product_item_pkey PRIMARY KEY (product_id, atom_type),
CONSTRAINT product_item_product_fkey FOREIGN KEY (product_id)
REFERENCES payments.product (product_id) ON DELETE CASCADE,
CONSTRAINT product_item_atom_fkey FOREIGN KEY (atom_type)
REFERENCES payments.catalog_atom (atom_type),
CONSTRAINT product_item_quantity_chk CHECK ((quantity > 0))
);
-- Price of a product. A chip pack carries a money price per method
-- (multi-currency Votes/Stars/RUB); a value carries a single chips price
-- (currency='CHIP', method NULL). `amount` is an integer in the currency's
-- minor units (RUB kopecks; Votes/Stars/chips whole, scale 1).
CREATE TABLE payments.product_price (
product_id uuid NOT NULL,
method text,
currency text NOT NULL,
amount bigint NOT NULL,
CONSTRAINT product_price_product_fkey FOREIGN KEY (product_id)
REFERENCES payments.product (product_id) ON DELETE CASCADE,
CONSTRAINT product_price_method_chk
CHECK ((method IS NULL) OR (method = ANY (ARRAY['vk'::text, 'telegram'::text, 'direct'::text]))),
CONSTRAINT product_price_currency_chk
CHECK ((currency = ANY (ARRAY['RUB'::text, 'VOTE'::text, 'XTR'::text, 'CHIP'::text]))),
CONSTRAINT product_price_amount_chk CHECK ((amount >= 0))
);
-- One price row per (product, method, currency); NULL method (a value's chip
-- price) collapses to one row via the COALESCE key.
CREATE UNIQUE INDEX product_price_unique_idx
ON payments.product_price (product_id, COALESCE(method, ''::text), currency);
-- A pre-created purchase intent. `expected_amount` is in `currency` minor units.
CREATE TABLE payments.orders (
order_id uuid NOT NULL,
account_id uuid NOT NULL,
platform text NOT NULL,
product_id uuid NOT NULL,
expected_amount bigint NOT NULL,
currency text NOT NULL,
origin text NOT NULL,
status text DEFAULT 'pending'::text NOT NULL,
provider text,
provider_payment_id text,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT orders_pkey PRIMARY KEY (order_id),
CONSTRAINT orders_product_fkey FOREIGN KEY (product_id)
REFERENCES payments.product (product_id),
CONSTRAINT orders_status_chk
CHECK ((status = ANY (ARRAY['pending'::text, 'paid'::text, 'expired'::text]))),
CONSTRAINT orders_origin_chk
CHECK ((origin = ANY (ARRAY['vk'::text, 'telegram'::text, 'direct'::text]))),
CONSTRAINT orders_currency_chk
CHECK ((currency = ANY (ARRAY['RUB'::text, 'VOTE'::text, 'XTR'::text, 'CHIP'::text]))),
CONSTRAINT orders_expected_amount_chk CHECK ((expected_amount >= 0))
);
-- The pending-expiry sweep scans (status, created_at).
CREATE INDEX orders_status_created_at_idx ON payments.orders (status, created_at);
-- The append-only operations ledger: every fund / spend / admin_grant / refund.
-- `chips_delta` is signed (0 for a grant, which credits values not chips);
-- `snapshot` records the sold atoms + price for a spend/grant. Never updated or
-- deleted (a trigger enforces it); a reversal is a new `refund` row.
CREATE TABLE payments.ledger (
ledger_id uuid NOT NULL,
account_id uuid NOT NULL,
kind text NOT NULL,
source text,
origin text,
chips_delta integer DEFAULT 0 NOT NULL,
product_id uuid,
order_id uuid,
provider text,
provider_payment_id text,
snapshot jsonb,
created_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT ledger_pkey PRIMARY KEY (ledger_id),
CONSTRAINT ledger_product_fkey FOREIGN KEY (product_id)
REFERENCES payments.product (product_id),
CONSTRAINT ledger_order_fkey FOREIGN KEY (order_id)
REFERENCES payments.orders (order_id),
CONSTRAINT ledger_kind_chk
CHECK ((kind = ANY (ARRAY['fund'::text, 'spend'::text, 'admin_grant'::text, 'refund'::text]))),
CONSTRAINT ledger_source_chk
CHECK ((source IS NULL) OR (source = ANY (ARRAY['vk'::text, 'telegram'::text, 'direct'::text]))),
CONSTRAINT ledger_origin_chk
CHECK ((origin IS NULL) OR (origin = ANY (ARRAY['vk'::text, 'telegram'::text, 'direct'::text])))
);
-- Idempotency key: a provider callback credits at most once. Partial so rows
-- without a provider payment id (spend/admin_grant) are unconstrained.
CREATE UNIQUE INDEX ledger_provider_payment_idx
ON payments.ledger (provider, provider_payment_id)
WHERE provider_payment_id IS NOT NULL;
-- +goose StatementBegin
CREATE FUNCTION payments.ledger_append_only() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
RAISE EXCEPTION 'payments.ledger is append-only: % denied', TG_OP;
END;
$$;
-- +goose StatementEnd
CREATE TRIGGER ledger_no_mutation
BEFORE UPDATE OR DELETE ON payments.ledger
FOR EACH ROW EXECUTE FUNCTION payments.ledger_append_only();
-- Materialised chip balance, segmented by funding source. A fast cache of the
-- ledger, recomputable from it; created lazily on first fund (up to three rows
-- per account, absent = zero).
CREATE TABLE payments.balances (
account_id uuid NOT NULL,
source text NOT NULL,
chips integer DEFAULT 0 NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT balances_pkey PRIMARY KEY (account_id, source),
CONSTRAINT balances_source_chk
CHECK ((source = ANY (ARRAY['vk'::text, 'telegram'::text, 'direct'::text]))),
CONSTRAINT balances_chips_chk CHECK ((chips >= 0))
);
-- Materialised benefits, segmented by the purchase origin. no-ads is a duration
-- (ads_paid_until) plus a perpetual override (ads_forever); hints is a count.
-- Created lazily on first grant/spend.
CREATE TABLE payments.benefits (
account_id uuid NOT NULL,
origin text NOT NULL,
ads_paid_until timestamp with time zone,
ads_forever boolean DEFAULT false NOT NULL,
hints integer DEFAULT 0 NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT benefits_pkey PRIMARY KEY (account_id, origin),
CONSTRAINT benefits_origin_chk
CHECK ((origin = ANY (ARRAY['vk'::text, 'telegram'::text, 'direct'::text]))),
CONSTRAINT benefits_hints_chk CHECK ((hints >= 0))
);
-- The single-row tunable config (durations in whole seconds — go-jet stringifies
-- interval; payouts as counts). Edited in the admin console, no release needed.
CREATE TABLE payments.config (
only_row boolean DEFAULT true NOT NULL,
rewarded_payout_chips integer DEFAULT 0 NOT NULL,
cooldown_global_seconds integer DEFAULT 300 NOT NULL,
cooldown_vs_ai_seconds integer DEFAULT 1800 NOT NULL,
cooldown_hint_seconds integer DEFAULT 60 NOT NULL,
order_ttl_seconds integer DEFAULT 1800 NOT NULL,
CONSTRAINT config_pkey PRIMARY KEY (only_row),
CONSTRAINT config_single_row_chk CHECK (only_row)
);
INSERT INTO payments.config (only_row) VALUES (true);
-- Payment lifecycle events for the dispatcher (live stream / botlink / email).
CREATE TABLE payments.payment_events (
event_id uuid NOT NULL,
account_id uuid NOT NULL,
order_id uuid,
type text NOT NULL,
payload jsonb,
created_at timestamp with time zone DEFAULT now() NOT NULL,
dispatched_at timestamp with time zone,
CONSTRAINT payment_events_pkey PRIMARY KEY (event_id),
CONSTRAINT payment_events_order_fkey FOREIGN KEY (order_id)
REFERENCES payments.orders (order_id),
CONSTRAINT payment_events_type_chk
CHECK ((type = ANY (ARRAY['succeeded'::text, 'failed'::text, 'refunded'::text])))
);
CREATE INDEX payment_events_dispatch_idx
ON payments.payment_events (dispatched_at)
WHERE dispatched_at IS NULL;
-- Confine the payments role to this schema: ALL on its tables, nothing on
-- backend. New tables added later inherit the grant via default privileges.
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA payments TO payments;
ALTER DEFAULT PRIVILEGES IN SCHEMA payments GRANT ALL ON TABLES TO payments;
-- +goose Down
ALTER DEFAULT PRIVILEGES IN SCHEMA payments REVOKE ALL ON TABLES FROM payments;
DROP SCHEMA IF EXISTS payments CASCADE;
-- +goose StatementBegin
DO $$
BEGIN
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'payments') THEN
EXECUTE 'DROP OWNED BY payments';
DROP ROLE payments;
END IF;
END
$$;
-- +goose StatementEnd