133 lines
4.6 KiB
Go
133 lines
4.6 KiB
Go
package identity
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
customer "git.sendico.io/tech/sxchnge/product/customers/model"
|
|
merrors "git.sendico.io/tech/sxchnge/shared/errors"
|
|
mlogger "git.sendico.io/tech/sxchnge/shared/logging"
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type Provider interface {
|
|
Namespace() string
|
|
Register(context.Context, string) (string, error)
|
|
Submit(context.Context, string, string, Facts) (Receipt, error)
|
|
}
|
|
|
|
type Service struct {
|
|
Store Store
|
|
Provider Provider
|
|
Logger mlogger.Logger
|
|
}
|
|
|
|
func unavailable() error { return merrors.ServiceUnavailable("sign-up facts unavailable") }
|
|
|
|
// Ensure completes only after a provider receipt has been persisted. Registration
|
|
// has no idempotency contract, so its durable claim is never automatically expired.
|
|
func (s *Service) Ensure(ctx context.Context, a *customer.Account) (result error) {
|
|
if a == nil || a.ID.IsZero() {
|
|
return merrors.InvalidArgument("customer required")
|
|
}
|
|
started := time.Now()
|
|
requestRef := ""
|
|
outcome := "unavailable"
|
|
defer func() {
|
|
requests.WithLabelValues(outcome).Inc()
|
|
duration.Observe(time.Since(started).Seconds())
|
|
fields := []zap.Field{zap.String("account_ref", a.ID.Hex()), zap.String("request_ref", requestRef), zap.String("outcome", outcome)}
|
|
if result != nil {
|
|
s.Logger.Warn("Sign-up facts handoff incomplete", fields...)
|
|
} else {
|
|
s.Logger.Info("Sign-up facts handoff completed", fields...)
|
|
}
|
|
}()
|
|
namespace := s.Provider.Namespace()
|
|
b, err := s.Store.Get(ctx, a.ID)
|
|
if errors.Is(err, merrors.ErrNoData) {
|
|
b = &Binding{Namespace: namespace, RegistrationRequest: bson.NewObjectID().Hex(), Facts: Facts{FullName: a.Name, Email: a.Login, CustomerRef: a.ID.Hex()}}
|
|
b.SetID(a.ID)
|
|
if err = s.Store.Create(ctx, b); err != nil {
|
|
if !errors.Is(err, merrors.ErrDataConflict) {
|
|
return merrors.OpaqueWrap(merrors.ErrServiceUnavailable, err, "persist sign-up facts")
|
|
}
|
|
b, err = s.Store.Get(ctx, a.ID)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return merrors.OpaqueWrap(merrors.ErrServiceUnavailable, err, "load sign-up facts")
|
|
}
|
|
requestRef = b.RegistrationRequest
|
|
if b.Namespace != namespace {
|
|
outcome = "attention_required"
|
|
return merrors.FailedPrecondition("sign-up provider binding changed")
|
|
}
|
|
if b.Receipt != nil {
|
|
outcome = "replayed"
|
|
return nil
|
|
}
|
|
if b.SubjectRef == "" {
|
|
claimed, err := s.Store.Claim(ctx, a.ID, namespace)
|
|
if err != nil {
|
|
return merrors.OpaqueWrap(merrors.ErrServiceUnavailable, err, "claim individual registration")
|
|
}
|
|
if !claimed {
|
|
// Another request may have completed while this request was claiming.
|
|
b, err = s.Store.Get(ctx, a.ID)
|
|
if err != nil {
|
|
return merrors.OpaqueWrap(merrors.ErrServiceUnavailable, err, "reload sign-up facts")
|
|
}
|
|
if b.SubjectRef == "" {
|
|
if b.RegisteringAt.Add(30 * time.Second).After(time.Now()) {
|
|
return unavailable()
|
|
}
|
|
outcome = "attention_required"
|
|
return merrors.FailedPrecondition("individual registration requires reconciliation")
|
|
}
|
|
} else {
|
|
subject, err := s.Provider.Register(ctx, b.RegistrationRequest)
|
|
if err != nil {
|
|
outcome = "attention_required"
|
|
return merrors.OpaqueWrap(merrors.ErrFailedPrecondition, err, "individual registration requires reconciliation")
|
|
}
|
|
// Persist even if the browser left while Sendico accepted registration.
|
|
saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
|
err = s.Store.Bind(saveCtx, a.ID, namespace, subject)
|
|
cancel()
|
|
if err != nil {
|
|
outcome = "attention_required"
|
|
s.Logger.Warn("Registered individual binding needs reconciliation", zap.String("account_ref", a.ID.Hex()), zap.String("subject_ref", subject))
|
|
return merrors.OpaqueWrap(merrors.ErrFailedPrecondition, err, "persist individual binding")
|
|
}
|
|
b.SubjectRef = subject
|
|
}
|
|
}
|
|
receipt, err := s.Provider.Submit(ctx, b.SubjectRef, sourceRef(a.ID), b.Facts)
|
|
if err != nil {
|
|
return merrors.OpaqueWrap(merrors.ErrServiceUnavailable, err, "submit sign-up facts")
|
|
}
|
|
if receipt.SubjectRef != b.SubjectRef || receipt.SubmissionRef == "" || receipt.AcceptedAt.IsZero() {
|
|
return unavailable()
|
|
}
|
|
if err = s.Store.Accept(ctx, a.ID, namespace, receipt); err != nil {
|
|
return merrors.OpaqueWrap(merrors.ErrServiceUnavailable, err, "persist facts receipt")
|
|
}
|
|
outcome = "accepted"
|
|
return nil
|
|
}
|
|
|
|
// Ready checks confirmed intake without external I/O inside activation transactions.
|
|
func (s *Service) Ready(ctx context.Context, id bson.ObjectID) error {
|
|
b, err := s.Store.Get(ctx, id)
|
|
if err != nil {
|
|
return merrors.OpaqueWrap(merrors.ErrServiceUnavailable, err, "load facts receipt")
|
|
}
|
|
if b.Namespace != s.Provider.Namespace() || b.Receipt == nil {
|
|
return unavailable()
|
|
}
|
|
return nil
|
|
}
|