Documentation
¶
Index ¶
- type Account
- type AccountBalanceParams
- type AccountBalancesParams
- type AccountBalancesRow
- type AccountStatementParams
- type AccountStatementRangeParams
- type AccountStatementRangeRow
- type AccountStatementRow
- type AccountStatusFlagsParams
- type AccountStatusFlagsRow
- type AllAccountBalancesRow
- type ApiKey
- type AuditAnchor
- type AuditLog
- type AuditOutbox
- type CreateAccountParams
- type CreateDisputeParams
- type CreatePostingParams
- type CreateTenantParams
- type CreateTransactionParams
- type CryptoKey
- type CurrentFXRateParams
- type DBTX
- type Dispute
- type FxMarkupDefault
- type FxRate
- type GetAPIKeyByHashRow
- type GetAPIKeyByIDRow
- type GetAccountParams
- type GetAccountRow
- type GetAuditHeadRow
- type GetCryptoKeyVersionParams
- type GetCryptoKeyVersionRow
- type GetCurrentCryptoKeyRow
- type GetDisputeParams
- type GetIdempotencyKeyParams
- type GetIdempotencyKeyRow
- type GetLatestAuditAnchorRow
- type GetOrCreateClearingAccountParams
- type GetOrCreateClearingAccountRow
- type GetPendingByIdempotencyKeyParams
- type GetPendingForUpdateParams
- type GetPendingTransactionParams
- type GetReversalOfParams
- type GetTransactionParams
- type IdempotencyKey
- type InsertAPIKeyParams
- type InsertAuditAnchorParams
- type InsertAuditLogParams
- type InsertAuditOutboxParams
- type InsertFXMarkupDefaultParams
- type InsertFXRateParams
- type InsertIdempotencyKeyParams
- type InsertPendingTransactionParams
- type InsertWebhookDeliveryParams
- type InsertWebhookSubscriptionParams
- type LatestEnvGlobalFXRateParams
- type LatestEnvGlobalFXRateRow
- type ListAPIKeysByTenantRow
- type ListAccountsParams
- type ListAccountsRow
- type ListActiveWebhookSubscriptionsByTenantRow
- type ListAuditByAccountParams
- type ListAuditByAccountRow
- type ListAuditByTransactionParams
- type ListAuditByTransactionRow
- type ListAuditForVerifyPageParams
- type ListAuditForVerifyPageRow
- type ListAuditForVerifyRow
- type ListAuditHeadsRow
- type ListAuditLogSinceChainSeqParams
- type ListAuditLogSinceChainSeqRow
- type ListAuditParams
- type ListAuditRow
- type ListCurrentFXRatesRow
- type ListDisputesParams
- type ListDueWebhookDeliveriesRow
- type ListPendingTransactionsParams
- type ListPostingsByTransactionIDsParams
- type ListPostingsByTransactionIDsRow
- type ListPostingsByTransactionParams
- type ListPostingsByTransactionRow
- type ListTransactionsParams
- type ListWebhookSubscriptionsByTenantRow
- type MarkWebhookDeliveryDeliveredParams
- type MarkWebhookDeliveryFailedParams
- type MintCryptoKeyVersionParams
- type MintCryptoKeyVersionRow
- type PendingApprovedForTransactionParams
- type PendingTransaction
- type Posting
- type Querier
- type Queries
- func (q *Queries) AccountBalance(ctx context.Context, arg AccountBalanceParams) (int64, error)
- func (q *Queries) AccountBalances(ctx context.Context, arg AccountBalancesParams) ([]AccountBalancesRow, error)
- func (q *Queries) AccountStatement(ctx context.Context, arg AccountStatementParams) ([]AccountStatementRow, error)
- func (q *Queries) AccountStatementRange(ctx context.Context, arg AccountStatementRangeParams) ([]AccountStatementRangeRow, error)
- func (q *Queries) AccountStatusFlags(ctx context.Context, arg AccountStatusFlagsParams) ([]AccountStatusFlagsRow, error)
- func (q *Queries) AllAccountBalances(ctx context.Context, tenantID uuid.UUID) ([]AllAccountBalancesRow, error)
- func (q *Queries) AuditOutboxWatermark(ctx context.Context) (int64, error)
- func (q *Queries) CountPendingOutbox(ctx context.Context, tenantID uuid.UUID) (int64, error)
- func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) error
- func (q *Queries) CreateDispute(ctx context.Context, arg CreateDisputeParams) error
- func (q *Queries) CreatePosting(ctx context.Context, arg CreatePostingParams) error
- func (q *Queries) CreateTenant(ctx context.Context, arg CreateTenantParams) error
- func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (time.Time, error)
- func (q *Queries) CurrentFXRate(ctx context.Context, arg CurrentFXRateParams) (FxRate, error)
- func (q *Queries) GetAPIKeyByHash(ctx context.Context, keyHash string) (GetAPIKeyByHashRow, error)
- func (q *Queries) GetAPIKeyByID(ctx context.Context, id uuid.UUID) (GetAPIKeyByIDRow, error)
- func (q *Queries) GetAccount(ctx context.Context, arg GetAccountParams) (GetAccountRow, error)
- func (q *Queries) GetAuditHead(ctx context.Context, tenantID uuid.UUID) (GetAuditHeadRow, error)
- func (q *Queries) GetCryptoKeyVersion(ctx context.Context, arg GetCryptoKeyVersionParams) (GetCryptoKeyVersionRow, error)
- func (q *Queries) GetCurrentCryptoKey(ctx context.Context, tenantID uuid.UUID) (GetCurrentCryptoKeyRow, error)
- func (q *Queries) GetDispute(ctx context.Context, arg GetDisputeParams) (Dispute, error)
- func (q *Queries) GetIdempotencyKey(ctx context.Context, arg GetIdempotencyKeyParams) (GetIdempotencyKeyRow, error)
- func (q *Queries) GetLastAuditHash(ctx context.Context, tenantID uuid.UUID) (pgtype.Text, error)
- func (q *Queries) GetLatestAuditAnchor(ctx context.Context, tenantID uuid.UUID) (GetLatestAuditAnchorRow, error)
- func (q *Queries) GetOrCreateClearingAccount(ctx context.Context, arg GetOrCreateClearingAccountParams) (GetOrCreateClearingAccountRow, error)
- func (q *Queries) GetPendingByIdempotencyKey(ctx context.Context, arg GetPendingByIdempotencyKeyParams) (PendingTransaction, error)
- func (q *Queries) GetPendingForUpdate(ctx context.Context, arg GetPendingForUpdateParams) (PendingTransaction, error)
- func (q *Queries) GetPendingTransaction(ctx context.Context, arg GetPendingTransactionParams) (PendingTransaction, error)
- func (q *Queries) GetReversalOf(ctx context.Context, arg GetReversalOfParams) (Transaction, error)
- func (q *Queries) GetTenant(ctx context.Context, id uuid.UUID) (Tenant, error)
- func (q *Queries) GetTransaction(ctx context.Context, arg GetTransactionParams) (Transaction, error)
- func (q *Queries) GetWebhookDelivery(ctx context.Context, id uuid.UUID) (WebhookDelivery, error)
- func (q *Queries) GetWebhookFanoutCursorForUpdate(ctx context.Context) (int64, error)
- func (q *Queries) GlobalFXMarkupDefault(ctx context.Context) (FxMarkupDefault, error)
- func (q *Queries) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) error
- func (q *Queries) InsertAuditAnchor(ctx context.Context, arg InsertAuditAnchorParams) error
- func (q *Queries) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) error
- func (q *Queries) InsertAuditOutbox(ctx context.Context, arg InsertAuditOutboxParams) error
- func (q *Queries) InsertFXMarkupDefault(ctx context.Context, arg InsertFXMarkupDefaultParams) (FxMarkupDefault, error)
- func (q *Queries) InsertFXRate(ctx context.Context, arg InsertFXRateParams) (FxRate, error)
- func (q *Queries) InsertIdempotencyKey(ctx context.Context, arg InsertIdempotencyKeyParams) (uuid.UUID, error)
- func (q *Queries) InsertPendingTransaction(ctx context.Context, arg InsertPendingTransactionParams) error
- func (q *Queries) InsertWebhookDelivery(ctx context.Context, arg InsertWebhookDeliveryParams) (int64, error)
- func (q *Queries) InsertWebhookSubscription(ctx context.Context, arg InsertWebhookSubscriptionParams) error
- func (q *Queries) LatestEnvGlobalFXRate(ctx context.Context, arg LatestEnvGlobalFXRateParams) (LatestEnvGlobalFXRateRow, error)
- func (q *Queries) ListAPIKeysByTenant(ctx context.Context, tenantID uuid.UUID) ([]ListAPIKeysByTenantRow, error)
- func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]ListAccountsRow, error)
- func (q *Queries) ListActiveWebhookSubscriptionsByTenant(ctx context.Context, tenantID uuid.UUID) ([]ListActiveWebhookSubscriptionsByTenantRow, error)
- func (q *Queries) ListAudit(ctx context.Context, arg ListAuditParams) ([]ListAuditRow, error)
- func (q *Queries) ListAuditByAccount(ctx context.Context, arg ListAuditByAccountParams) ([]ListAuditByAccountRow, error)
- func (q *Queries) ListAuditByTransaction(ctx context.Context, arg ListAuditByTransactionParams) ([]ListAuditByTransactionRow, error)
- func (q *Queries) ListAuditForVerify(ctx context.Context, tenantID uuid.UUID) ([]ListAuditForVerifyRow, error)
- func (q *Queries) ListAuditForVerifyPage(ctx context.Context, arg ListAuditForVerifyPageParams) ([]ListAuditForVerifyPageRow, error)
- func (q *Queries) ListAuditHeads(ctx context.Context) ([]ListAuditHeadsRow, error)
- func (q *Queries) ListAuditLogSinceChainSeq(ctx context.Context, arg ListAuditLogSinceChainSeqParams) ([]ListAuditLogSinceChainSeqRow, error)
- func (q *Queries) ListCurrentFXRates(ctx context.Context, tenantID pgtype.UUID) ([]ListCurrentFXRatesRow, error)
- func (q *Queries) ListDisputes(ctx context.Context, arg ListDisputesParams) ([]Dispute, error)
- func (q *Queries) ListDueWebhookDeliveries(ctx context.Context, batchLimit int32) ([]ListDueWebhookDeliveriesRow, error)
- func (q *Queries) ListPendingTransactions(ctx context.Context, arg ListPendingTransactionsParams) ([]PendingTransaction, error)
- func (q *Queries) ListPostingsByTransaction(ctx context.Context, arg ListPostingsByTransactionParams) ([]ListPostingsByTransactionRow, error)
- func (q *Queries) ListPostingsByTransactionIDs(ctx context.Context, arg ListPostingsByTransactionIDsParams) ([]ListPostingsByTransactionIDsRow, error)
- func (q *Queries) ListTenants(ctx context.Context, limit int32) ([]Tenant, error)
- func (q *Queries) ListTransactions(ctx context.Context, arg ListTransactionsParams) ([]Transaction, error)
- func (q *Queries) ListWebhookDeliveriesBySubscription(ctx context.Context, subscriptionID uuid.UUID) ([]WebhookDelivery, error)
- func (q *Queries) ListWebhookSubscriptionsByTenant(ctx context.Context, tenantID uuid.UUID) ([]ListWebhookSubscriptionsByTenantRow, error)
- func (q *Queries) MarkAuditOutboxProcessed(ctx context.Context, id int64) error
- func (q *Queries) MarkWebhookDeliveryDelivered(ctx context.Context, arg MarkWebhookDeliveryDeliveredParams) (int64, error)
- func (q *Queries) MarkWebhookDeliveryFailed(ctx context.Context, arg MarkWebhookDeliveryFailedParams) (int64, error)
- func (q *Queries) MintCryptoKeyVersion(ctx context.Context, arg MintCryptoKeyVersionParams) (MintCryptoKeyVersionRow, error)
- func (q *Queries) PendingApprovedForTransaction(ctx context.Context, arg PendingApprovedForTransactionParams) (bool, error)
- func (q *Queries) ResolveDispute(ctx context.Context, arg ResolveDisputeParams) (Dispute, error)
- func (q *Queries) RevokeAPIKey(ctx context.Context, id uuid.UUID) (int64, error)
- func (q *Queries) RolledUpBalance(ctx context.Context, arg RolledUpBalanceParams) (int64, error)
- func (q *Queries) ScanUnprocessedAuditOutbox(ctx context.Context, arg ScanUnprocessedAuditOutboxParams) ([]ScanUnprocessedAuditOutboxRow, error)
- func (q *Queries) SetAPIKeyScopesByHash(ctx context.Context, arg SetAPIKeyScopesByHashParams) error
- func (q *Queries) SetAccountParent(ctx context.Context, arg SetAccountParentParams) (int64, error)
- func (q *Queries) SetAccountStatus(ctx context.Context, arg SetAccountStatusParams) (int64, error)
- func (q *Queries) SetTenantSettings(ctx context.Context, arg SetTenantSettingsParams) (int64, error)
- func (q *Queries) SetTenantStatus(ctx context.Context, arg SetTenantStatusParams) (int64, error)
- func (q *Queries) SetWebhookFanoutCursor(ctx context.Context, lastChainSeq int64) error
- func (q *Queries) SetWebhookSubscriptionActive(ctx context.Context, arg SetWebhookSubscriptionActiveParams) (int64, error)
- func (q *Queries) ShredCurrentCryptoKey(ctx context.Context, tenantID uuid.UUID) error
- func (q *Queries) SweepExpiredIdempotencyKeysBatch(ctx context.Context, batchSize int32) (int64, error)
- func (q *Queries) SweepExpiredPending(ctx context.Context, olderThanSeconds float64) ([]PendingTransaction, error)
- func (q *Queries) TenantDailyDebits(ctx context.Context, tenantID uuid.UUID) ([]TenantDailyDebitsRow, error)
- func (q *Queries) TenantFXMarkupDefault(ctx context.Context, tenantID pgtype.UUID) (FxMarkupDefault, error)
- func (q *Queries) TouchAPIKeyLastUsed(ctx context.Context, arg TouchAPIKeyLastUsedParams) error
- func (q *Queries) TrialBalanceAccounts(ctx context.Context, tenantID uuid.UUID) ([]TrialBalanceAccountsRow, error)
- func (q *Queries) TrialBalanceByCurrency(ctx context.Context, tenantID uuid.UUID) ([]TrialBalanceByCurrencyRow, error)
- func (q *Queries) UpdatePendingStatus(ctx context.Context, arg UpdatePendingStatusParams) error
- func (q *Queries) WithTx(tx pgx.Tx) *Queries
- type ResolveDisputeParams
- type RolledUpBalanceParams
- type ScanUnprocessedAuditOutboxParams
- type ScanUnprocessedAuditOutboxRow
- type SetAPIKeyScopesByHashParams
- type SetAccountParentParams
- type SetAccountStatusParams
- type SetTenantSettingsParams
- type SetTenantStatusParams
- type SetWebhookSubscriptionActiveParams
- type Tenant
- type TenantDailyDebitsRow
- type TouchAPIKeyLastUsedParams
- type Transaction
- type TrialBalanceAccountsRow
- type TrialBalanceByCurrencyRow
- type UpdatePendingStatusParams
- type WebhookDelivery
- type WebhookFanoutCursor
- type WebhookSubscription
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AccountBalanceParams ¶
type AccountBalancesParams ¶
type AccountBalancesRow ¶
type AccountStatementParams ¶
type AccountStatementRangeParams ¶
type AccountStatementRangeParams struct {
TenantID uuid.UUID
AccountID uuid.UUID
FromTs pgtype.Timestamptz
ToTs pgtype.Timestamptz
RowLimit int32
}
type AccountStatementRow ¶
type AccountStatusFlagsRow ¶
type AllAccountBalancesRow ¶ added in v1.0.0
type AuditAnchor ¶
type AuditOutbox ¶
type CreateAccountParams ¶
type CreateDisputeParams ¶
type CreatePostingParams ¶
type CreateTenantParams ¶
type CreateTransactionParams ¶
type CreateTransactionParams struct {
ID uuid.UUID
TenantID uuid.UUID
FxSourceAmount pgtype.Int8
FxConvertedAmount pgtype.Int8
FxMidRateE8 pgtype.Int8
FxSpreadBps pgtype.Int4
FxAppliedE8 pgtype.Int8
FxRateSource pgtype.Text
FxEffectiveAt pgtype.Timestamptz
FxRateID pgtype.Int8
ReversesTransactionID pgtype.UUID
Reference pgtype.Text
EffectiveAt pgtype.Timestamptz
}
type CurrentFXRateParams ¶
type FxMarkupDefault ¶ added in v1.0.0
type GetAPIKeyByHashRow ¶
type GetAPIKeyByIDRow ¶
type GetAccountRow ¶
type GetAuditHeadRow ¶
type GetCryptoKeyVersionRow ¶
type GetCurrentCryptoKeyRow ¶
type GetIdempotencyKeyParams ¶
type GetIdempotencyKeyRow ¶
type GetLatestAuditAnchorRow ¶
type GetPendingByIdempotencyKeyParams ¶ added in v1.0.0
type GetPendingForUpdateParams ¶ added in v1.0.0
type GetPendingTransactionParams ¶ added in v1.0.0
type GetReversalOfParams ¶
type IdempotencyKey ¶
type InsertAPIKeyParams ¶
type InsertAuditAnchorParams ¶
type InsertAuditLogParams ¶
type InsertAuditOutboxParams ¶
type InsertFXMarkupDefaultParams ¶ added in v1.0.0
type InsertFXRateParams ¶
type InsertPendingTransactionParams ¶ added in v1.0.0
type LatestEnvGlobalFXRateParams ¶ added in v1.0.0
type LatestEnvGlobalFXRateRow ¶ added in v1.0.0
type ListAPIKeysByTenantRow ¶
type ListAccountsParams ¶
type ListAccountsRow ¶
type ListAuditByAccountRow ¶
type ListAuditForVerifyRow ¶
type ListAuditHeadsRow ¶
type ListAuditParams ¶ added in v1.0.0
type ListAuditRow ¶ added in v1.0.0
type ListCurrentFXRatesRow ¶ added in v1.0.0
type ListDisputesParams ¶
type ListPendingTransactionsParams ¶ added in v1.0.0
type ListTransactionsParams ¶
type ListTransactionsParams struct {
TenantID uuid.UUID
FromTs pgtype.Timestamptz
ToTs pgtype.Timestamptz
EffectiveFrom pgtype.Timestamptz
EffectiveTo pgtype.Timestamptz
Reference pgtype.Text
AfterCreatedAt time.Time
AfterID uuid.UUID
PageLimit int32
}
type MintCryptoKeyVersionRow ¶
type PendingApprovedForTransactionParams ¶ added in v1.0.0
type PendingTransaction ¶ added in v1.0.0
type PendingTransaction struct {
ID uuid.UUID
TenantID uuid.UUID
Kind string
Payload []byte
Status string
ThresholdCcy string
ThresholdAmt int64
CreatedBy string
CreatedAt time.Time
DecidedBy pgtype.Text
DecidedAt pgtype.Timestamptz
Reason pgtype.Text
TransactionID pgtype.UUID
IdempotencyKey pgtype.Text
}
type Querier ¶
type Querier interface {
AccountBalance(ctx context.Context, arg AccountBalanceParams) (int64, error)
// Task 5.5, audit A1.5: each named account's derived balance, read inside
// the caller's SERIALIZABLE RunInTx body so it is consistent with the
// CreateTransaction write that follows in the same transaction: two
// concurrent posts that would each individually keep an account above its
// floor, but together breach it, are a genuine read-write antidependency
// SERIALIZABLE detects and aborts one of. Called ONLY for accounts that
// AccountStatusFlags reported as having a MinBalance configured (see
// AccountStatusFlags's own doc comment for why this query is kept separate
// and only run when actually needed). The LEFT JOIN (rather than a subquery
// per account) means an account with no postings yet still returns one row,
// with balance COALESCEd to 0.
AccountBalances(ctx context.Context, arg AccountBalancesParams) ([]AccountBalancesRow, error)
// Postings affecting an account, newest first, each with the running balance as
// of that posting. The running balance is a window SUM over the account's full
// posting history (the CTE); the keyset filter and limit then return one page.
// after_created_at / after_id are the keyset position: pass a far-future
// timestamp and the max uuid for the first page.
AccountStatement(ctx context.Context, arg AccountStatementParams) ([]AccountStatementRow, error)
// The per-account period statement export (Task 6.3, audit A9.2): like
// AccountStatement above, the running balance is a window SUM over the
// account's FULL posting history (the CTE), so a filtered page still shows
// each entry's real running balance, not one reset to the filtered window.
// from_ts/to_ts are optional via sqlc.narg (NULL disables that bound, the
// same half-open [from, to) convention ListTransactions uses); the outer
// query applies the date filter and caps the result at row_limit, requested
// by the caller as one more than the export cap it wants so a truncated
// export can be detected without a second round trip.
AccountStatementRange(ctx context.Context, arg AccountStatementRangeParams) ([]AccountStatementRangeRow, error)
// Task 5.5, audit A1.5: each named account's current status, min_balance,
// and is_system flag ONLY (no balance), read inside the caller's
// SERIALIZABLE RunInTx body (see domain.Tx.AccountPostingStates). This is
// deliberately split from AccountBalances below: this query touches only the
// accounts table, which nothing in the posting path ever writes to (a post
// inserts into transactions/postings/audit_outbox/idempotency_keys, never
// accounts), so it can never be the read side of a SERIALIZABLE read-write
// antidependency against a concurrent post. That is exactly what a combined
// query joining postings would risk: reading every historical posting for a
// hot account inside the same transaction as a concurrent INSERT into that
// account's postings is precisely the kind of broad read-write overlap
// SERIALIZABLE flags, and doing it unconditionally on every single post
// (unlike the opt-in TenantDailyDebits check) reintroduced, under many-way
// single-tenant concurrency onto a handful of accounts, the same class of
// retry storm ADR-017 removed the audit chain read to get rid of (see
// TestPostConcurrentStressSingleTenant). AccountBalances is now only ever
// called for the subset of accounts that actually have a MinBalance set.
AccountStatusFlags(ctx context.Context, arg AccountStatusFlagsParams) ([]AccountStatusFlagsRow, error)
// Every account for a tenant with its own derived balance (LEFT JOIN so an
// account with no postings returns 0) and its parent_id, so the caller can
// build the tree and roll up in memory in one pass. Ordered by name, id for a
// stable base order the Go rollup then re-threads parent-before-child.
// Bounded read (audit remediation): one more than ledger.MaxReportRows, so the
// service can detect "too large for a single unpaged response" and refuse
// rather than stream an unbounded result set into memory. Keep in sync with
// ledger.MaxReportRows.
AllAccountBalances(ctx context.Context, tenantID uuid.UUID) ([]AllAccountBalancesRow, error)
// The oldest transaction id still in flight, cast the same way audit_outbox.txid
// is (xid8 has no direct cast to bigint). A row whose txid is strictly below
// this watermark is guaranteed committed and safe to chain (ADR-017,
// "Ordering: process only settled rows, in transaction-commit order").
AuditOutboxWatermark(ctx context.Context) (int64, error)
// Unprocessed outbox rows for one tenant: the lag the verify endpoint reports
// alongside the chained head (ADR-017 section 5), so a caller can see whether
// the chain is current or behind.
CountPendingOutbox(ctx context.Context, tenantID uuid.UUID) (int64, error)
// min_balance (Task 5.5, audit A1.5), party_reference, and party_type (Task
// 6.1, audit A9.1) are all nullable: sqlc.narg leaves each unset (NULL) when
// the caller passes no value, matching "no floor configured" / "no party
// linkage supplied", every account's behavior before these columns existed.
// status is NOT inserted explicitly: the column default ('active', migration
// 0022) applies, the same way CreateTenant leaves status to the column
// default.
CreateAccount(ctx context.Context, arg CreateAccountParams) error
// Task 6.3, audit A9.2: status is NOT inserted explicitly (the column
// default 'open' applies), the same convention CreateAccount leaves status
// to its own column default for.
CreateDispute(ctx context.Context, arg CreateDisputeParams) error
CreatePosting(ctx context.Context, arg CreatePostingParams) error
CreateTenant(ctx context.Context, arg CreateTenantParams) error
// currency lives on each posting now (ADR-014), not here: an FX transaction
// spans two currencies, so there is no single transaction-level value left to
// store. The fx_* columns are the immutable snapshot of the conversion
// actually applied; all nullable, since a single-currency transaction (still
// the common case) has none of this. reverses_transaction_id (Task 4.2,
// audit A1.2) is likewise nullable: only a reversal carries it. reference and
// effective_at (Task 4.3, audit A1.3) are both nullable client-supplied
// fields. created_at is RETURNED (not just inserted): the caller uses it to
// resolve effective_at's read-time fallback right after a fresh insert,
// without a second round trip (see Repository.txRepo.CreateTransaction).
CreateTransaction(ctx context.Context, arg CreateTransactionParams) (time.Time, error)
// The latest quote for (base, quote) at or before now, preferring a
// tenant-specific row over the global default (Task 2.4, audit A3.3):
// tenant_id = $1 matches the tenant's own rows, and tenant_id IS NULL always
// matches the global default, so a tenant with no row of its own still
// resolves the global one. ORDER BY (tenant_id IS NULL) sorts a tenant-owned
// row (false, i.e. 0) ahead of a global row (true, i.e. 1), so when both
// exist the tenant's own wins regardless of which is more recently
// effective; effective_at DESC, id DESC is then the same "latest, ties
// broken by insertion order" tiebreak CurrentFXRate always used, applied
// within whichever tier (tenant-owned or global) won.
CurrentFXRate(ctx context.Context, arg CurrentFXRateParams) (FxRate, error)
// Joins tenants so the resolver gets the tenant's current status alongside
// the key in the same round trip (Task 2.1, ADR-015): gating needs no extra
// query. The join is safe against a dangling reference: api_keys_tenant_fk
// (migration 0011) guarantees every api_keys row's tenant_id has a tenants
// row. scopes, expires_at, and last_used_at (Task 2.2) are returned as-is:
// expiry and scope enforcement happen in the resolver and the transport
// middleware, not in this query.
GetAPIKeyByHash(ctx context.Context, keyHash string) (GetAPIKeyByHashRow, error)
// Raw fetch by id, unfiltered by revoked_at: the admin surface (Task 2.2b)
// uses this to look up an existing key (including an already-revoked one) by
// id, for example to copy a key's tenant/name/scopes when rotating it. It
// does not join tenants: callers that need the tenant's current status look
// it up separately via GetTenant.
GetAPIKeyByID(ctx context.Context, id uuid.UUID) (GetAPIKeyByIDRow, error)
GetAccount(ctx context.Context, arg GetAccountParams) (GetAccountRow, error)
// The tenant's current head: chain_seq and row_hash of its latest audit_log
// row (Task 5.3). See GetLastAuditHash's own comment (ADR-017) for why
// chain_seq, not id or created_at, is the correct "latest" order; this is
// the same lookup, just also returning chain_seq, which GetLastAuditHash's
// only caller (the chainer) never needed since it only ever extends the
// chain from a row_hash. Used to surface the live head alongside the last
// off-box anchor (the verify-audit-chain endpoint, internal/api/audit.go)
// and by VerifyFromLatestAnchor to fall back to a full verify when no
// anchor exists yet. ErrNoRows means the tenant has no audit rows at all.
GetAuditHead(ctx context.Context, tenantID uuid.UUID) (GetAuditHeadRow, error)
// Read-only lookup of ONE SPECIFIC DEK version, for a decrypt whose stored
// ciphertext names the version it was sealed under (ADR-018). Never
// creates a row: a description already carrying internal/crypto.EncodingPrefix
// implies a key existed for that exact version at encrypt time, so
// pgx.ErrNoRows here is a genuine inconsistency for the caller to surface as
// an error, not the ordinary first-use case.
GetCryptoKeyVersion(ctx context.Context, arg GetCryptoKeyVersionParams) (GetCryptoKeyVersionRow, error)
// ADR-018 (Task 6.2 fix): tenant_id's CURRENT (highest-version) key row,
// whatever its shredded state. internal/crypto.Cipher.Encrypt uses this to
// decide whether to reuse it (found && not shredded) or mint a fresh,
// forward version (not found at all, or the current one is shredded): see
// MintCryptoKeyVersion below. pgx.ErrNoRows here means the tenant has never
// encrypted anything and never been shredded: a genuine first-use case, not
// an error, for the caller to handle.
GetCurrentCryptoKey(ctx context.Context, tenantID uuid.UUID) (GetCurrentCryptoKeyRow, error)
GetDispute(ctx context.Context, arg GetDisputeParams) (Dispute, error)
// An expired row is treated as absent (Task 4.5, audit A1.4): the "AND
// expires_at > now()" filter is what makes a key whose replay window has
// passed behave exactly like a key that was never written, from the caller's
// point of view, whether or not the background sweep has physically deleted
// the row yet.
GetIdempotencyKey(ctx context.Context, arg GetIdempotencyKeyParams) (GetIdempotencyKeyRow, error)
// The tenant's most recent row_hash, used to extend the per-tenant hash chain.
// Ordered by chain_seq, not id (ADR-017 IMPORTANT 2, migration 0016): id is a
// UUIDv7, monotonic only within the ONE process that minted it, so a leader
// failover to a different host with clock skew can mint an id LOWER than the
// current head, and ordering by id would then return the wrong "latest" row
// and corrupt the chain. chain_seq is a plain ascending sequence the single
// chainer process advances on every insert, in the same order it assigns
// row_hash values, so it stays correct across any failover regardless of
// clock skew. created_at is copied from the ORIGINATING event's post time
// (audit_outbox.occurred_at), which under concurrent posts across many
// transactions is NOT guaranteed to be monotonic with the order those
// transactions actually commit in (a transaction that starts later can
// commit first); ordering by created_at would occasionally return the wrong
// "latest" row too. A fresh tenant (or one with no rows yet) surfaces as
// pgx.ErrNoRows; the caller treats that as the chain's genesis
// (domain.AuditGenesisHash).
GetLastAuditHash(ctx context.Context, tenantID uuid.UUID) (pgtype.Text, error)
// The tenant's most recently recorded anchor (Task 5.3): the chain_seq,
// row_hash, and timestamp the anchor job last logged off-box for this
// tenant. ErrNoRows means no anchor has ever been recorded (a brand-new
// tenant, or one that posted before the anchor job's first tick).
GetLatestAuditAnchor(ctx context.Context, tenantID uuid.UUID) (GetLatestAuditAnchorRow, error)
// The per-tenant per-currency FX clearing account (ADR-014, is_system=true),
// created lazily on first use. Keyed by (tenant_id, name): name is the
// reserved, deterministic "fx.clearing.<CURRENCY>" string the caller builds,
// so a second call for the same tenant and currency always resolves to the
// same row instead of creating a duplicate. The ON CONFLICT arbiter is the
// partial unique index accounts_system_name_uniq (migration 0010), which only
// covers is_system rows, so this can never collide with an ordinary
// user-named account.
//
// On conflict this does a no-op DO UPDATE (id set to its own current value)
// rather than DO NOTHING. DO NOTHING never RETURNs the conflicting row, which
// an earlier version of this query worked around with a CTE plus a fallback
// SELECT unioned into the same statement, guarded by NOT EXISTS. That
// fallback ran against the single statement's original snapshot: when two
// callers raced to create the same tenant's first clearing account for a
// currency, the loser blocked on the conflict, and by the time it unblocked
// (after the winner committed) its own fallback SELECT could still miss the
// now-committed row, since the snapshot predated that commit. Both branches
// of the UNION then returned zero rows to the loser, surfacing as "no rows in
// result set" under concurrent Converts targeting the same pair. DO UPDATE
// instead forces Postgres's own EvalPlanQual re-fetch of the current row
// version as part of resolving the conflict, so RETURNING always yields
// exactly one row, new or existing, in a single round trip with no second
// snapshot to race against.
GetOrCreateClearingAccount(ctx context.Context, arg GetOrCreateClearingAccountParams) (GetOrCreateClearingAccountRow, error)
// ADR-025 section 6 (Lifecycle): the replay-dedup read. holdForApproval
// calls this before inserting a new pending, and again if the insert itself
// loses a race against a concurrent identical retry
// (pending_transactions_idempotency_idx unique violation), so a retry of the
// same gated create with the same idempotency key always returns the one
// pending that key produced, never a second one.
GetPendingByIdempotencyKey(ctx context.Context, arg GetPendingByIdempotencyKeyParams) (PendingTransaction, error)
// Task 4 (ADR-025): the row-locking read a decision (approve/reject/cancel)
// takes before transitioning a pending, so two racing decisions cannot both
// win; the loser's transaction blocks on this lock until the winner commits
// or rolls back, then re-reads the now-terminal row. Called only from
// inside RunInTx (via the Tx port), never as a standalone read.
GetPendingForUpdate(ctx context.Context, arg GetPendingForUpdateParams) (PendingTransaction, error)
GetPendingTransaction(ctx context.Context, arg GetPendingTransactionParams) (PendingTransaction, error)
// The reversal of a given original, if one exists (Task 4.2, audit A1.2):
// transactions_one_reversal_idx (migration 0017) guarantees at most one row
// can ever match, so this is a plain :one lookup, not a list.
GetReversalOf(ctx context.Context, arg GetReversalOfParams) (Transaction, error)
GetTenant(ctx context.Context, id uuid.UUID) (Tenant, error)
GetTransaction(ctx context.Context, arg GetTransactionParams) (Transaction, error)
// Raw fetch by id: tests and any future delivery-inspection tooling read a
// single row's full lifecycle state back this way.
GetWebhookDelivery(ctx context.Context, id uuid.UUID) (WebhookDelivery, error)
// Reads the singleton fan-out cursor and locks its row for the rest of the
// surrounding transaction (Task 4.1): the same "read the watermark, then
// act, all in one transaction" shape ADR-017's chainer uses for audit_log,
// so the cursor read and its eventual advance (SetWebhookFanoutCursor) never
// race a concurrent fan-out pass.
GetWebhookFanoutCursorForUpdate(ctx context.Context) (int64, error)
// The current global default only (tenant_id IS NULL), so the console can
// show it distinctly from a tenant override.
GlobalFXMarkupDefault(ctx context.Context) (FxMarkupDefault, error)
// scopes and expires_at (Task 2.2b) are now written on insert: the admin
// surface (internal/admin) is what actually sets them to something other
// than the column default. Every pre-2.2b caller (cmd/server's demo and
// load-test key provisioning, and every repository test that predates
// scopes) still works unchanged, because the Go-level repository method
// defaults an empty Scopes slice to {read,post} before it ever reaches this
// query, the same default the api_keys.scopes column itself carries.
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) error
// Records tenantID's current chain head as a new off-box-anchored
// checkpoint (Task 5.3, migration 0025). Called only by the periodic
// anchor job (internal/audit.AnchorJob), never the request path: the job
// runs with the RLS GUC unset (a cross-tenant worker, Task 5.4b), so this
// insert is not scoped through withTenant the way a request-path write
// would be.
// signature is the app-held HMAC over (tenant_id, chain_seq, row_hash), or
// NULL when AUDIT_ANCHOR_SIGNING_KEY is unset (signing disabled).
InsertAuditAnchor(ctx context.Context, arg InsertAuditAnchorParams) error
// outbox_id is the source audit_outbox row this audit_log row was chained
// from (ADR-017 MINOR 3, migration 0016): a UNIQUE constraint on it means a
// second attempt to chain the same outbox row fails this insert with a
// unique violation instead of silently forking the chain. chain_seq is left
// to its column DEFAULT (nextval), never supplied by the caller: it is what
// makes chain order immune to any host's clock (see GetLastAuditHash below).
//
// transaction_id, subject_type, and subject_id are all nullable (ADR-025,
// migration 0034): a chained non-transaction lifecycle event carries
// subject_type/subject_id instead of a transaction_id. hash_version is
// copied straight from the source outbox row, so recomputing this row's
// hash later (Verify) always uses the same preimage it was chained with.
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) error
// Writes one outbox row inside the caller's own transaction (ADR-017): the
// event is durable if and only if the surrounding post/convert transaction
// commits. occurred_at, txid, and created_at are all left to their column
// defaults (the database server's now() and pg_current_xact_id(), see
// migration 0015): no chain read, no hash computed, here.
//
// transaction_id, subject_type, and subject_id are all nullable (ADR-025,
// migration 0034): a non-transaction lifecycle event (for example
// approval.rejected) carries subject_type/subject_id instead of a
// transaction_id. hash_version records which row-hash preimage the caller
// computed against, so the chainer and Verify recompute with the same one.
InsertAuditOutbox(ctx context.Context, arg InsertAuditOutboxParams) error
// fx_markup_defaults is append-only (ADR-020): a new default is a new row.
// tenant_id NULL is the global default, a non-NULL tenant_id is that tenant's
// override. effective_at is server-stamped via COALESCE(narg, now()) for the
// same clock-skew reason InsertFXRate stamps server-side. default_spread_bps
// is a nullable narg: a NULL value inserts a cleared row (the tenant follows
// the global default again), matching the nullable fx_rates.spread_bps
// pattern.
InsertFXMarkupDefault(ctx context.Context, arg InsertFXMarkupDefaultParams) (FxMarkupDefault, error)
// fx_rates is append-only (ADR-014): a new quote is a new row, never an
// update, so every rate ever applied to a transaction stays reconstructible.
// tenant_id NULL makes this the global default rate for the pair; a non-NULL
// tenant_id makes it that tenant's own rate (Task 2.4, audit A3.3), resolved
// ahead of the global default by CurrentFXRate below.
//
// effective_at is a nullable named param (sqlc.narg): when the caller omits
// an explicit effective_at, COALESCE falls through to the DATABASE SERVER's
// now(), never the calling process's clock. Stamping an "immediate" rate
// with the caller's clock is what caused a real clock-skew bug (Task 2.4
// remediation): CurrentFXRate below gates on "effective_at <= now()" using
// the server's clock too, so if the caller's clock ran even slightly ahead,
// a just-inserted row was transiently invisible and the query silently fell
// through to the global default. Passing NULL here (rather than the CLI
// host's time.Now()) makes the two clocks the same clock for the common,
// unscheduled case; an explicit future effective_at (a scheduled rate) is
// unaffected, since the caller still supplies it directly.
InsertFXRate(ctx context.Context, arg InsertFXRateParams) (FxRate, error)
// Inserts a fresh idempotency key with a server-computed expires_at (now() +
// ttl_seconds, never the calling process's clock: Task 4.5, audit A1.4,
// consistent with fx_rates.effective_at's own server-side stamping).
//
// ON CONFLICT upserts rather than plain-inserts because a row for
// (tenant_id, idempotency_key) can already be present and EXPIRED: the
// lookup path (GetIdempotencyKey) treats an expired row as absent, so the
// caller proceeds to post a brand-new transaction and lands here again with
// the same key. Without the upsert that would hit the primary key and be
// misread as a genuine duplicate (replaying a stale, no-longer-relevant
// transaction). The "WHERE idempotency_keys.expires_at <= now()" guard on
// the DO UPDATE means the replace only happens when the existing row has, in
// fact, expired; a conflict against a still-live row leaves it untouched and
// the update affects zero rows, so RETURNING yields no row and the caller
// (postgres.txRepo.InsertIdempotencyKey) maps pgx.ErrNoRows to
// domain.ErrDuplicateIdempotencyKey exactly as a plain unique-violation used
// to, preserving replay-on-duplicate for the still-live case.
InsertIdempotencyKey(ctx context.Context, arg InsertIdempotencyKeyParams) (uuid.UUID, error)
// Task 4 (ADR-025): status is NOT inserted explicitly (the column default
// 'pending' applies), the same convention CreateDispute leaves status to its
// own column default for. idempotency_key (migration 0036, ADR-025 section
// 6) is nullable via sqlc.narg: a held create with no caller-supplied key
// inserts NULL, which never collides under pending_transactions_idempotency_idx
// (a partial unique index that only applies where the key is present).
InsertPendingTransaction(ctx context.Context, arg InsertPendingTransactionParams) error
// Creates one fan-out row for a (subscription, audit event) pairing. The
// ON CONFLICT DO NOTHING against the UNIQUE (subscription_id,
// audit_chain_seq) index (migration 0021) is what makes fan-out
// exactly-once into this table even if the fan-out step ever ran twice over
// the same audit_log range: a repeat attempt is a silent no-op, not a
// duplicate delivery row, so execrows reports 0 for a pairing that already
// existed and 1 for a genuinely new one.
InsertWebhookDelivery(ctx context.Context, arg InsertWebhookDeliveryParams) (int64, error)
// Inserts a new webhook_subscriptions row, always active (a caller creates a
// subscription to receive events; deactivating happens later, via
// SetWebhookSubscriptionActive). secret is stored as-is, never hashed (Task
// 4.1, audit A7.1): the delivery worker must read it back in full to sign
// every outbound payload.
InsertWebhookSubscription(ctx context.Context, arg InsertWebhookSubscriptionParams) error
// The latest env-seeded global row for a pair, used by fx.Seed to decide
// whether FX_RATES itself changed. Comparing against this (not the current
// winner) means an admin-API-written row is never clobbered by a re-seed:
// Seed only re-asserts an env rate when the FX_RATES entry differs from the
// last thing Seed itself wrote for that pair.
LatestEnvGlobalFXRate(ctx context.Context, arg LatestEnvGlobalFXRateParams) (LatestEnvGlobalFXRateRow, error)
// Every key for a tenant, oldest first, including revoked ones: the admin
// surface's list view (Task 2.2b) is meant to show a tenant's full key
// history, not just its live keys. Never selects key_hash: plaintext is
// shown once at issue/rotate time and is never recoverable afterward, and
// the hash itself has no business leaving the resolver's lookup path.
ListAPIKeysByTenant(ctx context.Context, tenantID uuid.UUID) ([]ListAPIKeysByTenantRow, error)
ListAccounts(ctx context.Context, arg ListAccountsParams) ([]ListAccountsRow, error)
// The fan-out step's per-tenant read (Task 4.1): every ACTIVE subscription
// for one tenant, including the secret and event_types the fan-out needs to
// decide whether and how to create a webhook_deliveries row for it. Ordered
// by id only for a stable, deterministic iteration order within one fan-out
// pass; it carries no meaning beyond that.
ListActiveWebhookSubscriptionsByTenant(ctx context.Context, tenantID uuid.UUID) ([]ListActiveWebhookSubscriptionsByTenantRow, error)
// Keyset page of the tenant's entire audit log, newest first, paged by id
// alone (see ListAuditByAccount for why id, assigned in chain-insertion order,
// drives ordering rather than created_at). after_id is the keyset position:
// pass the max uuid for the first page, then the last id of the prior page.
ListAudit(ctx context.Context, arg ListAuditParams) ([]ListAuditRow, error)
// Keyset page of audit rows for every transaction with a posting touching the
// account, newest first. after_id is the keyset position: pass the max uuid
// for the first page. Ordered and paged by id alone, not (created_at, id):
// see GetLastAuditHash's comment (ADR-017) for why id, assigned by the
// chainer in true chain-insertion order, is what must drive ordering here,
// not created_at (copied from the original event's post time, which is not
// guaranteed monotonic with commit order under concurrent posts).
ListAuditByAccount(ctx context.Context, arg ListAuditByAccountParams) ([]ListAuditByAccountRow, error)
// Ordered by id, not created_at: see GetLastAuditHash's comment (ADR-017) for
// why id is the chain-consistent order and created_at is not.
ListAuditByTransaction(ctx context.Context, arg ListAuditByTransactionParams) ([]ListAuditByTransactionRow, error)
// Every audit row for the tenant, in true chain order: the full walk used to
// recompute and check the tamper-evident hash chain end to end. Ordered by
// chain_seq, not id or created_at: see GetLastAuditHash's comment (ADR-017,
// migration 0016) for why chain_seq, not id, is the failover-safe chain
// order.
//
// Includes subject_type, subject_id, and hash_version (ADR-025, migration
// 0034) so recomputing a row's hash uses the same preimage it was chained
// with.
ListAuditForVerify(ctx context.Context, tenantID uuid.UUID) ([]ListAuditForVerifyRow, error)
// A bounded page of the tenant's audit rows in true chain order, strictly
// after after_chain_seq (Task 5.3, audit A2.4): the streaming counterpart to
// ListAuditForVerify above, which loads the entire chain into memory at
// once. AuditService.Verify calls this in a loop, advancing
// after_chain_seq to the last row's chain_seq on each page, until a page
// comes back with fewer than page_limit rows, so memory use is bounded by
// page_limit regardless of how long the chain has grown. Includes
// chain_seq (unlike ListAuditForVerify) precisely so the caller can advance
// the cursor without a second round trip. after_chain_seq is 0 to start
// from genesis, or an anchor's chain_seq to start from a trusted checkpoint
// (VerifyFromLatestAnchor).
//
// Also includes subject_type, subject_id, and hash_version (ADR-025,
// migration 0034): this is the query AuditService.Verify actually pages
// through, so it must return everything ComputeAuditRowHash needs to
// recompute each row (both v1 and v2) exactly as it was chained.
ListAuditForVerifyPage(ctx context.Context, arg ListAuditForVerifyPageParams) ([]ListAuditForVerifyPageRow, error)
// One row per tenant with at least one audit_log entry: that tenant's
// current chain head (chain_seq, row_hash). The anchor job (Task 5.3) reads
// this once per tick rather than enumerating tenants and calling
// GetAuditHead once per tenant (an N+1 round trip): it inserts one
// audit_anchors row and emits one structured log line per tenant returned
// here.
ListAuditHeads(ctx context.Context) ([]ListAuditHeadsRow, error)
// The fan-out step's source read: every chained audit_log event past the
// cursor, oldest first, up to batch_limit. Reads chain_seq (ADR-017's
// failover-safe, skew-proof linearization key), not id or created_at, for
// the same reason GetLastAuditHash does: it is the one order the chainer
// itself guarantees is monotonic across any failover.
//
// transaction_id is nullable (ADR-025, migration 0034): a chained
// non-transaction lifecycle event (for example approval.rejected) has none.
// The fan-out worker maps a null transaction_id to an empty, omitted field
// in the webhook payload, the same convention the rest of the audit read
// path uses. subject_type/subject_id (also ADR-025) are what that kind of
// event carries instead: the fan-out worker copies them onto the payload
// (Task 10) so a consumer can tell which subject the event concerns.
ListAuditLogSinceChainSeq(ctx context.Context, arg ListAuditLogSinceChainSeqParams) ([]ListAuditLogSinceChainSeqRow, error)
// The current effective row per (base, quote) for a tenant plus the global
// defaults: DISTINCT ON collapses each pair to one row, and the ORDER BY puts
// a tenant-owned row (tenant_id IS NULL = false, sorts first) ahead of a
// global row, then latest effective, matching CurrentFXRate's precedence.
// Pass tenant_id NULL to list globals only (tenant_id = NULL is never true, so
// only the tenant_id IS NULL rows match).
ListCurrentFXRates(ctx context.Context, tenantID pgtype.UUID) ([]ListCurrentFXRatesRow, error)
// Filtered, keyset-paged list of a tenant's disputes, newest first (Task
// 6.3, audit A9.2). status is an optional filter via sqlc.narg: NULL
// disables it (every status is returned). Keyset paged by (created_at, id)
// descending, the identical cursor shape ListTransactions and
// AccountStatement already use.
ListDisputes(ctx context.Context, arg ListDisputesParams) ([]Dispute, error)
// The delivery worker's batch read: pending/failed rows whose backoff has
// elapsed, oldest due first, joined to their subscription for the url and
// secret needed to sign and send. "AND ws.active" means a subscription that
// was deactivated after fan-out already created pending rows for it simply
// stops being picked up here (see SetWebhookSubscriptionActive's doc
// comment): its existing rows are left exactly as they are, neither
// delivered nor purged, just never attempted again. FOR UPDATE OF wd SKIP
// LOCKED is defense in depth against two workers ever running at once (the
// leader-election lock is what actually prevents that in normal operation,
// ADR-017's discipline mirrored for this worker): a second reader skips a
// row the first is mid-processing rather than picking it up concurrently.
// This query runs as its own standalone statement (never inside a
// surrounding transaction that also makes the outbound HTTP call), so the
// row lock is released the moment this statement completes; correctness
// does not depend on holding it any longer than that.
ListDueWebhookDeliveries(ctx context.Context, batchLimit int32) ([]ListDueWebhookDeliveriesRow, error)
// Filtered, keyset-paged list of a tenant's pendings, newest first (Task 4,
// ADR-025). status is an optional filter via sqlc.narg: NULL disables it
// (every status is returned). Keyset paged by (created_at, id) descending,
// the identical cursor shape ListDisputes already uses.
ListPendingTransactions(ctx context.Context, arg ListPendingTransactionsParams) ([]PendingTransaction, error)
ListPostingsByTransaction(ctx context.Context, arg ListPostingsByTransactionParams) ([]ListPostingsByTransactionRow, error)
// Batch posting fetch for a page of transactions (Task 4.4, audit A7.2):
// ListTransactions returns up to a page's worth of transaction rows, and
// assembling each one's postings via ListPostingsByTransaction one at a time
// would be N+1 queries for a full page. This fetches every posting for every
// transaction id in the page in a single round trip; the caller groups rows
// back by transaction_id (see Repository.ListTransactions). Ordered by
// transaction_id then (created_at, id), matching ListPostingsByTransaction's
// own per-transaction order, so grouping by transaction_id yields each
// transaction's postings already in that transaction's insertion order.
ListPostingsByTransactionIDs(ctx context.Context, arg ListPostingsByTransactionIDsParams) ([]ListPostingsByTransactionIDsRow, error)
ListTenants(ctx context.Context, limit int32) ([]Tenant, error)
// Filtered, keyset-paged list of a tenant's transactions, newest first (Task
// 4.4, audit A7.2). from_ts/to_ts/reference are optional filters via
// sqlc.narg: NULL disables that filter's clause (it becomes a no-op OR),
// so this single query serves every filter combination rather than one
// built dynamically per request. from_ts is inclusive (created_at >=),
// to_ts is exclusive (created_at <): a half-open [from, to) window.
//
// effective_from/effective_to are the same half-open window, but over the
// value date (Task 4.3's effective_at) rather than created_at (follow-up
// F2): COALESCE(effective_at, created_at) is the same read-time fallback
// used everywhere else effective_at is read (see assembleTransaction), so a
// transaction posted with no explicit value date is filtered as if its value
// date were its post time, never silently excluded from an effective_at
// window that would otherwise match its created_at. Independent of
// from_ts/to_ts: a caller may filter on created_at, effective_at, both, or
// neither.
//
// Keyset paged by (created_at, id) descending, the identical cursor shape
// AccountStatement already uses: after_created_at/after_id are the keyset
// position, a far-future timestamp and the max uuid for the first page.
// page_limit is requested by the caller as one more than the page size it
// actually wants, so a next page can be detected without a second round
// trip; this query itself just returns up to page_limit rows in whatever
// amount it is asked for.
ListTransactions(ctx context.Context, arg ListTransactionsParams) ([]Transaction, error)
// Every delivery row for one subscription, in fan-out order (the same
// chain_seq order the source audit_log events were read in). Used by tests
// to assert fan-out and delivery outcomes end to end.
ListWebhookDeliveriesBySubscription(ctx context.Context, subscriptionID uuid.UUID) ([]WebhookDelivery, error)
// Every subscription for a tenant, oldest first, active or not: the admin
// surface's list view. Never selects secret: it is shown once, at creation
// time, and never recoverable through a list call.
ListWebhookSubscriptionsByTenant(ctx context.Context, tenantID uuid.UUID) ([]ListWebhookSubscriptionsByTenantRow, error)
// Sets processed_at for one outbox row. The chainer calls this in the same
// transaction as the audit_log insert it produced, so a crash between the two
// is impossible: either both happen or neither does.
MarkAuditOutboxProcessed(ctx context.Context, id int64) error
// A 2xx response: terminal success. attempts is set to the total number of
// tries this delivery took (including the successful one), the same total
// MarkWebhookDeliveryFailed accumulates on the way here, so attempts always
// means "how many times this delivery was actually tried", not just "how
// many times it failed". "AND status IN ('pending', 'failed')" guards
// against a state regression: in a two-leader window (SKIP LOCKED is
// defense in depth, not a guarantee, see ListDueWebhookDeliveries) two
// workers can pick up the same due row, and without this guard a slower
// worker's write could still land after a faster worker already reached a
// terminal outcome for it. Once a row is 'delivered' or 'dead', neither mark
// query can move it again: this returns 0 affected rows instead, which the
// caller (internal/webhook) treats as a no-op, not an error.
MarkWebhookDeliveryDelivered(ctx context.Context, arg MarkWebhookDeliveryDeliveredParams) (int64, error)
// A non-2xx response or transport error: the caller (internal/webhook)
// computes the new attempts count, the resulting status ('failed' to retry
// again later, or 'dead' once attempts reaches the configured max), the next
// backoff deadline, and the error text to record, all in Go (backoff and the
// max-attempts cap are application config, not schema), and this query just
// persists that decision. "AND status IN ('pending', 'failed')" is the same
// terminal-state guard MarkWebhookDeliveryDelivered carries (see its doc
// comment): a delivery already 'delivered' (or already 'dead') can never be
// regressed by a late failure write from a second worker that raced this one.
MarkWebhookDeliveryFailed(ctx context.Context, arg MarkWebhookDeliveryFailedParams) (int64, error)
// Atomically creates tenant_id's crypto_keys row at the given version,
// wrapping the CALLER-GENERATED candidate_wrapped_dek (ADR-018: only
// internal/crypto.Cipher ever holds the master key needed to produce one).
// pg_advisory_xact_lock serializes this against ShredCurrentCryptoKey for
// the SAME tenant (both take the identical hashtextextended(tenant_id, 0)
// key), so a version this call is in the middle of minting can never be the
// exact version a concurrent shred call means to destroy, or vice versa;
// the lock is released automatically at the end of this call's transaction
// (withTenant's COMMIT/ROLLBACK).
//
// The ON CONFLICT DO UPDATE is the same "force RETURNING to yield the
// winning row" trick migration 0027's original GetOrCreateCryptoKey used at
// the single-version grain (see also GetOrCreateClearingAccount): if two
// callers race to mint the exact same (tenant_id, version) pair, for example
// two concurrent first-use Encrypt calls for a brand-new tenant both
// targeting version 1, one wins and the other's own candidate wrapped_dek is
// silently discarded; both RETURNING the SAME winning row in one round
// trip, so both callers end up using the identical DEK.
// sqlc.arg(tenant_id), repeated, names the SAME parameter (one field in the
// generated Params struct) at every use; the redundant ::uuid cast at each
// use (not just the ::text one hashtextextended needs) is deliberate, not
// decorative: Postgres assigns an untyped placeholder's type by unifying
// every one of its uses in the statement, and the ::text cast below would
// otherwise make the planner infer the WHOLE parameter as text, including
// the INSERT's tenant_id (uuid) column, which then fails to bind ("column
// tenant_id is of type uuid but expression is of type text"). Casting to
// uuid explicitly at each use pins its type regardless of statement order.
MintCryptoKeyVersion(ctx context.Context, arg MintCryptoKeyVersionParams) (MintCryptoKeyVersionRow, error)
// Task 6 (ADR-025): the reverse-of-approved exemption's read. True only when
// some pending transaction's decision produced transaction_id AND that
// decision was an approval (a rejected or cancelled pending never sets
// transaction_id at all, per the transaction_id IS NULL OR status =
// 'approved' check in migration 0035, so the status filter here is mostly
// belt-and-suspenders). TransactionService.ReverseTransaction calls this
// before gating a reversal, never from inside RunInTx.
PendingApprovedForTransaction(ctx context.Context, arg PendingApprovedForTransactionParams) (bool, error)
// Task 6.3, audit A9.2: the guarded transition out of 'open'. The WHERE
// status = 'open' clause is what makes resolving an already-resolved (or
// concurrently-being-resolved) dispute return zero rows rather than
// silently overwriting a prior resolution: the caller
// (postgres.Repository.ResolveDispute) treats a zero-row result as
// domain.ErrDisputeAlreadyResolved.
ResolveDispute(ctx context.Context, arg ResolveDisputeParams) (Dispute, error)
// COALESCE makes this idempotent: revoking an already-revoked key keeps its
// original revoked_at instead of bumping it, and still reports one row
// affected (not zero), so the admin service can tell "no such key" (0 rows)
// from "already revoked" (1 row, unchanged) without a separate lookup.
RevokeAPIKey(ctx context.Context, id uuid.UUID) (int64, error)
// The balance of an account and everything under it: gather the subtree via
// parent_id, then sum those accounts' postings. Same-currency subtree, so this
// is one number.
RolledUpBalance(ctx context.Context, arg RolledUpBalanceParams) (int64, error)
// The chainer's batch read: unprocessed rows whose inserting transaction is
// guaranteed settled (txid < the watermark passed in), oldest commit order
// first. Ordering by (txid, id) is the total order ADR-017 defines: it is
// stable because txid is not reused and id is a bigserial tiebreaker for the
// (rare) case of equal txid.
//
// Includes subject_type, subject_id, and hash_version (ADR-025, migration
// 0034) so the chainer can copy them onto the audit_log row it builds and
// hash with the row's own recorded version.
ScanUnprocessedAuditOutbox(ctx context.Context, arg ScanUnprocessedAuditOutboxParams) ([]ScanUnprocessedAuditOutboxRow, error)
// Reconciles an already-existing key's scopes to exactly the given set, keyed
// by hash rather than id (ADR-019 follow-up, review fix): InsertAPIKey is
// insert-or-ignore on the unique key_hash, so a demo key row left over from a
// previous boot never has its scopes touched by a plain re-insert. cmd/server
// calls this right after provisioning the demo key, keyed by
// domain.HashAPIKey of its known plaintext, so the row's scopes always match
// demoKeyScopes(cfg.demoMode): it gains admin scope when demo mode turns on,
// and correctly loses it when a deployment flips out of demo mode. A hash
// with no matching row affects zero rows, which is fine: the INSERT that
// runs immediately before this in provisioning order creates it first.
SetAPIKeyScopesByHash(ctx context.Context, arg SetAPIKeyScopesByHashParams) error
// Re-parent (or clear, when parent_id is NULL) one account. Cycle, currency,
// and same-tenant are enforced by accounts_hierarchy_guard / the composite FK.
SetAccountParent(ctx context.Context, arg SetAccountParentParams) (int64, error)
// Task 5.5, audit A1.5: freezes, closes, or reactivates one account. Scoped
// to tenant_id like every other write here, so a caller can never flip a
// status on another tenant's account by id alone.
SetAccountStatus(ctx context.Context, arg SetAccountStatusParams) (int64, error)
// Task 2.4b (audit A3.4): a whole-document replace of the settings jsonb
// column, used by admin.Service.SetTenantPolicy to write {"policy": {...}}.
SetTenantSettings(ctx context.Context, arg SetTenantSettingsParams) (int64, error)
SetTenantStatus(ctx context.Context, arg SetTenantStatusParams) (int64, error)
// Advances the singleton fan-out cursor. Always called in the same
// transaction as the webhook_deliveries inserts it corresponds to (Task
// 4.1), so a crash between the two is impossible: either both the inserts
// and the cursor advance land, or neither does, and a retried fan-out pass
// sees the same audit_log rows again (harmless: InsertWebhookDelivery's
// ON CONFLICT DO NOTHING against the (subscription_id, audit_chain_seq)
// unique index makes re-inserting the same pairing a no-op).
SetWebhookFanoutCursor(ctx context.Context, lastChainSeq int64) error
// Flips active for one subscription by id. The admin surface's
// DeleteSubscription calls this with active=false instead of a hard DELETE
// (see domain.Repository.SetWebhookSubscriptionActive's doc comment for
// why): a webhook_deliveries row's foreign key to its subscription has no
// cascade, so deactivating, not deleting, is what "stops future deliveries"
// without breaking or discarding delivery history.
SetWebhookSubscriptionActive(ctx context.Context, arg SetWebhookSubscriptionActiveParams) (int64, error)
// Irreversibly destroys tenant_id's CURRENT (highest-version) DEK (ADR-018):
// every posting description ever encrypted under that version becomes
// permanently unreadable, while postings.description and audit_log.after
// keep their exact stored ciphertext bytes untouched, so the tamper-evident
// hash chain (which hashes those bytes, never decrypts them) stays
// verifiable. Money data and every OTHER tenant's keys are completely
// unaffected. The tenant is NOT bricked: its next Encrypt call
// (internal/crypto.Cipher) sees the current version shredded and mints the
// next one, so posting, converting, and reversing all keep working.
//
// Serialized against MintCryptoKeyVersion by the same per-tenant
// pg_advisory_xact_lock (see that query's own comment): this can never
// target a version a concurrent first-use mint is still in the middle of
// creating, and a concurrent mint can never land a version this call has
// already decided to shred out from under it.
//
// A tenant with NO crypto_keys row at all yet (never encrypted anything)
// still gets a permanent version-1 shredded tombstone (GREATEST(...,1)
// below): without one, that tenant's very next Encrypt would happily mint a
// brand-new, LIVE version 1, silently undoing an operator's already-issued
// shred request. Idempotent: shredding an already-shredded current version
// leaves its shredded_at at its ORIGINAL value (COALESCE), matching
// RevokeAPIKey's own idempotent-revoke convention.
// Same sqlc.arg(tenant_id), repeated, plus the same explicit ::uuid cast at
// every use as MintCryptoKeyVersion, and for the same reason (see that
// query's own comment): the ::text cast inside hashtextextended would
// otherwise make the planner infer the whole parameter as text, breaking
// the uuid comparison/insert below.
ShredCurrentCryptoKey(ctx context.Context, tenantID uuid.UUID) error
// Deletes up to batch_size idempotency keys whose replay window has passed,
// across all tenants (Task 4.5, audit A1.4; batched per the follow-up
// review: a plain "DELETE ... WHERE expires_at < now()" has no bound, so a
// large backlog could delete an unbounded number of rows in one statement
// and contend with live posts). The ctid subquery caps each statement's own
// row scan/lock footprint to batch_size rows instead of the whole table.
// Not tenant-scoped and not run inside RunInTx: it is a plain maintenance
// statement. The Go caller (postgres.Repository.SweepExpiredIdempotencyKeys)
// loops this query in batches until a call deletes 0 rows (or a max
// iteration guard trips), summing the deleted count for the sweep log line.
SweepExpiredIdempotencyKeysBatch(ctx context.Context, batchSize int32) (int64, error)
// Task 4 (ADR-025): the TTL sweep, mirroring
// SweepExpiredIdempotencyKeysBatch's role for idempotency keys. Runs
// outside any tenant's RunInTx (a background goroutine with no tenant GUC
// set, so the RLS policy's allow-when-unset branch lets it see every
// tenant), on an interval, moving every still-pending row older than
// older_than_seconds to 'expired' and decided_by 'system'. RETURNING the
// full rows lets the caller emit one approval.expired lifecycle event per
// row expired, without a second read. older_than_seconds is a float8 of
// seconds, not a Postgres interval literal, the same
// server-clock-independent-duration convention InsertIdempotencyKey's
// ttl_seconds already uses.
SweepExpiredPending(ctx context.Context, olderThanSeconds float64) ([]PendingTransaction, error)
// Task 2.4b (audit A3.4): each currency's already-posted debit total for
// today (date_trunc('day', now()), the DATABASE SERVER's clock, so it lines
// up with the same clock that stamped created_at on every posting). Read
// from inside the caller's SERIALIZABLE RunInTx body, under the per-tenant
// in-process serialization (ADR-012), so a daily-volume policy check is race
// free: two concurrent posts for the same tenant can never both read this
// same total and both post believing they are under the cap. Only positive
// amounts (debits, ADR-002) are summed: a transaction's credits are the
// mirror image of its debits within each currency (the balance invariant),
// so counting only debits avoids double-counting the same movement.
TenantDailyDebits(ctx context.Context, tenantID uuid.UUID) ([]TenantDailyDebitsRow, error)
// The current default owned by exactly this tenant (no global fallback), so
// the console can show whether the tenant has its own override at all.
TenantFXMarkupDefault(ctx context.Context, tenantID pgtype.UUID) (FxMarkupDefault, error)
// Updates last_used_at for a single key by id. Called best-effort and
// throttled from the auth resolver (Task 2.2): not every request, so this is
// not a write on the hot path of every authenticated call.
TouchAPIKeyLastUsed(ctx context.Context, arg TouchAPIKeyLastUsedParams) error
// Task 6.3, audit A9.2: every account's derived balance, including system
// (FX clearing) accounts, which hold the FX position and are part of the
// balance proof. The LEFT JOIN means an account with no postings yet still
// returns one row, with balance COALESCEd to 0, the same shape
// AccountBalances (postings.sql) already uses.
// Bounded read (audit remediation): one more than ledger.MaxReportRows so the
// service can refuse an over-large trial balance rather than build it unbounded
// in memory. Keep in sync with ledger.MaxReportRows.
TrialBalanceAccounts(ctx context.Context, tenantID uuid.UUID) ([]TrialBalanceAccountsRow, error)
// Task 6.3, audit A9.2: the double-entry balance proof. Each currency's net
// posted total across every account in the tenant; in a correct ledger every
// total is zero (ADR-001).
TrialBalanceByCurrency(ctx context.Context, tenantID uuid.UUID) ([]TrialBalanceByCurrencyRow, error)
// Task 4 (ADR-025): the decision write. Always called after
// GetPendingForUpdate has already locked and validated the row within the
// same surrounding transaction, so this is a plain unconditional update, not
// a guarded one the way ResolveDispute's UPDATE ... WHERE status = 'open'
// is: the row lock is what prevents a second concurrent decision here, not
// a WHERE clause on this statement.
UpdatePendingStatus(ctx context.Context, arg UpdatePendingStatusParams) error
}
type Queries ¶
type Queries struct {
// contains filtered or unexported fields
}
func (*Queries) AccountBalance ¶
func (*Queries) AccountBalances ¶
func (q *Queries) AccountBalances(ctx context.Context, arg AccountBalancesParams) ([]AccountBalancesRow, error)
Task 5.5, audit A1.5: each named account's derived balance, read inside the caller's SERIALIZABLE RunInTx body so it is consistent with the CreateTransaction write that follows in the same transaction: two concurrent posts that would each individually keep an account above its floor, but together breach it, are a genuine read-write antidependency SERIALIZABLE detects and aborts one of. Called ONLY for accounts that AccountStatusFlags reported as having a MinBalance configured (see AccountStatusFlags's own doc comment for why this query is kept separate and only run when actually needed). The LEFT JOIN (rather than a subquery per account) means an account with no postings yet still returns one row, with balance COALESCEd to 0.
func (*Queries) AccountStatement ¶
func (q *Queries) AccountStatement(ctx context.Context, arg AccountStatementParams) ([]AccountStatementRow, error)
Postings affecting an account, newest first, each with the running balance as of that posting. The running balance is a window SUM over the account's full posting history (the CTE); the keyset filter and limit then return one page. after_created_at / after_id are the keyset position: pass a far-future timestamp and the max uuid for the first page.
func (*Queries) AccountStatementRange ¶
func (q *Queries) AccountStatementRange(ctx context.Context, arg AccountStatementRangeParams) ([]AccountStatementRangeRow, error)
The per-account period statement export (Task 6.3, audit A9.2): like AccountStatement above, the running balance is a window SUM over the account's FULL posting history (the CTE), so a filtered page still shows each entry's real running balance, not one reset to the filtered window. from_ts/to_ts are optional via sqlc.narg (NULL disables that bound, the same half-open [from, to) convention ListTransactions uses); the outer query applies the date filter and caps the result at row_limit, requested by the caller as one more than the export cap it wants so a truncated export can be detected without a second round trip.
func (*Queries) AccountStatusFlags ¶
func (q *Queries) AccountStatusFlags(ctx context.Context, arg AccountStatusFlagsParams) ([]AccountStatusFlagsRow, error)
Task 5.5, audit A1.5: each named account's current status, min_balance, and is_system flag ONLY (no balance), read inside the caller's SERIALIZABLE RunInTx body (see domain.Tx.AccountPostingStates). This is deliberately split from AccountBalances below: this query touches only the accounts table, which nothing in the posting path ever writes to (a post inserts into transactions/postings/audit_outbox/idempotency_keys, never accounts), so it can never be the read side of a SERIALIZABLE read-write antidependency against a concurrent post. That is exactly what a combined query joining postings would risk: reading every historical posting for a hot account inside the same transaction as a concurrent INSERT into that account's postings is precisely the kind of broad read-write overlap SERIALIZABLE flags, and doing it unconditionally on every single post (unlike the opt-in TenantDailyDebits check) reintroduced, under many-way single-tenant concurrency onto a handful of accounts, the same class of retry storm ADR-017 removed the audit chain read to get rid of (see TestPostConcurrentStressSingleTenant). AccountBalances is now only ever called for the subset of accounts that actually have a MinBalance set.
func (*Queries) AllAccountBalances ¶ added in v1.0.0
func (q *Queries) AllAccountBalances(ctx context.Context, tenantID uuid.UUID) ([]AllAccountBalancesRow, error)
Every account for a tenant with its own derived balance (LEFT JOIN so an account with no postings returns 0) and its parent_id, so the caller can build the tree and roll up in memory in one pass. Ordered by name, id for a stable base order the Go rollup then re-threads parent-before-child. Bounded read (audit remediation): one more than ledger.MaxReportRows, so the service can detect "too large for a single unpaged response" and refuse rather than stream an unbounded result set into memory. Keep in sync with ledger.MaxReportRows.
func (*Queries) AuditOutboxWatermark ¶
The oldest transaction id still in flight, cast the same way audit_outbox.txid is (xid8 has no direct cast to bigint). A row whose txid is strictly below this watermark is guaranteed committed and safe to chain (ADR-017, "Ordering: process only settled rows, in transaction-commit order").
func (*Queries) CountPendingOutbox ¶
Unprocessed outbox rows for one tenant: the lag the verify endpoint reports alongside the chained head (ADR-017 section 5), so a caller can see whether the chain is current or behind.
func (*Queries) CreateAccount ¶
func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) error
min_balance (Task 5.5, audit A1.5), party_reference, and party_type (Task 6.1, audit A9.1) are all nullable: sqlc.narg leaves each unset (NULL) when the caller passes no value, matching "no floor configured" / "no party linkage supplied", every account's behavior before these columns existed. status is NOT inserted explicitly: the column default ('active', migration 0022) applies, the same way CreateTenant leaves status to the column default.
func (*Queries) CreateDispute ¶
func (q *Queries) CreateDispute(ctx context.Context, arg CreateDisputeParams) error
Task 6.3, audit A9.2: status is NOT inserted explicitly (the column default 'open' applies), the same convention CreateAccount leaves status to its own column default for.
func (*Queries) CreatePosting ¶
func (q *Queries) CreatePosting(ctx context.Context, arg CreatePostingParams) error
func (*Queries) CreateTenant ¶
func (q *Queries) CreateTenant(ctx context.Context, arg CreateTenantParams) error
func (*Queries) CreateTransaction ¶
func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (time.Time, error)
currency lives on each posting now (ADR-014), not here: an FX transaction spans two currencies, so there is no single transaction-level value left to store. The fx_* columns are the immutable snapshot of the conversion actually applied; all nullable, since a single-currency transaction (still the common case) has none of this. reverses_transaction_id (Task 4.2, audit A1.2) is likewise nullable: only a reversal carries it. reference and effective_at (Task 4.3, audit A1.3) are both nullable client-supplied fields. created_at is RETURNED (not just inserted): the caller uses it to resolve effective_at's read-time fallback right after a fresh insert, without a second round trip (see Repository.txRepo.CreateTransaction).
func (*Queries) CurrentFXRate ¶
The latest quote for (base, quote) at or before now, preferring a tenant-specific row over the global default (Task 2.4, audit A3.3): tenant_id = $1 matches the tenant's own rows, and tenant_id IS NULL always matches the global default, so a tenant with no row of its own still resolves the global one. ORDER BY (tenant_id IS NULL) sorts a tenant-owned row (false, i.e. 0) ahead of a global row (true, i.e. 1), so when both exist the tenant's own wins regardless of which is more recently effective; effective_at DESC, id DESC is then the same "latest, ties broken by insertion order" tiebreak CurrentFXRate always used, applied within whichever tier (tenant-owned or global) won.
func (*Queries) GetAPIKeyByHash ¶
Joins tenants so the resolver gets the tenant's current status alongside the key in the same round trip (Task 2.1, ADR-015): gating needs no extra query. The join is safe against a dangling reference: api_keys_tenant_fk (migration 0011) guarantees every api_keys row's tenant_id has a tenants row. scopes, expires_at, and last_used_at (Task 2.2) are returned as-is: expiry and scope enforcement happen in the resolver and the transport middleware, not in this query.
func (*Queries) GetAPIKeyByID ¶
Raw fetch by id, unfiltered by revoked_at: the admin surface (Task 2.2b) uses this to look up an existing key (including an already-revoked one) by id, for example to copy a key's tenant/name/scopes when rotating it. It does not join tenants: callers that need the tenant's current status look it up separately via GetTenant.
func (*Queries) GetAccount ¶
func (q *Queries) GetAccount(ctx context.Context, arg GetAccountParams) (GetAccountRow, error)
func (*Queries) GetAuditHead ¶
The tenant's current head: chain_seq and row_hash of its latest audit_log row (Task 5.3). See GetLastAuditHash's own comment (ADR-017) for why chain_seq, not id or created_at, is the correct "latest" order; this is the same lookup, just also returning chain_seq, which GetLastAuditHash's only caller (the chainer) never needed since it only ever extends the chain from a row_hash. Used to surface the live head alongside the last off-box anchor (the verify-audit-chain endpoint, internal/api/audit.go) and by VerifyFromLatestAnchor to fall back to a full verify when no anchor exists yet. ErrNoRows means the tenant has no audit rows at all.
func (*Queries) GetCryptoKeyVersion ¶
func (q *Queries) GetCryptoKeyVersion(ctx context.Context, arg GetCryptoKeyVersionParams) (GetCryptoKeyVersionRow, error)
Read-only lookup of ONE SPECIFIC DEK version, for a decrypt whose stored ciphertext names the version it was sealed under (ADR-018). Never creates a row: a description already carrying internal/crypto.EncodingPrefix implies a key existed for that exact version at encrypt time, so pgx.ErrNoRows here is a genuine inconsistency for the caller to surface as an error, not the ordinary first-use case.
func (*Queries) GetCurrentCryptoKey ¶
func (q *Queries) GetCurrentCryptoKey(ctx context.Context, tenantID uuid.UUID) (GetCurrentCryptoKeyRow, error)
ADR-018 (Task 6.2 fix): tenant_id's CURRENT (highest-version) key row, whatever its shredded state. internal/crypto.Cipher.Encrypt uses this to decide whether to reuse it (found && not shredded) or mint a fresh, forward version (not found at all, or the current one is shredded): see MintCryptoKeyVersion below. pgx.ErrNoRows here means the tenant has never encrypted anything and never been shredded: a genuine first-use case, not an error, for the caller to handle.
func (*Queries) GetDispute ¶
func (*Queries) GetIdempotencyKey ¶
func (q *Queries) GetIdempotencyKey(ctx context.Context, arg GetIdempotencyKeyParams) (GetIdempotencyKeyRow, error)
An expired row is treated as absent (Task 4.5, audit A1.4): the "AND expires_at > now()" filter is what makes a key whose replay window has passed behave exactly like a key that was never written, from the caller's point of view, whether or not the background sweep has physically deleted the row yet.
func (*Queries) GetLastAuditHash ¶
The tenant's most recent row_hash, used to extend the per-tenant hash chain. Ordered by chain_seq, not id (ADR-017 IMPORTANT 2, migration 0016): id is a UUIDv7, monotonic only within the ONE process that minted it, so a leader failover to a different host with clock skew can mint an id LOWER than the current head, and ordering by id would then return the wrong "latest" row and corrupt the chain. chain_seq is a plain ascending sequence the single chainer process advances on every insert, in the same order it assigns row_hash values, so it stays correct across any failover regardless of clock skew. created_at is copied from the ORIGINATING event's post time (audit_outbox.occurred_at), which under concurrent posts across many transactions is NOT guaranteed to be monotonic with the order those transactions actually commit in (a transaction that starts later can commit first); ordering by created_at would occasionally return the wrong "latest" row too. A fresh tenant (or one with no rows yet) surfaces as pgx.ErrNoRows; the caller treats that as the chain's genesis (domain.AuditGenesisHash).
func (*Queries) GetLatestAuditAnchor ¶
func (q *Queries) GetLatestAuditAnchor(ctx context.Context, tenantID uuid.UUID) (GetLatestAuditAnchorRow, error)
The tenant's most recently recorded anchor (Task 5.3): the chain_seq, row_hash, and timestamp the anchor job last logged off-box for this tenant. ErrNoRows means no anchor has ever been recorded (a brand-new tenant, or one that posted before the anchor job's first tick).
func (*Queries) GetOrCreateClearingAccount ¶
func (q *Queries) GetOrCreateClearingAccount(ctx context.Context, arg GetOrCreateClearingAccountParams) (GetOrCreateClearingAccountRow, error)
The per-tenant per-currency FX clearing account (ADR-014, is_system=true), created lazily on first use. Keyed by (tenant_id, name): name is the reserved, deterministic "fx.clearing.<CURRENCY>" string the caller builds, so a second call for the same tenant and currency always resolves to the same row instead of creating a duplicate. The ON CONFLICT arbiter is the partial unique index accounts_system_name_uniq (migration 0010), which only covers is_system rows, so this can never collide with an ordinary user-named account.
On conflict this does a no-op DO UPDATE (id set to its own current value) rather than DO NOTHING. DO NOTHING never RETURNs the conflicting row, which an earlier version of this query worked around with a CTE plus a fallback SELECT unioned into the same statement, guarded by NOT EXISTS. That fallback ran against the single statement's original snapshot: when two callers raced to create the same tenant's first clearing account for a currency, the loser blocked on the conflict, and by the time it unblocked (after the winner committed) its own fallback SELECT could still miss the now-committed row, since the snapshot predated that commit. Both branches of the UNION then returned zero rows to the loser, surfacing as "no rows in result set" under concurrent Converts targeting the same pair. DO UPDATE instead forces Postgres's own EvalPlanQual re-fetch of the current row version as part of resolving the conflict, so RETURNING always yields exactly one row, new or existing, in a single round trip with no second snapshot to race against.
func (*Queries) GetPendingByIdempotencyKey ¶ added in v1.0.0
func (q *Queries) GetPendingByIdempotencyKey(ctx context.Context, arg GetPendingByIdempotencyKeyParams) (PendingTransaction, error)
ADR-025 section 6 (Lifecycle): the replay-dedup read. holdForApproval calls this before inserting a new pending, and again if the insert itself loses a race against a concurrent identical retry (pending_transactions_idempotency_idx unique violation), so a retry of the same gated create with the same idempotency key always returns the one pending that key produced, never a second one.
func (*Queries) GetPendingForUpdate ¶ added in v1.0.0
func (q *Queries) GetPendingForUpdate(ctx context.Context, arg GetPendingForUpdateParams) (PendingTransaction, error)
Task 4 (ADR-025): the row-locking read a decision (approve/reject/cancel) takes before transitioning a pending, so two racing decisions cannot both win; the loser's transaction blocks on this lock until the winner commits or rolls back, then re-reads the now-terminal row. Called only from inside RunInTx (via the Tx port), never as a standalone read.
func (*Queries) GetPendingTransaction ¶ added in v1.0.0
func (q *Queries) GetPendingTransaction(ctx context.Context, arg GetPendingTransactionParams) (PendingTransaction, error)
func (*Queries) GetReversalOf ¶
func (q *Queries) GetReversalOf(ctx context.Context, arg GetReversalOfParams) (Transaction, error)
The reversal of a given original, if one exists (Task 4.2, audit A1.2): transactions_one_reversal_idx (migration 0017) guarantees at most one row can ever match, so this is a plain :one lookup, not a list.
func (*Queries) GetTransaction ¶
func (q *Queries) GetTransaction(ctx context.Context, arg GetTransactionParams) (Transaction, error)
func (*Queries) GetWebhookDelivery ¶
Raw fetch by id: tests and any future delivery-inspection tooling read a single row's full lifecycle state back this way.
func (*Queries) GetWebhookFanoutCursorForUpdate ¶
Reads the singleton fan-out cursor and locks its row for the rest of the surrounding transaction (Task 4.1): the same "read the watermark, then act, all in one transaction" shape ADR-017's chainer uses for audit_log, so the cursor read and its eventual advance (SetWebhookFanoutCursor) never race a concurrent fan-out pass.
func (*Queries) GlobalFXMarkupDefault ¶ added in v1.0.0
func (q *Queries) GlobalFXMarkupDefault(ctx context.Context) (FxMarkupDefault, error)
The current global default only (tenant_id IS NULL), so the console can show it distinctly from a tenant override.
func (*Queries) InsertAPIKey ¶
func (q *Queries) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) error
scopes and expires_at (Task 2.2b) are now written on insert: the admin surface (internal/admin) is what actually sets them to something other than the column default. Every pre-2.2b caller (cmd/server's demo and load-test key provisioning, and every repository test that predates scopes) still works unchanged, because the Go-level repository method defaults an empty Scopes slice to {read,post} before it ever reaches this query, the same default the api_keys.scopes column itself carries.
func (*Queries) InsertAuditAnchor ¶
func (q *Queries) InsertAuditAnchor(ctx context.Context, arg InsertAuditAnchorParams) error
Records tenantID's current chain head as a new off-box-anchored checkpoint (Task 5.3, migration 0025). Called only by the periodic anchor job (internal/audit.AnchorJob), never the request path: the job runs with the RLS GUC unset (a cross-tenant worker, Task 5.4b), so this insert is not scoped through withTenant the way a request-path write would be. signature is the app-held HMAC over (tenant_id, chain_seq, row_hash), or NULL when AUDIT_ANCHOR_SIGNING_KEY is unset (signing disabled).
func (*Queries) InsertAuditLog ¶
func (q *Queries) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) error
outbox_id is the source audit_outbox row this audit_log row was chained from (ADR-017 MINOR 3, migration 0016): a UNIQUE constraint on it means a second attempt to chain the same outbox row fails this insert with a unique violation instead of silently forking the chain. chain_seq is left to its column DEFAULT (nextval), never supplied by the caller: it is what makes chain order immune to any host's clock (see GetLastAuditHash below).
transaction_id, subject_type, and subject_id are all nullable (ADR-025, migration 0034): a chained non-transaction lifecycle event carries subject_type/subject_id instead of a transaction_id. hash_version is copied straight from the source outbox row, so recomputing this row's hash later (Verify) always uses the same preimage it was chained with.
func (*Queries) InsertAuditOutbox ¶
func (q *Queries) InsertAuditOutbox(ctx context.Context, arg InsertAuditOutboxParams) error
Writes one outbox row inside the caller's own transaction (ADR-017): the event is durable if and only if the surrounding post/convert transaction commits. occurred_at, txid, and created_at are all left to their column defaults (the database server's now() and pg_current_xact_id(), see migration 0015): no chain read, no hash computed, here.
transaction_id, subject_type, and subject_id are all nullable (ADR-025, migration 0034): a non-transaction lifecycle event (for example approval.rejected) carries subject_type/subject_id instead of a transaction_id. hash_version records which row-hash preimage the caller computed against, so the chainer and Verify recompute with the same one.
func (*Queries) InsertFXMarkupDefault ¶ added in v1.0.0
func (q *Queries) InsertFXMarkupDefault(ctx context.Context, arg InsertFXMarkupDefaultParams) (FxMarkupDefault, error)
fx_markup_defaults is append-only (ADR-020): a new default is a new row. tenant_id NULL is the global default, a non-NULL tenant_id is that tenant's override. effective_at is server-stamped via COALESCE(narg, now()) for the same clock-skew reason InsertFXRate stamps server-side. default_spread_bps is a nullable narg: a NULL value inserts a cleared row (the tenant follows the global default again), matching the nullable fx_rates.spread_bps pattern.
func (*Queries) InsertFXRate ¶
fx_rates is append-only (ADR-014): a new quote is a new row, never an update, so every rate ever applied to a transaction stays reconstructible. tenant_id NULL makes this the global default rate for the pair; a non-NULL tenant_id makes it that tenant's own rate (Task 2.4, audit A3.3), resolved ahead of the global default by CurrentFXRate below.
effective_at is a nullable named param (sqlc.narg): when the caller omits an explicit effective_at, COALESCE falls through to the DATABASE SERVER's now(), never the calling process's clock. Stamping an "immediate" rate with the caller's clock is what caused a real clock-skew bug (Task 2.4 remediation): CurrentFXRate below gates on "effective_at <= now()" using the server's clock too, so if the caller's clock ran even slightly ahead, a just-inserted row was transiently invisible and the query silently fell through to the global default. Passing NULL here (rather than the CLI host's time.Now()) makes the two clocks the same clock for the common, unscheduled case; an explicit future effective_at (a scheduled rate) is unaffected, since the caller still supplies it directly.
func (*Queries) InsertIdempotencyKey ¶
func (q *Queries) InsertIdempotencyKey(ctx context.Context, arg InsertIdempotencyKeyParams) (uuid.UUID, error)
Inserts a fresh idempotency key with a server-computed expires_at (now() + ttl_seconds, never the calling process's clock: Task 4.5, audit A1.4, consistent with fx_rates.effective_at's own server-side stamping).
ON CONFLICT upserts rather than plain-inserts because a row for (tenant_id, idempotency_key) can already be present and EXPIRED: the lookup path (GetIdempotencyKey) treats an expired row as absent, so the caller proceeds to post a brand-new transaction and lands here again with the same key. Without the upsert that would hit the primary key and be misread as a genuine duplicate (replaying a stale, no-longer-relevant transaction). The "WHERE idempotency_keys.expires_at <= now()" guard on the DO UPDATE means the replace only happens when the existing row has, in fact, expired; a conflict against a still-live row leaves it untouched and the update affects zero rows, so RETURNING yields no row and the caller (postgres.txRepo.InsertIdempotencyKey) maps pgx.ErrNoRows to domain.ErrDuplicateIdempotencyKey exactly as a plain unique-violation used to, preserving replay-on-duplicate for the still-live case.
func (*Queries) InsertPendingTransaction ¶ added in v1.0.0
func (q *Queries) InsertPendingTransaction(ctx context.Context, arg InsertPendingTransactionParams) error
Task 4 (ADR-025): status is NOT inserted explicitly (the column default 'pending' applies), the same convention CreateDispute leaves status to its own column default for. idempotency_key (migration 0036, ADR-025 section 6) is nullable via sqlc.narg: a held create with no caller-supplied key inserts NULL, which never collides under pending_transactions_idempotency_idx (a partial unique index that only applies where the key is present).
func (*Queries) InsertWebhookDelivery ¶
func (q *Queries) InsertWebhookDelivery(ctx context.Context, arg InsertWebhookDeliveryParams) (int64, error)
Creates one fan-out row for a (subscription, audit event) pairing. The ON CONFLICT DO NOTHING against the UNIQUE (subscription_id, audit_chain_seq) index (migration 0021) is what makes fan-out exactly-once into this table even if the fan-out step ever ran twice over the same audit_log range: a repeat attempt is a silent no-op, not a duplicate delivery row, so execrows reports 0 for a pairing that already existed and 1 for a genuinely new one.
func (*Queries) InsertWebhookSubscription ¶
func (q *Queries) InsertWebhookSubscription(ctx context.Context, arg InsertWebhookSubscriptionParams) error
Inserts a new webhook_subscriptions row, always active (a caller creates a subscription to receive events; deactivating happens later, via SetWebhookSubscriptionActive). secret is stored as-is, never hashed (Task 4.1, audit A7.1): the delivery worker must read it back in full to sign every outbound payload.
func (*Queries) LatestEnvGlobalFXRate ¶ added in v1.0.0
func (q *Queries) LatestEnvGlobalFXRate(ctx context.Context, arg LatestEnvGlobalFXRateParams) (LatestEnvGlobalFXRateRow, error)
The latest env-seeded global row for a pair, used by fx.Seed to decide whether FX_RATES itself changed. Comparing against this (not the current winner) means an admin-API-written row is never clobbered by a re-seed: Seed only re-asserts an env rate when the FX_RATES entry differs from the last thing Seed itself wrote for that pair.
func (*Queries) ListAPIKeysByTenant ¶
func (q *Queries) ListAPIKeysByTenant(ctx context.Context, tenantID uuid.UUID) ([]ListAPIKeysByTenantRow, error)
Every key for a tenant, oldest first, including revoked ones: the admin surface's list view (Task 2.2b) is meant to show a tenant's full key history, not just its live keys. Never selects key_hash: plaintext is shown once at issue/rotate time and is never recoverable afterward, and the hash itself has no business leaving the resolver's lookup path.
func (*Queries) ListAccounts ¶
func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]ListAccountsRow, error)
func (*Queries) ListActiveWebhookSubscriptionsByTenant ¶
func (q *Queries) ListActiveWebhookSubscriptionsByTenant(ctx context.Context, tenantID uuid.UUID) ([]ListActiveWebhookSubscriptionsByTenantRow, error)
The fan-out step's per-tenant read (Task 4.1): every ACTIVE subscription for one tenant, including the secret and event_types the fan-out needs to decide whether and how to create a webhook_deliveries row for it. Ordered by id only for a stable, deterministic iteration order within one fan-out pass; it carries no meaning beyond that.
func (*Queries) ListAudit ¶ added in v1.0.0
func (q *Queries) ListAudit(ctx context.Context, arg ListAuditParams) ([]ListAuditRow, error)
Keyset page of the tenant's entire audit log, newest first, paged by id alone (see ListAuditByAccount for why id, assigned in chain-insertion order, drives ordering rather than created_at). after_id is the keyset position: pass the max uuid for the first page, then the last id of the prior page.
func (*Queries) ListAuditByAccount ¶
func (q *Queries) ListAuditByAccount(ctx context.Context, arg ListAuditByAccountParams) ([]ListAuditByAccountRow, error)
Keyset page of audit rows for every transaction with a posting touching the account, newest first. after_id is the keyset position: pass the max uuid for the first page. Ordered and paged by id alone, not (created_at, id): see GetLastAuditHash's comment (ADR-017) for why id, assigned by the chainer in true chain-insertion order, is what must drive ordering here, not created_at (copied from the original event's post time, which is not guaranteed monotonic with commit order under concurrent posts).
func (*Queries) ListAuditByTransaction ¶
func (q *Queries) ListAuditByTransaction(ctx context.Context, arg ListAuditByTransactionParams) ([]ListAuditByTransactionRow, error)
Ordered by id, not created_at: see GetLastAuditHash's comment (ADR-017) for why id is the chain-consistent order and created_at is not.
func (*Queries) ListAuditForVerify ¶
func (q *Queries) ListAuditForVerify(ctx context.Context, tenantID uuid.UUID) ([]ListAuditForVerifyRow, error)
Every audit row for the tenant, in true chain order: the full walk used to recompute and check the tamper-evident hash chain end to end. Ordered by chain_seq, not id or created_at: see GetLastAuditHash's comment (ADR-017, migration 0016) for why chain_seq, not id, is the failover-safe chain order.
Includes subject_type, subject_id, and hash_version (ADR-025, migration 0034) so recomputing a row's hash uses the same preimage it was chained with.
func (*Queries) ListAuditForVerifyPage ¶
func (q *Queries) ListAuditForVerifyPage(ctx context.Context, arg ListAuditForVerifyPageParams) ([]ListAuditForVerifyPageRow, error)
A bounded page of the tenant's audit rows in true chain order, strictly after after_chain_seq (Task 5.3, audit A2.4): the streaming counterpart to ListAuditForVerify above, which loads the entire chain into memory at once. AuditService.Verify calls this in a loop, advancing after_chain_seq to the last row's chain_seq on each page, until a page comes back with fewer than page_limit rows, so memory use is bounded by page_limit regardless of how long the chain has grown. Includes chain_seq (unlike ListAuditForVerify) precisely so the caller can advance the cursor without a second round trip. after_chain_seq is 0 to start from genesis, or an anchor's chain_seq to start from a trusted checkpoint (VerifyFromLatestAnchor).
Also includes subject_type, subject_id, and hash_version (ADR-025, migration 0034): this is the query AuditService.Verify actually pages through, so it must return everything ComputeAuditRowHash needs to recompute each row (both v1 and v2) exactly as it was chained.
func (*Queries) ListAuditHeads ¶
func (q *Queries) ListAuditHeads(ctx context.Context) ([]ListAuditHeadsRow, error)
One row per tenant with at least one audit_log entry: that tenant's current chain head (chain_seq, row_hash). The anchor job (Task 5.3) reads this once per tick rather than enumerating tenants and calling GetAuditHead once per tenant (an N+1 round trip): it inserts one audit_anchors row and emits one structured log line per tenant returned here.
func (*Queries) ListAuditLogSinceChainSeq ¶
func (q *Queries) ListAuditLogSinceChainSeq(ctx context.Context, arg ListAuditLogSinceChainSeqParams) ([]ListAuditLogSinceChainSeqRow, error)
The fan-out step's source read: every chained audit_log event past the cursor, oldest first, up to batch_limit. Reads chain_seq (ADR-017's failover-safe, skew-proof linearization key), not id or created_at, for the same reason GetLastAuditHash does: it is the one order the chainer itself guarantees is monotonic across any failover.
transaction_id is nullable (ADR-025, migration 0034): a chained non-transaction lifecycle event (for example approval.rejected) has none. The fan-out worker maps a null transaction_id to an empty, omitted field in the webhook payload, the same convention the rest of the audit read path uses. subject_type/subject_id (also ADR-025) are what that kind of event carries instead: the fan-out worker copies them onto the payload (Task 10) so a consumer can tell which subject the event concerns.
func (*Queries) ListCurrentFXRates ¶ added in v1.0.0
func (q *Queries) ListCurrentFXRates(ctx context.Context, tenantID pgtype.UUID) ([]ListCurrentFXRatesRow, error)
The current effective row per (base, quote) for a tenant plus the global defaults: DISTINCT ON collapses each pair to one row, and the ORDER BY puts a tenant-owned row (tenant_id IS NULL = false, sorts first) ahead of a global row, then latest effective, matching CurrentFXRate's precedence. Pass tenant_id NULL to list globals only (tenant_id = NULL is never true, so only the tenant_id IS NULL rows match).
func (*Queries) ListDisputes ¶
Filtered, keyset-paged list of a tenant's disputes, newest first (Task 6.3, audit A9.2). status is an optional filter via sqlc.narg: NULL disables it (every status is returned). Keyset paged by (created_at, id) descending, the identical cursor shape ListTransactions and AccountStatement already use.
func (*Queries) ListDueWebhookDeliveries ¶
func (q *Queries) ListDueWebhookDeliveries(ctx context.Context, batchLimit int32) ([]ListDueWebhookDeliveriesRow, error)
The delivery worker's batch read: pending/failed rows whose backoff has elapsed, oldest due first, joined to their subscription for the url and secret needed to sign and send. "AND ws.active" means a subscription that was deactivated after fan-out already created pending rows for it simply stops being picked up here (see SetWebhookSubscriptionActive's doc comment): its existing rows are left exactly as they are, neither delivered nor purged, just never attempted again. FOR UPDATE OF wd SKIP LOCKED is defense in depth against two workers ever running at once (the leader-election lock is what actually prevents that in normal operation, ADR-017's discipline mirrored for this worker): a second reader skips a row the first is mid-processing rather than picking it up concurrently. This query runs as its own standalone statement (never inside a surrounding transaction that also makes the outbound HTTP call), so the row lock is released the moment this statement completes; correctness does not depend on holding it any longer than that.
func (*Queries) ListPendingTransactions ¶ added in v1.0.0
func (q *Queries) ListPendingTransactions(ctx context.Context, arg ListPendingTransactionsParams) ([]PendingTransaction, error)
Filtered, keyset-paged list of a tenant's pendings, newest first (Task 4, ADR-025). status is an optional filter via sqlc.narg: NULL disables it (every status is returned). Keyset paged by (created_at, id) descending, the identical cursor shape ListDisputes already uses.
func (*Queries) ListPostingsByTransaction ¶
func (q *Queries) ListPostingsByTransaction(ctx context.Context, arg ListPostingsByTransactionParams) ([]ListPostingsByTransactionRow, error)
func (*Queries) ListPostingsByTransactionIDs ¶
func (q *Queries) ListPostingsByTransactionIDs(ctx context.Context, arg ListPostingsByTransactionIDsParams) ([]ListPostingsByTransactionIDsRow, error)
Batch posting fetch for a page of transactions (Task 4.4, audit A7.2): ListTransactions returns up to a page's worth of transaction rows, and assembling each one's postings via ListPostingsByTransaction one at a time would be N+1 queries for a full page. This fetches every posting for every transaction id in the page in a single round trip; the caller groups rows back by transaction_id (see Repository.ListTransactions). Ordered by transaction_id then (created_at, id), matching ListPostingsByTransaction's own per-transaction order, so grouping by transaction_id yields each transaction's postings already in that transaction's insertion order.
func (*Queries) ListTenants ¶
func (*Queries) ListTransactions ¶
func (q *Queries) ListTransactions(ctx context.Context, arg ListTransactionsParams) ([]Transaction, error)
Filtered, keyset-paged list of a tenant's transactions, newest first (Task 4.4, audit A7.2). from_ts/to_ts/reference are optional filters via sqlc.narg: NULL disables that filter's clause (it becomes a no-op OR), so this single query serves every filter combination rather than one built dynamically per request. from_ts is inclusive (created_at >=), to_ts is exclusive (created_at <): a half-open [from, to) window.
effective_from/effective_to are the same half-open window, but over the value date (Task 4.3's effective_at) rather than created_at (follow-up F2): COALESCE(effective_at, created_at) is the same read-time fallback used everywhere else effective_at is read (see assembleTransaction), so a transaction posted with no explicit value date is filtered as if its value date were its post time, never silently excluded from an effective_at window that would otherwise match its created_at. Independent of from_ts/to_ts: a caller may filter on created_at, effective_at, both, or neither.
Keyset paged by (created_at, id) descending, the identical cursor shape AccountStatement already uses: after_created_at/after_id are the keyset position, a far-future timestamp and the max uuid for the first page. page_limit is requested by the caller as one more than the page size it actually wants, so a next page can be detected without a second round trip; this query itself just returns up to page_limit rows in whatever amount it is asked for.
func (*Queries) ListWebhookDeliveriesBySubscription ¶
func (q *Queries) ListWebhookDeliveriesBySubscription(ctx context.Context, subscriptionID uuid.UUID) ([]WebhookDelivery, error)
Every delivery row for one subscription, in fan-out order (the same chain_seq order the source audit_log events were read in). Used by tests to assert fan-out and delivery outcomes end to end.
func (*Queries) ListWebhookSubscriptionsByTenant ¶
func (q *Queries) ListWebhookSubscriptionsByTenant(ctx context.Context, tenantID uuid.UUID) ([]ListWebhookSubscriptionsByTenantRow, error)
Every subscription for a tenant, oldest first, active or not: the admin surface's list view. Never selects secret: it is shown once, at creation time, and never recoverable through a list call.
func (*Queries) MarkAuditOutboxProcessed ¶
Sets processed_at for one outbox row. The chainer calls this in the same transaction as the audit_log insert it produced, so a crash between the two is impossible: either both happen or neither does.
func (*Queries) MarkWebhookDeliveryDelivered ¶
func (q *Queries) MarkWebhookDeliveryDelivered(ctx context.Context, arg MarkWebhookDeliveryDeliveredParams) (int64, error)
A 2xx response: terminal success. attempts is set to the total number of tries this delivery took (including the successful one), the same total MarkWebhookDeliveryFailed accumulates on the way here, so attempts always means "how many times this delivery was actually tried", not just "how many times it failed". "AND status IN ('pending', 'failed')" guards against a state regression: in a two-leader window (SKIP LOCKED is defense in depth, not a guarantee, see ListDueWebhookDeliveries) two workers can pick up the same due row, and without this guard a slower worker's write could still land after a faster worker already reached a terminal outcome for it. Once a row is 'delivered' or 'dead', neither mark query can move it again: this returns 0 affected rows instead, which the caller (internal/webhook) treats as a no-op, not an error.
func (*Queries) MarkWebhookDeliveryFailed ¶
func (q *Queries) MarkWebhookDeliveryFailed(ctx context.Context, arg MarkWebhookDeliveryFailedParams) (int64, error)
A non-2xx response or transport error: the caller (internal/webhook) computes the new attempts count, the resulting status ('failed' to retry again later, or 'dead' once attempts reaches the configured max), the next backoff deadline, and the error text to record, all in Go (backoff and the max-attempts cap are application config, not schema), and this query just persists that decision. "AND status IN ('pending', 'failed')" is the same terminal-state guard MarkWebhookDeliveryDelivered carries (see its doc comment): a delivery already 'delivered' (or already 'dead') can never be regressed by a late failure write from a second worker that raced this one.
func (*Queries) MintCryptoKeyVersion ¶
func (q *Queries) MintCryptoKeyVersion(ctx context.Context, arg MintCryptoKeyVersionParams) (MintCryptoKeyVersionRow, error)
Atomically creates tenant_id's crypto_keys row at the given version, wrapping the CALLER-GENERATED candidate_wrapped_dek (ADR-018: only internal/crypto.Cipher ever holds the master key needed to produce one). pg_advisory_xact_lock serializes this against ShredCurrentCryptoKey for the SAME tenant (both take the identical hashtextextended(tenant_id, 0) key), so a version this call is in the middle of minting can never be the exact version a concurrent shred call means to destroy, or vice versa; the lock is released automatically at the end of this call's transaction (withTenant's COMMIT/ROLLBACK).
The ON CONFLICT DO UPDATE is the same "force RETURNING to yield the winning row" trick migration 0027's original GetOrCreateCryptoKey used at the single-version grain (see also GetOrCreateClearingAccount): if two callers race to mint the exact same (tenant_id, version) pair, for example two concurrent first-use Encrypt calls for a brand-new tenant both targeting version 1, one wins and the other's own candidate wrapped_dek is silently discarded; both RETURNING the SAME winning row in one round trip, so both callers end up using the identical DEK. sqlc.arg(tenant_id), repeated, names the SAME parameter (one field in the generated Params struct) at every use; the redundant ::uuid cast at each use (not just the ::text one hashtextextended needs) is deliberate, not decorative: Postgres assigns an untyped placeholder's type by unifying every one of its uses in the statement, and the ::text cast below would otherwise make the planner infer the WHOLE parameter as text, including the INSERT's tenant_id (uuid) column, which then fails to bind ("column tenant_id is of type uuid but expression is of type text"). Casting to uuid explicitly at each use pins its type regardless of statement order.
func (*Queries) PendingApprovedForTransaction ¶ added in v1.0.0
func (q *Queries) PendingApprovedForTransaction(ctx context.Context, arg PendingApprovedForTransactionParams) (bool, error)
Task 6 (ADR-025): the reverse-of-approved exemption's read. True only when some pending transaction's decision produced transaction_id AND that decision was an approval (a rejected or cancelled pending never sets transaction_id at all, per the transaction_id IS NULL OR status = 'approved' check in migration 0035, so the status filter here is mostly belt-and-suspenders). TransactionService.ReverseTransaction calls this before gating a reversal, never from inside RunInTx.
func (*Queries) ResolveDispute ¶
Task 6.3, audit A9.2: the guarded transition out of 'open'. The WHERE status = 'open' clause is what makes resolving an already-resolved (or concurrently-being-resolved) dispute return zero rows rather than silently overwriting a prior resolution: the caller (postgres.Repository.ResolveDispute) treats a zero-row result as domain.ErrDisputeAlreadyResolved.
func (*Queries) RevokeAPIKey ¶
COALESCE makes this idempotent: revoking an already-revoked key keeps its original revoked_at instead of bumping it, and still reports one row affected (not zero), so the admin service can tell "no such key" (0 rows) from "already revoked" (1 row, unchanged) without a separate lookup.
func (*Queries) RolledUpBalance ¶ added in v1.0.0
The balance of an account and everything under it: gather the subtree via parent_id, then sum those accounts' postings. Same-currency subtree, so this is one number.
func (*Queries) ScanUnprocessedAuditOutbox ¶
func (q *Queries) ScanUnprocessedAuditOutbox(ctx context.Context, arg ScanUnprocessedAuditOutboxParams) ([]ScanUnprocessedAuditOutboxRow, error)
The chainer's batch read: unprocessed rows whose inserting transaction is guaranteed settled (txid < the watermark passed in), oldest commit order first. Ordering by (txid, id) is the total order ADR-017 defines: it is stable because txid is not reused and id is a bigserial tiebreaker for the (rare) case of equal txid.
Includes subject_type, subject_id, and hash_version (ADR-025, migration 0034) so the chainer can copy them onto the audit_log row it builds and hash with the row's own recorded version.
func (*Queries) SetAPIKeyScopesByHash ¶
func (q *Queries) SetAPIKeyScopesByHash(ctx context.Context, arg SetAPIKeyScopesByHashParams) error
Reconciles an already-existing key's scopes to exactly the given set, keyed by hash rather than id (ADR-019 follow-up, review fix): InsertAPIKey is insert-or-ignore on the unique key_hash, so a demo key row left over from a previous boot never has its scopes touched by a plain re-insert. cmd/server calls this right after provisioning the demo key, keyed by domain.HashAPIKey of its known plaintext, so the row's scopes always match demoKeyScopes(cfg.demoMode): it gains admin scope when demo mode turns on, and correctly loses it when a deployment flips out of demo mode. A hash with no matching row affects zero rows, which is fine: the INSERT that runs immediately before this in provisioning order creates it first.
func (*Queries) SetAccountParent ¶ added in v1.0.0
Re-parent (or clear, when parent_id is NULL) one account. Cycle, currency, and same-tenant are enforced by accounts_hierarchy_guard / the composite FK.
func (*Queries) SetAccountStatus ¶
Task 5.5, audit A1.5: freezes, closes, or reactivates one account. Scoped to tenant_id like every other write here, so a caller can never flip a status on another tenant's account by id alone.
func (*Queries) SetTenantSettings ¶
func (q *Queries) SetTenantSettings(ctx context.Context, arg SetTenantSettingsParams) (int64, error)
Task 2.4b (audit A3.4): a whole-document replace of the settings jsonb column, used by admin.Service.SetTenantPolicy to write {"policy": {...}}.
func (*Queries) SetTenantStatus ¶
func (*Queries) SetWebhookFanoutCursor ¶
Advances the singleton fan-out cursor. Always called in the same transaction as the webhook_deliveries inserts it corresponds to (Task 4.1), so a crash between the two is impossible: either both the inserts and the cursor advance land, or neither does, and a retried fan-out pass sees the same audit_log rows again (harmless: InsertWebhookDelivery's ON CONFLICT DO NOTHING against the (subscription_id, audit_chain_seq) unique index makes re-inserting the same pairing a no-op).
func (*Queries) SetWebhookSubscriptionActive ¶
func (q *Queries) SetWebhookSubscriptionActive(ctx context.Context, arg SetWebhookSubscriptionActiveParams) (int64, error)
Flips active for one subscription by id. The admin surface's DeleteSubscription calls this with active=false instead of a hard DELETE (see domain.Repository.SetWebhookSubscriptionActive's doc comment for why): a webhook_deliveries row's foreign key to its subscription has no cascade, so deactivating, not deleting, is what "stops future deliveries" without breaking or discarding delivery history.
func (*Queries) ShredCurrentCryptoKey ¶
Irreversibly destroys tenant_id's CURRENT (highest-version) DEK (ADR-018): every posting description ever encrypted under that version becomes permanently unreadable, while postings.description and audit_log.after keep their exact stored ciphertext bytes untouched, so the tamper-evident hash chain (which hashes those bytes, never decrypts them) stays verifiable. Money data and every OTHER tenant's keys are completely unaffected. The tenant is NOT bricked: its next Encrypt call (internal/crypto.Cipher) sees the current version shredded and mints the next one, so posting, converting, and reversing all keep working.
Serialized against MintCryptoKeyVersion by the same per-tenant pg_advisory_xact_lock (see that query's own comment): this can never target a version a concurrent first-use mint is still in the middle of creating, and a concurrent mint can never land a version this call has already decided to shred out from under it.
A tenant with NO crypto_keys row at all yet (never encrypted anything) still gets a permanent version-1 shredded tombstone (GREATEST(...,1) below): without one, that tenant's very next Encrypt would happily mint a brand-new, LIVE version 1, silently undoing an operator's already-issued shred request. Idempotent: shredding an already-shredded current version leaves its shredded_at at its ORIGINAL value (COALESCE), matching RevokeAPIKey's own idempotent-revoke convention. Same sqlc.arg(tenant_id), repeated, plus the same explicit ::uuid cast at every use as MintCryptoKeyVersion, and for the same reason (see that query's own comment): the ::text cast inside hashtextextended would otherwise make the planner infer the whole parameter as text, breaking the uuid comparison/insert below.
func (*Queries) SweepExpiredIdempotencyKeysBatch ¶
func (q *Queries) SweepExpiredIdempotencyKeysBatch(ctx context.Context, batchSize int32) (int64, error)
Deletes up to batch_size idempotency keys whose replay window has passed, across all tenants (Task 4.5, audit A1.4; batched per the follow-up review: a plain "DELETE ... WHERE expires_at < now()" has no bound, so a large backlog could delete an unbounded number of rows in one statement and contend with live posts). The ctid subquery caps each statement's own row scan/lock footprint to batch_size rows instead of the whole table. Not tenant-scoped and not run inside RunInTx: it is a plain maintenance statement. The Go caller (postgres.Repository.SweepExpiredIdempotencyKeys) loops this query in batches until a call deletes 0 rows (or a max iteration guard trips), summing the deleted count for the sweep log line.
func (*Queries) SweepExpiredPending ¶ added in v1.0.0
func (q *Queries) SweepExpiredPending(ctx context.Context, olderThanSeconds float64) ([]PendingTransaction, error)
Task 4 (ADR-025): the TTL sweep, mirroring SweepExpiredIdempotencyKeysBatch's role for idempotency keys. Runs outside any tenant's RunInTx (a background goroutine with no tenant GUC set, so the RLS policy's allow-when-unset branch lets it see every tenant), on an interval, moving every still-pending row older than older_than_seconds to 'expired' and decided_by 'system'. RETURNING the full rows lets the caller emit one approval.expired lifecycle event per row expired, without a second read. older_than_seconds is a float8 of seconds, not a Postgres interval literal, the same server-clock-independent-duration convention InsertIdempotencyKey's ttl_seconds already uses.
func (*Queries) TenantDailyDebits ¶
func (q *Queries) TenantDailyDebits(ctx context.Context, tenantID uuid.UUID) ([]TenantDailyDebitsRow, error)
Task 2.4b (audit A3.4): each currency's already-posted debit total for today (date_trunc('day', now()), the DATABASE SERVER's clock, so it lines up with the same clock that stamped created_at on every posting). Read from inside the caller's SERIALIZABLE RunInTx body, under the per-tenant in-process serialization (ADR-012), so a daily-volume policy check is race free: two concurrent posts for the same tenant can never both read this same total and both post believing they are under the cap. Only positive amounts (debits, ADR-002) are summed: a transaction's credits are the mirror image of its debits within each currency (the balance invariant), so counting only debits avoids double-counting the same movement.
func (*Queries) TenantFXMarkupDefault ¶ added in v1.0.0
func (q *Queries) TenantFXMarkupDefault(ctx context.Context, tenantID pgtype.UUID) (FxMarkupDefault, error)
The current default owned by exactly this tenant (no global fallback), so the console can show whether the tenant has its own override at all.
func (*Queries) TouchAPIKeyLastUsed ¶
func (q *Queries) TouchAPIKeyLastUsed(ctx context.Context, arg TouchAPIKeyLastUsedParams) error
Updates last_used_at for a single key by id. Called best-effort and throttled from the auth resolver (Task 2.2): not every request, so this is not a write on the hot path of every authenticated call.
func (*Queries) TrialBalanceAccounts ¶
func (q *Queries) TrialBalanceAccounts(ctx context.Context, tenantID uuid.UUID) ([]TrialBalanceAccountsRow, error)
Task 6.3, audit A9.2: every account's derived balance, including system (FX clearing) accounts, which hold the FX position and are part of the balance proof. The LEFT JOIN means an account with no postings yet still returns one row, with balance COALESCEd to 0, the same shape AccountBalances (postings.sql) already uses. Bounded read (audit remediation): one more than ledger.MaxReportRows so the service can refuse an over-large trial balance rather than build it unbounded in memory. Keep in sync with ledger.MaxReportRows.
func (*Queries) TrialBalanceByCurrency ¶
func (q *Queries) TrialBalanceByCurrency(ctx context.Context, tenantID uuid.UUID) ([]TrialBalanceByCurrencyRow, error)
Task 6.3, audit A9.2: the double-entry balance proof. Each currency's net posted total across every account in the tenant; in a correct ledger every total is zero (ADR-001).
func (*Queries) UpdatePendingStatus ¶ added in v1.0.0
func (q *Queries) UpdatePendingStatus(ctx context.Context, arg UpdatePendingStatusParams) error
Task 4 (ADR-025): the decision write. Always called after GetPendingForUpdate has already locked and validated the row within the same surrounding transaction, so this is a plain unconditional update, not a guarded one the way ResolveDispute's UPDATE ... WHERE status = 'open' is: the row lock is what prevents a second concurrent decision here, not a WHERE clause on this statement.
type ResolveDisputeParams ¶
type RolledUpBalanceParams ¶ added in v1.0.0
type SetAccountParentParams ¶ added in v1.0.0
type SetAccountStatusParams ¶
type SetTenantSettingsParams ¶
type SetTenantStatusParams ¶
type TenantDailyDebitsRow ¶
type TouchAPIKeyLastUsedParams ¶
type TouchAPIKeyLastUsedParams struct {
ID uuid.UUID
LastUsedAt pgtype.Timestamptz
}
type Transaction ¶
type Transaction struct {
ID uuid.UUID
TenantID uuid.UUID
CreatedAt time.Time
FxSourceAmount pgtype.Int8
FxConvertedAmount pgtype.Int8
FxMidRateE8 pgtype.Int8
FxSpreadBps pgtype.Int4
FxAppliedE8 pgtype.Int8
FxRateSource pgtype.Text
FxEffectiveAt pgtype.Timestamptz
FxRateID pgtype.Int8
ReversesTransactionID pgtype.UUID
Reference pgtype.Text
EffectiveAt pgtype.Timestamptz
}