-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstate_validator.go
More file actions
144 lines (127 loc) · 4.29 KB
/
state_validator.go
File metadata and controls
144 lines (127 loc) · 4.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Package paywall implements state transition validation for escrow payments
package paywall
import (
"fmt"
"time"
)
// StateTransitionHistory records a state change in the payment lifecycle
type StateTransitionHistory struct {
// FromState is the previous escrow state
FromState EscrowState `json:"from_state"`
// ToState is the new escrow state
ToState EscrowState `json:"to_state"`
// Timestamp is when the transition occurred
Timestamp time.Time `json:"timestamp"`
// Actor identifies who initiated the transition
Actor string `json:"actor"`
// Reason provides context for the transition
Reason string `json:"reason,omitempty"`
}
// EscrowStateValidator validates state transitions for escrow payments
type EscrowStateValidator struct {
// validTransitions maps each state to its allowed next states
validTransitions map[EscrowState][]EscrowState
}
// NewEscrowStateValidator creates a validator with standard transition rules
func NewEscrowStateValidator() *EscrowStateValidator {
return &EscrowStateValidator{
validTransitions: map[EscrowState][]EscrowState{
// Pending can transition to Funded or back to None (cancelled)
EscrowPending: {EscrowFunded, EscrowNone},
// Funded can transition to Completed, Disputed, or Refunded
EscrowFunded: {EscrowCompleted, EscrowDisputed, EscrowRefunded},
// Disputed can transition to Completed or Refunded (via arbiter decision)
EscrowDisputed: {EscrowCompleted, EscrowRefunded},
// Terminal states cannot transition
EscrowCompleted: {},
EscrowRefunded: {},
EscrowNone: {EscrowPending}, // Can start new escrow
},
}
}
// ValidateTransition checks if a state transition is allowed
// Parameters:
// - from: Current escrow state
// - to: Desired escrow state
//
// Returns:
// - error: nil if valid, error describing why transition is invalid
func (v *EscrowStateValidator) ValidateTransition(from, to EscrowState) error {
// Same state is always valid (no-op)
if from == to {
return nil
}
// Get allowed transitions for current state
allowed, exists := v.validTransitions[from]
if !exists {
return fmt.Errorf("invalid source state: %s", from.String())
}
// Check if target state is allowed
for _, validState := range allowed {
if validState == to {
return nil
}
}
return fmt.Errorf("invalid transition: %s -> %s (allowed: %v)",
from.String(), to.String(), v.stateSliceToStrings(allowed))
}
// IsTerminalState checks if a state is terminal (cannot transition further)
func (v *EscrowStateValidator) IsTerminalState(state EscrowState) bool {
allowed, exists := v.validTransitions[state]
return exists && len(allowed) == 0
}
// GetAllowedTransitions returns all valid next states for the current state
func (v *EscrowStateValidator) GetAllowedTransitions(from EscrowState) []EscrowState {
allowed, exists := v.validTransitions[from]
if !exists {
return []EscrowState{}
}
// Return a copy to prevent modification
result := make([]EscrowState, len(allowed))
copy(result, allowed)
return result
}
// stateSliceToStrings converts state slice to string slice for error messages
func (v *EscrowStateValidator) stateSliceToStrings(states []EscrowState) []string {
strs := make([]string, len(states))
for i, s := range states {
strs[i] = s.String()
}
return strs
}
// ValidateAndRecordTransition validates a transition and creates a history entry
// Parameters:
// - payment: Payment to update
// - toState: Desired new state
// - actor: Who is making the transition
// - reason: Why the transition is happening
//
// Returns:
// - error: nil if valid and recorded, error if invalid
func (v *EscrowStateValidator) ValidateAndRecordTransition(
payment *Payment,
toState EscrowState,
actor string,
reason string,
) error {
// Validate transition
if err := v.ValidateTransition(payment.EscrowState, toState); err != nil {
return err
}
// Create history entry
historyEntry := StateTransitionHistory{
FromState: payment.EscrowState,
ToState: toState,
Timestamp: time.Now(),
Actor: actor,
Reason: reason,
}
// Append to payment history
if payment.StateTransitionHistory == nil {
payment.StateTransitionHistory = []StateTransitionHistory{}
}
payment.StateTransitionHistory = append(payment.StateTransitionHistory, historyEntry)
// Update payment state
payment.EscrowState = toState
return nil
}