diff --git a/adapters/postgres/continuity_repository.go b/adapters/postgres/continuity_repository.go index b77ad46..edb6da2 100644 --- a/adapters/postgres/continuity_repository.go +++ b/adapters/postgres/continuity_repository.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "git.cin.su/m/Assistant/contracts/authority" "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" continuitydomain "git.cin.su/m/Assistant/modules/continuity/domain" @@ -217,17 +218,16 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide return nil, fmt.Errorf("invalid response candidate query") } rows, err := r.pool.Query(ctx, `SELECT - a.attention_item_id,a.owner_principal_id,a.scope_id,a.source_kind,a.source_prospective_item_id,a.level,a.state,a.summary, - a.due_at_unix_millis,a.created_at_unix_millis,a.delivered_at_unix_millis,a.action_intent_id,a.authorized_action_id,a.action_attempt_id,a.delivery_outcome, - a.response_id,a.response_utterance_id,a.response_operation,a.response_outcome,a.responded_at_unix_millis,a.revision, + COALESCE(a.attention_item_id,''),COALESCE(a.owner_principal_id,''),COALESCE(a.scope_id,''),COALESCE(a.source_kind,''),COALESCE(a.source_prospective_item_id,''),COALESCE(a.level,''),COALESCE(a.state,''),COALESCE(a.summary,''), + COALESCE(a.due_at_unix_millis,0),COALESCE(a.created_at_unix_millis,0),COALESCE(a.delivered_at_unix_millis,0),COALESCE(a.action_intent_id,''),COALESCE(a.authorized_action_id,''),COALESCE(a.action_attempt_id,''),COALESCE(a.delivery_outcome,''), + COALESCE(a.response_id,''),COALESCE(a.response_utterance_id,''),COALESCE(a.response_operation,''),COALESCE(a.response_outcome,''),COALESCE(a.responded_at_unix_millis,0),COALESCE(a.revision,0), p.prospective_item_id,p.commitment_id,p.owner_principal_id,p.scope_id,p.goal_id,p.source_intent_id,p.source_utterance_id,p.summary,p.trigger_at_unix_millis,p.state,p.revision - FROM continuity.attention_items a - JOIN continuity.prospective_items p ON p.prospective_item_id=a.source_prospective_item_id - WHERE a.owner_principal_id=$1 - AND a.source_kind=$2 - AND a.state IN ($3,$4,$5) + FROM continuity.prospective_items p + LEFT JOIN continuity.attention_items a ON a.source_kind=$2 AND a.source_prospective_item_id=p.prospective_item_id AND a.owner_principal_id=p.owner_principal_id + WHERE p.owner_principal_id=$1 + AND (a.attention_item_id IS NULL OR a.state IN ($3,$4,$5)) AND p.state IN ($6,$7) - ORDER BY a.delivered_at_unix_millis DESC, a.due_at_unix_millis DESC, a.attention_item_id DESC + ORDER BY COALESCE(a.delivered_at_unix_millis,0) DESC, p.trigger_at_unix_millis DESC, p.prospective_item_id DESC LIMIT $8`, owner, continuity.AttentionSourceProspectiveReminder, continuity.AttentionDelivered, continuity.AttentionPending, continuity.AttentionFailed, continuity.ProspectiveDelivered, continuity.ProspectiveArmed, limit) @@ -238,20 +238,45 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide var candidates []continuitydomain.ResponseCandidate for rows.Next() { var candidate continuitydomain.ResponseCandidate + var attentionID, attentionOwner, attentionScope, sourceKind, sourceProspective, level, attentionState, attentionSummary string + var actionIntent, authorizedAction, actionAttempt, deliveryOutcome, responseID, responseUtterance, responseOperation, responseOutcome string + var dueAt, createdAt, deliveredAt, respondedAt int64 + var attentionRevision uint64 if err := rows.Scan( - &candidate.Attention.ID, &candidate.Attention.OwnerPrincipalID, &candidate.Attention.ScopeID, &candidate.Attention.SourceKind, - &candidate.Attention.SourceProspectiveID, &candidate.Attention.Level, &candidate.Attention.State, &candidate.Attention.Summary, - &candidate.Attention.DueAtUnixMillis, &candidate.Attention.CreatedAtUnixMillis, &candidate.Attention.DeliveredAtUnixMillis, - &candidate.Attention.ActionIntentID, &candidate.Attention.AuthorizedActionID, &candidate.Attention.ActionAttemptID, - &candidate.Attention.DeliveryOutcome, &candidate.Attention.ResponseID, &candidate.Attention.ResponseUtteranceID, - &candidate.Attention.ResponseOperation, &candidate.Attention.ResponseOutcome, &candidate.Attention.RespondedAtUnixMillis, - &candidate.Attention.Revision, + &attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &level, &attentionState, &attentionSummary, + &dueAt, &createdAt, &deliveredAt, &actionIntent, &authorizedAction, &actionAttempt, &deliveryOutcome, + &responseID, &responseUtterance, &responseOperation, &responseOutcome, &respondedAt, &attentionRevision, &candidate.Prospective.ID, &candidate.Prospective.CommitmentID, &candidate.Prospective.OwnerPrincipalID, &candidate.Prospective.ScopeID, &candidate.Prospective.GoalID, &candidate.Prospective.SourceIntentID, &candidate.Prospective.SourceUtteranceID, &candidate.Prospective.Summary, &candidate.Prospective.TriggerAtUnixMillis, &candidate.Prospective.State, &candidate.Prospective.Revision); err != nil { return nil, fmt.Errorf("scan human response candidate: %w", err) } + if attentionID != "" { + candidate.Attention = continuity.AttentionItem{ + ID: continuity.AttentionItemID(attentionID), + OwnerPrincipalID: identity.PrincipalID(attentionOwner), + ScopeID: authority.ScopeID(attentionScope), + SourceKind: continuity.AttentionSourceKind(sourceKind), + SourceProspectiveID: continuity.ProspectiveItemID(sourceProspective), + Level: continuity.AttentionLevel(level), + State: continuity.AttentionState(attentionState), + Summary: attentionSummary, + DueAtUnixMillis: dueAt, + CreatedAtUnixMillis: createdAt, + DeliveredAtUnixMillis: deliveredAt, + ActionIntentID: continuity.CommittedActionIntentID(actionIntent), + AuthorizedActionID: continuity.CommittedAuthorizedActionID(authorizedAction), + ActionAttemptID: continuity.CommittedActionAttemptID(actionAttempt), + DeliveryOutcome: deliveryOutcome, + ResponseID: continuity.HumanResponseID(responseID), + ResponseUtteranceID: responseUtterance, + ResponseOperation: continuity.HumanResponseOperation(responseOperation), + ResponseOutcome: continuity.ResponseOutcome(responseOutcome), + RespondedAtUnixMillis: respondedAt, + Revision: continuity.AttentionRevision(attentionRevision), + } + } candidates = append(candidates, candidate) } if err := rows.Err(); err != nil { @@ -292,6 +317,9 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response if tag.RowsAffected() != 1 { return ErrProspectiveStateConflict } + if response.TargetAttentionID == "" { + return ErrAttentionNotFound + } if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil { return err } @@ -306,8 +334,10 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response if tag.RowsAffected() != 1 { return ErrProspectiveStateConflict } - if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil { - return err + if response.TargetAttentionID != "" { + if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil { + return err + } } case continuity.ResponseCancel: tag, err := tx.Exec(ctx, `UPDATE continuity.prospective_items SET state=$1, revision=revision+1 @@ -320,10 +350,15 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response if tag.RowsAffected() != 1 { return ErrProspectiveStateConflict } - if err = markAttentionResponded(ctx, tx, response, continuity.AttentionSuppressed); err != nil { - return err + if response.TargetAttentionID != "" { + if err = markAttentionResponded(ctx, tx, response, continuity.AttentionSuppressed); err != nil { + return err + } } case continuity.ResponseAcknowledge, continuity.ResponseDismiss: + if response.TargetAttentionID == "" { + return ErrAttentionNotFound + } if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil { return err } diff --git a/modules/continuity/domain/human_response.go b/modules/continuity/domain/human_response.go index 3660042..4d564ec 100644 --- a/modules/continuity/domain/human_response.go +++ b/modules/continuity/domain/human_response.go @@ -63,9 +63,14 @@ func InterpretHumanResponse(request contract.HumanResponseRequest, owner identit return contract.HumanResponseResult{Response: response, Handled: true, Reply: response.Clarification}, nil } candidate := candidates[0] - response.TargetKind = contract.ResponseTargetAttentionItem - response.TargetAttentionID = candidate.Attention.ID - response.TargetProspectiveID = candidate.Attention.SourceProspectiveID + if candidate.Attention.ID != "" { + response.TargetKind = contract.ResponseTargetAttentionItem + response.TargetAttentionID = candidate.Attention.ID + response.TargetProspectiveID = candidate.Attention.SourceProspectiveID + } else { + response.TargetKind = contract.ResponseTargetProspectiveItem + response.TargetProspectiveID = candidate.Prospective.ID + } response.ResolutionState = contract.ResponseResolved switch operation { case contract.ResponseComplete: diff --git a/modules/continuity/domain/human_response_test.go b/modules/continuity/domain/human_response_test.go index fb820ad..cd57a59 100644 --- a/modules/continuity/domain/human_response_test.go +++ b/modules/continuity/domain/human_response_test.go @@ -34,6 +34,15 @@ func TestInterpretHumanResponseCancelsSingleReminder(t *testing.T) { } } +func TestInterpretHumanResponseCancelsArmedProspectiveBeforeAttention(t *testing.T) { + result := interpretResponseForTest(t, "Отмени встретить курьера", []ResponseCandidate{{ + Prospective: continuity.ProspectiveItem{ID: "p1", OwnerPrincipalID: "principal-1", ScopeID: "scope-1", Summary: "встретить курьера", State: continuity.ProspectiveArmed, Revision: 1}, + }}) + if !result.Handled || result.Response.TargetKind != continuity.ResponseTargetProspectiveItem || result.Response.TargetProspectiveID != "p1" || result.Response.Outcome != continuity.ResponseOutcomeCancelled { + t.Fatalf("unexpected prospective cancel response: %#v", result) + } +} + func TestInterpretHumanResponseAmbiguousTargetAsksClarificationWithoutResolvedMutation(t *testing.T) { result := interpretResponseForTest(t, "Готово", []ResponseCandidate{responseCandidate("a1", "p1", "первое"), responseCandidate("a2", "p2", "второе")}) if !result.Handled || result.Response.ResolutionState != continuity.ResponseAmbiguous || result.Response.Outcome != continuity.ResponseOutcomeClarificationNeeded || !strings.Contains(result.Reply, "несколько") {