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  }