flamingo.me/flamingo-commerce/v3@v3.11.0/checkout/domain/placeorder/process/process.go (about) 1 package process 2 3 import ( 4 "context" 5 "encoding/gob" 6 "errors" 7 "fmt" 8 "net/url" 9 10 "github.com/google/uuid" 11 12 "go.opencensus.io/stats" 13 "go.opencensus.io/stats/view" 14 "go.opencensus.io/tag" 15 16 "flamingo.me/flamingo/v3/framework/flamingo" 17 "flamingo.me/flamingo/v3/framework/opencensus" 18 19 "flamingo.me/flamingo-commerce/v3/cart/domain/cart" 20 "flamingo.me/flamingo-commerce/v3/cart/domain/validation" 21 "flamingo.me/flamingo-commerce/v3/checkout/application" 22 ) 23 24 type ( 25 // Provider for Processes 26 Provider func() *Process 27 28 // Process representing a place order process and has a current context with infos about result and current state 29 Process struct { 30 context Context 31 allStates map[string]State 32 failedState State 33 logger flamingo.Logger 34 area string 35 } 36 37 // Factory use to get Process instance 38 Factory struct { 39 provider Provider 40 startState State 41 failedState State 42 } 43 44 // RollbackReference a reference that can be used to trigger a rollback 45 RollbackReference struct { 46 StateName string 47 Data RollbackData 48 } 49 50 // RollbackData needed for rollback of a state 51 RollbackData interface{} 52 53 // FailedReason gives a human readable reason for a state failure 54 FailedReason interface { 55 Reason() string 56 } 57 58 // ErrorOccurredReason is used for unspecified errors 59 ErrorOccurredReason struct { 60 Error string 61 } 62 63 // CanceledByCustomerReason is used when customer cancels order 64 CanceledByCustomerReason struct{} 65 66 // PaymentErrorOccurredReason is used for errors during payment 67 PaymentErrorOccurredReason struct { 68 Error string 69 } 70 71 // PaymentCanceledByCustomerReason is used to signal that payment was canceled by customer 72 PaymentCanceledByCustomerReason struct{} 73 74 // CartValidationErrorReason contains the ValidationResult 75 CartValidationErrorReason struct { 76 ValidationResult validation.Result 77 } 78 ) 79 80 var ( 81 // processedState counts processed states 82 processedState = stats.Int64("flamingo-commerce/checkout/placeorder/state_run_count", "Counts how often a state is run", stats.UnitDimensionless) 83 // failedStateTransition counts failed state transitions 84 failedStateTransition = stats.Int64("flamingo-commerce/checkout/placeorder/state_failed_count", "Counts how often running a state resulted in a failure", stats.UnitDimensionless) 85 keyState, _ = tag.NewKey("state") 86 ) 87 88 func init() { 89 gob.Register(ErrorOccurredReason{}) 90 gob.Register(PaymentErrorOccurredReason{}) 91 gob.Register(PaymentCanceledByCustomerReason{}) 92 gob.Register(CartValidationErrorReason{}) 93 gob.Register(CanceledByCustomerReason{}) 94 95 if err := opencensus.View("flamingo-commerce/checkout/placeorder/state_run_count", processedState, view.Count(), keyState); err != nil { 96 panic(err) 97 } 98 99 if err := opencensus.View("flamingo-commerce/checkout/placeorder/state_failed_count", failedStateTransition, view.Count(), keyState); err != nil { 100 panic(err) 101 } 102 } 103 104 // Reason for the error occurred 105 func (e ErrorOccurredReason) Reason() string { 106 return e.Error 107 } 108 109 // Reason for the error occurred 110 func (e PaymentErrorOccurredReason) Reason() string { 111 return e.Error 112 } 113 114 // Reason for the error occurred 115 func (e PaymentCanceledByCustomerReason) Reason() string { 116 return "Payment canceled by customer" 117 } 118 119 // Reason for the error occurred 120 func (e CanceledByCustomerReason) Reason() string { 121 return "Place order canceled by customer" 122 } 123 124 // Reason for failing 125 func (e CartValidationErrorReason) Reason() string { 126 return "Cart invalid" 127 } 128 129 // Inject dependencies 130 func (f *Factory) Inject( 131 provider Provider, 132 dep *struct { 133 StartState State `inject:"startState"` 134 FailedState State `inject:"failedState"` 135 }, 136 ) { 137 f.provider = provider 138 139 if dep != nil { 140 f.failedState = dep.FailedState 141 f.startState = dep.StartState 142 } 143 } 144 145 // New process with initial state 146 func (f *Factory) New(returnURL *url.URL, cart cart.Cart) (*Process, error) { 147 if f.startState == nil { 148 return nil, errors.New("no start state given") 149 } 150 p := f.provider() 151 p.failedState = f.failedState 152 p.context = Context{ 153 UUID: uuid.New().String(), 154 CurrentStateName: f.startState.Name(), 155 Cart: cart, 156 ReturnURL: returnURL, 157 } 158 159 return p, nil 160 } 161 162 // NewFromProcessContext returns a new process with given Context 163 func (f *Factory) NewFromProcessContext(pctx Context) (*Process, error) { 164 p := f.provider() 165 p.failedState = f.failedState 166 p.context = pctx 167 168 return p, nil 169 } 170 171 // Inject dependencies 172 func (p *Process) Inject( 173 allStates map[string]State, 174 logger flamingo.Logger, 175 cfg *struct { 176 Area string `inject:"config:area"` 177 }, 178 ) *Process { 179 p.allStates = allStates 180 p.logger = logger. 181 WithField(flamingo.LogKeyModule, "checkout"). 182 WithField(flamingo.LogKeyCategory, "process") 183 184 if cfg != nil { 185 p.area = cfg.Area 186 } 187 188 return p 189 } 190 191 // Run triggers run on current state 192 func (p *Process) Run(ctx context.Context) { 193 currentState, err := p.CurrentState() 194 if err != nil { 195 p.Failed(ctx, ErrorOccurredReason{Error: err.Error()}) 196 return 197 } 198 199 censusCtx, _ := tag.New(ctx, tag.Upsert(opencensus.KeyArea, p.area), tag.Upsert(keyState, currentState.Name())) 200 stats.Record(censusCtx, processedState.M(1)) 201 202 runResult := currentState.Run(ctx, p) 203 if runResult.RollbackData != nil { 204 p.context.RollbackReferences = append(p.context.RollbackReferences, RollbackReference{ 205 StateName: currentState.Name(), 206 Data: runResult.RollbackData, 207 }) 208 } 209 210 if runResult.Failed != nil { 211 stats.Record(censusCtx, failedStateTransition.M(1)) 212 p.Failed(ctx, runResult.Failed) 213 } 214 } 215 216 // CurrentState of the process context 217 func (p *Process) CurrentState() (State, error) { 218 state, found := p.allStates[p.Context().CurrentStateName] 219 if !found { 220 return nil, fmt.Errorf("current process context state %q not found", p.Context().CurrentStateName) 221 } 222 return state, nil 223 } 224 225 func (p *Process) rollback(ctx context.Context) error { 226 for i := len(p.context.RollbackReferences) - 1; i >= 0; i-- { 227 rollbackRef := p.context.RollbackReferences[i] 228 state, ok := p.allStates[rollbackRef.StateName] 229 if !ok { 230 p.logger.Error(fmt.Errorf("state %q not found for rollback", rollbackRef.StateName)) 231 continue 232 } 233 234 err := state.Rollback(ctx, rollbackRef.Data) 235 if _, ok := err.(*FatalRollbackError); ok { 236 return err 237 } 238 239 if err != nil { 240 p.logger.Error(fmt.Sprintf("Non fatal error during state %q continue rollback: %s", state.Name(), err)) 241 } 242 } 243 244 return nil 245 } 246 247 // Context to get current process context 248 func (p *Process) Context() Context { 249 return p.context 250 } 251 252 // UpdateState updates the current state in the context and its related state data 253 func (p *Process) UpdateState(s string, stateData StateData) { 254 p.context.CurrentStateName = s 255 p.context.CurrentStateData = stateData 256 } 257 258 // UpdateCart updates the cart in the current state context 259 func (p *Process) UpdateCart(cartToStore cart.Cart) { 260 p.context.Cart = cartToStore 261 } 262 263 // UpdateOrderInfo updates the order infos of the current context 264 func (p *Process) UpdateOrderInfo(info *application.PlaceOrderInfo) { 265 p.context.PlaceOrderInfo = info 266 } 267 268 // Failed performs all collected rollbacks and switches to FailedState 269 func (p *Process) Failed(ctx context.Context, reason FailedReason) { 270 err := p.rollback(ctx) 271 if err != nil { 272 p.logger.WithContext(ctx).Error("fatal rollback error: ", err) 273 } 274 275 p.context.FailedReason = reason 276 p.UpdateState(p.failedState.Name(), nil) 277 }