Files
Assistant/adapters/postgres/continuity_repository.go
m 18fbc7f105
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 15s
DEV-000065 fix notification binding origin
2026-08-17 16:33:05 +03:00

387 lines
22 KiB
Go

package postgres
import (
"context"
"errors"
"fmt"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
continuitydomain "git.cin.su/m/Assistant/modules/continuity/domain"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ErrProspectiveNotFound = errors.New("prospective item not found")
ErrProspectiveOwnerConflict = errors.New("prospective item owner conflict")
ErrProspectiveStateConflict = errors.New("prospective item state conflict")
ErrProspectiveRevisionConflict = errors.New("prospective item revision conflict")
ErrActionReceiptNotFound = errors.New("continuity action receipt not found")
ErrActionIntentConflict = errors.New("action intent was already used for a different command")
ErrAttentionNotFound = errors.New("attention item not found")
ErrAttentionStateConflict = errors.New("attention item state conflict")
ErrAttentionRevisionConflict = errors.New("attention item revision conflict")
)
type ContinuityConfig struct{ Host, Port, Database, User, Password string }
type ContinuityRepository struct{ pool *pgxpool.Pool }
func OpenContinuity(ctx context.Context, config ContinuityConfig) (*ContinuityRepository, error) {
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password)
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
return nil, err
}
return &ContinuityRepository{pool: pool}, nil
}
func (r *ContinuityRepository) Close() { r.pool.Close() }
func (r *ContinuityRepository) Save(ctx context.Context, item continuity.ProspectiveItem) error {
_, err := r.pool.Exec(ctx, `INSERT INTO continuity.prospective_items (prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT (prospective_item_id) DO UPDATE SET state=EXCLUDED.state, revision=EXCLUDED.revision`, item.ID, item.CommitmentID, item.OwnerPrincipalID, item.ScopeID, item.GoalID, item.SourceIntentID, item.SourceUtteranceID, item.Summary, item.TriggerAtUnixMillis, item.State, item.Revision)
return err
}
func (r *ContinuityRepository) Load(ctx context.Context, id continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) {
var item continuity.ProspectiveItem
err := r.pool.QueryRow(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE prospective_item_id=$1`, id).Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary, &item.TriggerAtUnixMillis, &item.State, &item.Revision)
if err != nil {
return continuity.ProspectiveItem{}, fmt.Errorf("load prospective item: %w", err)
}
return item, nil
}
func (r *ContinuityRepository) ByOwner(ctx context.Context, owner identity.PrincipalID, limit int) ([]continuity.ProspectiveItem, error) {
rows, err := r.pool.Query(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision FROM continuity.prospective_items WHERE owner_principal_id=$1 ORDER BY trigger_at_unix_millis, prospective_item_id LIMIT $2`, owner, limit)
if err != nil {
return nil, fmt.Errorf("list prospective items by owner: %w", err)
}
defer rows.Close()
items := make([]continuity.ProspectiveItem, 0)
for rows.Next() {
var item continuity.ProspectiveItem
if err := rows.Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary, &item.TriggerAtUnixMillis, &item.State, &item.Revision); err != nil {
return nil, fmt.Errorf("scan prospective item: %w", err)
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("list prospective items by owner: %w", err)
}
return items, nil
}
func (r *ContinuityRepository) DueArmed(ctx context.Context, nowUnixMillis int64, limit int) ([]continuity.ProspectiveItem, error) {
if limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid due reminder limit")
}
rows, err := r.pool.Query(ctx, `SELECT prospective_item_id, commitment_id, owner_principal_id, scope_id, goal_id, source_intent_id, source_utterance_id, summary, trigger_at_unix_millis, state, revision
FROM continuity.prospective_items
WHERE state=$1 AND trigger_at_unix_millis <= $2
ORDER BY trigger_at_unix_millis, prospective_item_id LIMIT $3`, continuity.ProspectiveArmed, nowUnixMillis, limit)
if err != nil {
return nil, fmt.Errorf("list due prospective items: %w", err)
}
defer rows.Close()
items := make([]continuity.ProspectiveItem, 0)
for rows.Next() {
var item continuity.ProspectiveItem
if err := rows.Scan(&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary, &item.TriggerAtUnixMillis, &item.State, &item.Revision); err != nil {
return nil, fmt.Errorf("scan due prospective item: %w", err)
}
items = append(items, item)
}
return items, rows.Err()
}
func (r *ContinuityRepository) ClaimDueReminder(ctx context.Context, id continuity.ProspectiveItemID, revision continuity.ProspectiveRevision, attentionID continuity.AttentionItemID, nowUnixMillis int64) (continuity.ProspectiveItem, continuity.AttentionItem, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
var triggered continuity.ProspectiveItem
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
SET state=$1, revision=revision+1
WHERE prospective_item_id=$2 AND state=$3 AND revision=$4 AND trigger_at_unix_millis <= $5
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
continuity.ProspectiveTriggered, id, continuity.ProspectiveArmed, revision, nowUnixMillis).Scan(
&triggered.ID, &triggered.CommitmentID, &triggered.OwnerPrincipalID, &triggered.ScopeID, &triggered.GoalID,
&triggered.SourceIntentID, &triggered.SourceUtteranceID, &triggered.Summary, &triggered.TriggerAtUnixMillis, &triggered.State, &triggered.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, ErrProspectiveRevisionConflict
}
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("trigger due reminder: %w", err)
}
var revalidating continuity.ProspectiveItem
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
SET state=$1, revision=revision+1
WHERE prospective_item_id=$2 AND state=$3 AND revision=$4
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
continuity.ProspectiveRevalidating, triggered.ID, continuity.ProspectiveTriggered, triggered.Revision).Scan(
&revalidating.ID, &revalidating.CommitmentID, &revalidating.OwnerPrincipalID, &revalidating.ScopeID, &revalidating.GoalID,
&revalidating.SourceIntentID, &revalidating.SourceUtteranceID, &revalidating.Summary, &revalidating.TriggerAtUnixMillis, &revalidating.State, &revalidating.Revision)
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("begin due reminder revalidation: %w", err)
}
var delivered continuity.ProspectiveItem
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
SET state=$1, revision=revision+1
WHERE prospective_item_id=$2 AND state=$3 AND revision=$4
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
continuity.ProspectiveDelivered, revalidating.ID, continuity.ProspectiveRevalidating, revalidating.Revision).Scan(
&delivered.ID, &delivered.CommitmentID, &delivered.OwnerPrincipalID, &delivered.ScopeID, &delivered.GoalID,
&delivered.SourceIntentID, &delivered.SourceUtteranceID, &delivered.Summary, &delivered.TriggerAtUnixMillis, &delivered.State, &delivered.Revision)
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("finish due reminder revalidation: %w", err)
}
attention, err := continuitydomain.BuildDueReminderAttention(delivered, attentionID, nowUnixMillis)
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err
}
err = tx.QueryRow(ctx, `INSERT INTO continuity.attention_items (
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,
action_attempt_id,delivery_outcome,revision)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,0,'','','','',1)
ON CONFLICT (source_kind, source_prospective_item_id) DO UPDATE SET source_prospective_item_id=EXCLUDED.source_prospective_item_id
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
attention.ID, attention.OwnerPrincipalID, attention.ScopeID, attention.SourceKind, attention.SourceProspectiveID,
attention.Level, attention.State, attention.Summary, attention.DueAtUnixMillis, attention.CreatedAtUnixMillis).Scan(
&attention.ID, &attention.OwnerPrincipalID, &attention.ScopeID, &attention.SourceKind, &attention.SourceProspectiveID,
&attention.Level, &attention.State, &attention.Summary, &attention.DueAtUnixMillis, &attention.CreatedAtUnixMillis,
&attention.DeliveredAtUnixMillis, &attention.ActionIntentID, &attention.AuthorizedActionID, &attention.ActionAttemptID,
&attention.DeliveryOutcome, &attention.Revision)
if err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, fmt.Errorf("create due reminder attention: %w", err)
}
if err = tx.Commit(ctx); err != nil {
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err
}
return delivered, attention, nil
}
func (r *ContinuityRepository) PendingAttention(ctx context.Context, limit int) ([]continuity.AttentionItem, error) {
if limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid attention limit")
}
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision
FROM continuity.attention_items WHERE state=$1 ORDER BY due_at_unix_millis, attention_item_id LIMIT $2`, continuity.AttentionPending, limit)
if err != nil {
return nil, fmt.Errorf("list pending attention: %w", err)
}
defer rows.Close()
return scanAttentionRows(rows)
}
func (r *ContinuityRepository) AttentionByOwner(ctx context.Context, owner identity.PrincipalID, limit int) ([]continuity.AttentionItem, error) {
if owner == "" || limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid attention owner query")
}
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision
FROM continuity.attention_items WHERE owner_principal_id=$1 ORDER BY due_at_unix_millis DESC, attention_item_id DESC LIMIT $2`, owner, limit)
if err != nil {
return nil, fmt.Errorf("list attention by owner: %w", err)
}
defer rows.Close()
return scanAttentionRows(rows)
}
func (r *ContinuityRepository) MarkAttentionDelivering(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision) (continuity.AttentionItem, error) {
var item continuity.AttentionItem
err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items SET state=$1, revision=revision+1
WHERE attention_item_id=$2 AND state=$3 AND revision=$4
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
continuity.AttentionDelivering, id, continuity.AttentionPending, revision).Scan(
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
}
return item, err
}
func (r *ContinuityRepository) MarkAttentionDelivered(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string, deliveredAtUnixMillis int64) (continuity.AttentionItem, error) {
var item continuity.AttentionItem
err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items
SET state=$1, revision=revision+1, delivered_at_unix_millis=$2, action_intent_id=$3, authorized_action_id=$4,
action_attempt_id=$5, delivery_outcome=$6
WHERE attention_item_id=$7 AND state=$8 AND revision=$9
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
continuity.AttentionDelivered, deliveredAtUnixMillis, intentID, authorizedID, attemptID, outcome,
id, continuity.AttentionDelivering, revision).Scan(
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
}
return item, err
}
func (r *ContinuityRepository) MarkAttentionFailed(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string) (continuity.AttentionItem, error) {
var item continuity.AttentionItem
err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items
SET state=$1, revision=revision+1, action_intent_id=$2, authorized_action_id=$3,
action_attempt_id=$4, delivery_outcome=$5
WHERE attention_item_id=$6 AND state=$7 AND revision=$8
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,revision`,
continuity.AttentionFailed, intentID, authorizedID, attemptID, outcome,
id, continuity.AttentionDelivering, revision).Scan(
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.AttentionItem{}, ErrAttentionRevisionConflict
}
return item, err
}
// SatisfyDelivered owns both the Prospective CAS and its idempotency receipt in
// one Continuity-local transaction. It never reaches Commit storage.
func (r *ContinuityRepository) SatisfyDelivered(ctx context.Context, command continuity.SatisfyDeliveredCommand) (continuity.ActionReceipt, error) {
if err := continuitydomain.ValidateSatisfyDelivered(command); err != nil {
return continuity.ActionReceipt{}, err
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return continuity.ActionReceipt{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
// Serialize deliveries of one ActionIntent inside the canonical owner. The
// receipt remains the durable correctness source; the lock is transaction-local.
if _, err = tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1,0))`, command.ActionIntentID); err != nil {
return continuity.ActionReceipt{}, err
}
if existing, loadErr := loadActionReceipt(ctx, tx, command.ActionIntentID); loadErr == nil {
if receiptMatchesCommand(existing, command) {
return existing, tx.Commit(ctx)
}
return continuity.ActionReceipt{}, ErrActionIntentConflict
} else if !errors.Is(loadErr, ErrActionReceiptNotFound) {
return continuity.ActionReceipt{}, loadErr
}
var item continuity.ProspectiveItem
err = tx.QueryRow(ctx, `UPDATE continuity.prospective_items
SET state=$1, revision=revision+1
WHERE prospective_item_id=$2 AND owner_principal_id=$3 AND state=$4 AND revision=$5
RETURNING prospective_item_id,commitment_id,owner_principal_id,scope_id,goal_id,source_intent_id,source_utterance_id,summary,trigger_at_unix_millis,state,revision`,
continuity.ProspectiveSatisfied, command.Subject.ProspectiveItemID, command.OwnerPrincipalID,
continuity.ProspectiveDelivered, command.Subject.ExpectedRevision).Scan(
&item.ID, &item.CommitmentID, &item.OwnerPrincipalID, &item.ScopeID, &item.GoalID, &item.SourceIntentID, &item.SourceUtteranceID, &item.Summary,
&item.TriggerAtUnixMillis, &item.State, &item.Revision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.ActionReceipt{}, diagnoseProspectiveConflict(ctx, tx, command)
}
if err != nil {
return continuity.ActionReceipt{}, fmt.Errorf("satisfy delivered prospective item: %w", err)
}
receipt := continuity.ActionReceipt{
ActionIntentID: command.ActionIntentID, AuthorizedActionID: command.AuthorizedActionID,
OwnerPrincipalID: command.OwnerPrincipalID, Subject: command.Subject,
ResultState: item.State, ResultRevision: item.Revision,
}
_, err = tx.Exec(ctx, `INSERT INTO continuity.action_receipts (
action_intent_id,authorized_action_id,owner_principal_id,prospective_item_id,expected_revision,
from_state,to_state,evidence_ref,result_state,result_revision)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, receipt.ActionIntentID, receipt.AuthorizedActionID,
receipt.OwnerPrincipalID, receipt.Subject.ProspectiveItemID, receipt.Subject.ExpectedRevision,
receipt.Subject.FromState, receipt.Subject.ToState, receipt.Subject.EvidenceRef,
receipt.ResultState, receipt.ResultRevision)
if err != nil {
return continuity.ActionReceipt{}, fmt.Errorf("save continuity action receipt: %w", err)
}
if err = tx.Commit(ctx); err != nil {
return continuity.ActionReceipt{}, err
}
return receipt, nil
}
func (r *ContinuityRepository) LoadActionReceipt(ctx context.Context, intentID continuity.CommittedActionIntentID) (continuity.ActionReceipt, error) {
return loadActionReceipt(ctx, r.pool, intentID)
}
type continuityRowQuerier interface {
QueryRow(context.Context, string, ...any) pgx.Row
}
type continuityRows interface {
Next() bool
Scan(...any) error
Err() error
}
func scanAttentionRows(rows continuityRows) ([]continuity.AttentionItem, error) {
items := make([]continuity.AttentionItem, 0)
for rows.Next() {
var item continuity.AttentionItem
if err := rows.Scan(&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID,
&item.Level, &item.State, &item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis,
&item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID, &item.ActionAttemptID,
&item.DeliveryOutcome, &item.Revision); err != nil {
return nil, fmt.Errorf("scan attention item: %w", err)
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("scan attention items: %w", err)
}
return items, nil
}
func loadActionReceipt(ctx context.Context, querier continuityRowQuerier, intentID continuity.CommittedActionIntentID) (continuity.ActionReceipt, error) {
var receipt continuity.ActionReceipt
err := querier.QueryRow(ctx, `SELECT action_intent_id,authorized_action_id,owner_principal_id,prospective_item_id,
expected_revision,from_state,to_state,evidence_ref,result_state,result_revision
FROM continuity.action_receipts WHERE action_intent_id=$1`, intentID).Scan(
&receipt.ActionIntentID, &receipt.AuthorizedActionID, &receipt.OwnerPrincipalID,
&receipt.Subject.ProspectiveItemID, &receipt.Subject.ExpectedRevision, &receipt.Subject.FromState,
&receipt.Subject.ToState, &receipt.Subject.EvidenceRef, &receipt.ResultState, &receipt.ResultRevision)
if errors.Is(err, pgx.ErrNoRows) {
return continuity.ActionReceipt{}, ErrActionReceiptNotFound
}
if err != nil {
return continuity.ActionReceipt{}, fmt.Errorf("load continuity action receipt: %w", err)
}
return receipt, nil
}
func receiptMatchesCommand(receipt continuity.ActionReceipt, command continuity.SatisfyDeliveredCommand) bool {
return receipt.ActionIntentID == command.ActionIntentID && receipt.AuthorizedActionID == command.AuthorizedActionID &&
receipt.OwnerPrincipalID == command.OwnerPrincipalID && receipt.Subject == command.Subject
}
func diagnoseProspectiveConflict(ctx context.Context, querier continuityRowQuerier, command continuity.SatisfyDeliveredCommand) error {
var owner identity.PrincipalID
var state continuity.ProspectiveState
var revision continuity.ProspectiveRevision
err := querier.QueryRow(ctx, `SELECT owner_principal_id,state,revision FROM continuity.prospective_items WHERE prospective_item_id=$1`, command.Subject.ProspectiveItemID).Scan(&owner, &state, &revision)
if errors.Is(err, pgx.ErrNoRows) {
return ErrProspectiveNotFound
}
if err != nil {
return err
}
if owner != command.OwnerPrincipalID {
return ErrProspectiveOwnerConflict
}
if state != continuity.ProspectiveDelivered {
return ErrProspectiveStateConflict
}
if revision != command.Subject.ExpectedRevision {
return ErrProspectiveRevisionConflict
}
return ErrProspectiveRevisionConflict
}