package customerverification import ( "context" "errors" "fmt" "io" "github.com/google/uuid" ) var ( ErrUnsupportedType = errors.New("unsupported file type") ErrInvalidSide = errors.New("side must be 'front' or 'back'") ) type Service struct { repo Repository storage *Storage } func NewService(repo Repository, storage *Storage) *Service { return &Service{repo: repo, storage: storage} } type DocumentInput struct { Reader io.Reader ContentType string } // Submit stores the front/back document images and (re)creates the // customer's verification record in "pending" status -- including on // resubmission after a rejection, so admins always review the latest pair. func (s *Service) Submit(ctx context.Context, userID uuid.UUID, front, back DocumentInput) (*CustomerVerification, error) { frontExt, ok := allowedTypes[front.ContentType] if !ok { return nil, ErrUnsupportedType } backExt, ok := allowedTypes[back.ContentType] if !ok { return nil, ErrUnsupportedType } frontKey := uuid.NewString() + frontExt if err := s.storage.Save(frontKey, front.Reader); err != nil { return nil, fmt.Errorf("save front document: %w", err) } backKey := uuid.NewString() + backExt if err := s.storage.Save(backKey, back.Reader); err != nil { _ = s.storage.Delete(frontKey) return nil, fmt.Errorf("save back document: %w", err) } v := &CustomerVerification{ ID: uuid.New(), UserID: userID, FrontKey: frontKey, BackKey: backKey, Status: StatusPending, } if err := s.repo.Upsert(ctx, v); err != nil { _ = s.storage.Delete(frontKey) _ = s.storage.Delete(backKey) return nil, err } return v, nil } func (s *Service) GetByUser(ctx context.Context, userID uuid.UUID) (*CustomerVerification, error) { return s.repo.FindByUserID(ctx, userID) } func (s *Service) Get(ctx context.Context, id uuid.UUID) (*CustomerVerification, error) { return s.repo.FindByID(ctx, id) } func (s *Service) List(ctx context.Context, status string) ([]*CustomerVerification, error) { return s.repo.List(ctx, status) } func (s *Service) Review(ctx context.Context, id uuid.UUID, status, adminNote string) (*CustomerVerification, error) { if !ValidStatuses[status] { return nil, fmt.Errorf("invalid status %q", status) } return s.repo.UpdateStatus(ctx, id, status, adminNote) } // IsApproved satisfies orders.VerificationChecker: an unsubmitted customer // (no row at all) is simply "not approved yet", not an error. func (s *Service) IsApproved(ctx context.Context, userID uuid.UUID) (bool, error) { v, err := s.repo.FindByUserID(ctx, userID) if errors.Is(err, ErrNotFound) { return false, nil } if err != nil { return false, err } return v.Status == StatusApproved, nil } // DocumentPath resolves which side of a verification record to read from // disk, for the handler to stream after checking the caller is authorized. func (s *Service) DocumentPath(v *CustomerVerification, side string) (string, error) { switch side { case "front": return s.storage.Path(v.FrontKey), nil case "back": return s.storage.Path(v.BackKey), nil default: return "", ErrInvalidSide } }