Submit customer identity proofs and show reviewed limits
This commit is contained in:
1 parent
7bf8b8087a
commit
1354551ae8
50 files changed
+1777
-67
No files matched your search
@@ -0,0 +1,149 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
customer "git.sendico.io/tech/sxchnge/product/customers/model"
|
||||
merrors "git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Profile holds unverified customer assertions. Email and customer reference are
|
||||
// always supplied by the authenticated backend, never accepted from the browser.
|
||||
type Profile struct {
|
||||
FullName string
|
||||
DateOfBirth string
|
||||
ResidenceCountry string
|
||||
}
|
||||
|
||||
func (p Profile) Validate() error {
|
||||
birth, err := time.Parse("2006-01-02", p.DateOfBirth)
|
||||
if strings.TrimSpace(p.FullName) == "" || len(p.FullName) > 800 || err != nil || !birth.Before(time.Now()) || len(p.ResidenceCountry) != 2 || p.ResidenceCountry[0] < 'A' || p.ResidenceCountry[0] > 'Z' || p.ResidenceCountry[1] < 'A' || p.ResidenceCountry[1] > 'Z' {
|
||||
return merrors.InvalidArgument("complete identity profile required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Verification struct {
|
||||
Status string
|
||||
UpgradePending bool
|
||||
Reason string
|
||||
SubmissionID string
|
||||
Profile Profile
|
||||
}
|
||||
|
||||
// VerificationProvider consumes reviewed state through the platform public API.
|
||||
// It cannot accept a browser-selected subject, policy profile or approval.
|
||||
type VerificationProvider interface {
|
||||
Verification(context.Context, string) (Verification, error)
|
||||
SubmitVerification(context.Context, string, string, Profile, Facts, []byte, []byte) (Verification, error)
|
||||
}
|
||||
|
||||
// Decimal amounts are transported losslessly in their currency's major unit.
|
||||
// No fiat scale or illustrative client caps are applied to provider policy.
|
||||
type Limit struct {
|
||||
Operation string
|
||||
Period string
|
||||
Currency string
|
||||
Cap string
|
||||
Remaining string
|
||||
Used string
|
||||
Reserved string
|
||||
}
|
||||
type Limits struct {
|
||||
Tier string
|
||||
ProfileRef string
|
||||
Entries []Limit
|
||||
}
|
||||
type LimitsProvider interface {
|
||||
Limits(context.Context, string) (Limits, error)
|
||||
}
|
||||
|
||||
func (s *Service) Limits(ctx context.Context, a *customer.Account) (Limits, error) {
|
||||
b, err := s.binding(ctx, a)
|
||||
if err != nil {
|
||||
return Limits{}, err
|
||||
}
|
||||
p, ok := s.Provider.(LimitsProvider)
|
||||
if !ok {
|
||||
return Limits{}, unavailable()
|
||||
}
|
||||
return p.Limits(ctx, b.SubjectRef)
|
||||
}
|
||||
|
||||
func (s *Service) binding(ctx context.Context, a *customer.Account) (*Binding, error) {
|
||||
if s == nil || s.Provider == nil || s.Store == nil {
|
||||
return nil, merrors.ServiceUnavailable("identity verification unavailable")
|
||||
}
|
||||
if a == nil || a.ID.IsZero() {
|
||||
return nil, merrors.InvalidArgument("customer required")
|
||||
}
|
||||
b, err := s.Store.Get(ctx, a.ID)
|
||||
if errors.Is(err, merrors.ErrNoData) || err == nil && b.Receipt == nil {
|
||||
if err = s.Ensure(ctx, a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err = s.Store.Get(ctx, a.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if b.Namespace != s.Provider.Namespace() || b.SubjectRef == "" || b.Receipt == nil || b.Receipt.SubjectRef != b.SubjectRef {
|
||||
return nil, merrors.FailedPrecondition("identity binding requires reconciliation")
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (s *Service) Verification(ctx context.Context, a *customer.Account) (Verification, error) {
|
||||
b, err := s.binding(ctx, a)
|
||||
if err != nil {
|
||||
return Verification{}, err
|
||||
}
|
||||
provider, ok := s.Provider.(VerificationProvider)
|
||||
if !ok {
|
||||
return Verification{}, unavailable()
|
||||
}
|
||||
v, err := provider.Verification(ctx, b.SubjectRef)
|
||||
if err == nil && v.Profile.FullName == "" {
|
||||
v.Profile.FullName = a.Name
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (s *Service) SubmitVerification(ctx context.Context, a *customer.Account, key string, p Profile, document, selfie []byte) (Verification, error) {
|
||||
if len(key) < 16 || len(key) > 128 {
|
||||
return Verification{}, merrors.InvalidArgument("valid idempotency key required")
|
||||
}
|
||||
if err := p.Validate(); err != nil {
|
||||
return Verification{}, err
|
||||
}
|
||||
for _, photo := range [][]byte{document, selfie} {
|
||||
if err := ValidatePhoto(photo); err != nil {
|
||||
return Verification{}, err
|
||||
}
|
||||
}
|
||||
b, err := s.binding(ctx, a)
|
||||
if err != nil {
|
||||
return Verification{}, err
|
||||
}
|
||||
provider, ok := s.Provider.(VerificationProvider)
|
||||
if !ok {
|
||||
return Verification{}, unavailable()
|
||||
}
|
||||
digest := sha256.Sum256([]byte(a.ID.Hex() + "\x00" + key))
|
||||
facts := b.Facts
|
||||
facts.Email = a.Login
|
||||
facts.CustomerRef = a.ID.Hex()
|
||||
v, err := provider.SubmitVerification(ctx, b.SubjectRef, "sxchnge:identity:"+hex.EncodeToString(digest[:]), p, facts, document, selfie)
|
||||
if err != nil {
|
||||
s.Logger.Warn("Identity proof submission incomplete", zap.String("account_ref", a.ID.Hex()))
|
||||
return Verification{}, err
|
||||
}
|
||||
s.Logger.Info("Identity proofs accepted for review", zap.String("account_ref", a.ID.Hex()), zap.String("submission_ref", v.SubmissionID))
|
||||
return v, nil
|
||||
}
|
||||
Reference in new issue
Block a user