Skip to content

Commit 5425718

Browse files
committed
Add to onboarding list after creating new user
1 parent 0f0e0cb commit 5425718

9 files changed

Lines changed: 183 additions & 76 deletions

File tree

api/cmd/experiments/main.go

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,16 @@
11
package main
22

33
import (
4-
"context"
5-
"errors"
6-
"fmt"
74
"log"
8-
"os"
95

6+
"github.com/NdoleStudio/httpsms/pkg/di"
107
"github.com/joho/godotenv"
11-
"go.opentelemetry.io/otel/metric/instrument"
12-
13-
"github.com/uptrace/uptrace-go/uptrace"
14-
"go.opentelemetry.io/otel"
15-
"go.opentelemetry.io/otel/attribute"
16-
"go.opentelemetry.io/otel/metric/global"
178
)
189

1910
func main() {
2011
err := godotenv.Load("../../.env")
2112
if err != nil {
2213
log.Fatal("Error loading .env file")
2314
}
24-
25-
ctx := context.Background()
26-
27-
// Configure OpenTelemetry with sensible defaults.
28-
uptrace.ConfigureOpentelemetry(
29-
uptrace.WithDSN(os.Getenv("UPTRACE_DSN")),
30-
uptrace.WithServiceName(os.Getenv("httpsms")),
31-
uptrace.WithServiceVersion("1.0.0"),
32-
)
33-
// Send buffered spans and free resources.
34-
defer uptrace.Shutdown(ctx)
35-
36-
// Create a tracer. Usually, tracer is a global variable.
37-
tracer := otel.Tracer("app_or_package_name")
38-
39-
// Create a root span (a trace) to measure some operation.
40-
ctx, main := tracer.Start(ctx, "main-operation")
41-
// End the span when the operation we are measuring is done.
42-
defer main.End()
43-
44-
// We pass the main (parent) span in the ctx so we can reconstruct a trace later.
45-
_, child1 := tracer.Start(ctx, "child1-of-main")
46-
child1.SetAttributes(attribute.String("key1", "value1"))
47-
child1.RecordError(errors.New("error1"))
48-
child1.End()
49-
50-
_, child2 := tracer.Start(ctx, "child2-of-main")
51-
child2.SetAttributes(attribute.Int("key2", 42), attribute.Float64("key3", 123.456))
52-
child2.End()
53-
54-
fmt.Printf("trace: %s\n", uptrace.TraceURL(main))
55-
56-
meter := global.MeterProvider().Meter("httpsms")
57-
counter, err := meter.SyncInt64().Counter(
58-
"test.my_counter",
59-
instrument.WithUnit("1"),
60-
instrument.WithDescription("Just a test counter"),
61-
)
62-
if err != nil {
63-
log.Fatal(err)
64-
}
65-
66-
// Increment the counter.
67-
counter.Add(ctx, 1, attribute.String("foo", "bar"))
68-
counter.Add(ctx, 10, attribute.String("hello", "world"))
15+
_ := di.NewContainer("http-sms")
6916
}

api/go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ require (
2424
github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177
2525
github.com/pkg/errors v0.9.1
2626
github.com/rs/zerolog v1.26.1
27+
github.com/sendgrid/sendgrid-go v3.12.0+incompatible
2728
github.com/swaggo/swag v1.8.2
2829
github.com/thedevsaddam/govalidator v1.9.10
2930
github.com/uptrace/uptrace-go v1.9.0
3031
go.opentelemetry.io/otel v1.9.0
31-
go.opentelemetry.io/otel/metric v0.31.0
3232
go.opentelemetry.io/otel/sdk v1.9.0
3333
go.opentelemetry.io/otel/trace v1.9.0
3434
google.golang.org/api v0.85.0
@@ -96,6 +96,7 @@ require (
9696
github.com/olekukonko/tablewriter v0.0.1 // indirect
9797
github.com/rivo/uniseg v0.2.0 // indirect
9898
github.com/russross/blackfriday/v2 v2.0.1 // indirect
99+
github.com/sendgrid/rest v2.6.9+incompatible // indirect
99100
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
100101
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
101102
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 // indirect
@@ -112,6 +113,7 @@ require (
112113
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.9.0 // indirect
113114
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.9.0 // indirect
114115
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.9.0 // indirect
116+
go.opentelemetry.io/otel/metric v0.31.0 // indirect
115117
go.opentelemetry.io/otel/sdk/metric v0.31.0 // indirect
116118
go.opentelemetry.io/proto/otlp v0.18.0 // indirect
117119
go.uber.org/atomic v1.7.0 // indirect

api/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,10 @@ github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+
463463
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
464464
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
465465
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
466+
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
467+
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
468+
github.com/sendgrid/sendgrid-go v3.12.0+incompatible h1:/N2vx18Fg1KmQOh6zESc5FJB8pYwt5QFBDflYPh1KVg=
469+
github.com/sendgrid/sendgrid-go v3.12.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
466470
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
467471
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
468472
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=

api/pkg/di/container.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,18 @@ func (container *Container) PhoneService() (service *services.PhoneService) {
524524
)
525525
}
526526

527+
// MarketingService creates a new instance of services.MarketingService
528+
func (container *Container) MarketingService() (service *services.MarketingService) {
529+
container.logger.Debug(fmt.Sprintf("creating %T", service))
530+
return services.NewMarketingService(
531+
container.Logger(),
532+
container.Tracer(),
533+
container.FirebaseAuthClient(),
534+
os.Getenv("SENDGRID_API_KEY"),
535+
os.Getenv("SENDGRID_LIST_ID"),
536+
)
537+
}
538+
527539
// UserService creates a new instance of services.UserService
528540
func (container *Container) UserService() (service *services.UserService) {
529541
container.logger.Debug(fmt.Sprintf("creating %T", service))
@@ -533,6 +545,7 @@ func (container *Container) UserService() (service *services.UserService) {
533545
container.UserRepository(),
534546
container.Mailer(),
535547
container.UserEmailFactory(),
548+
container.MarketingService(),
536549
)
537550
}
538551

api/pkg/repositories/gorm_user_repository.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,18 @@ func (repository *gormUserRepository) Load(ctx context.Context, userID entities.
101101
return user, nil
102102
}
103103

104-
func (repository *gormUserRepository) LoadOrStore(ctx context.Context, authUser entities.AuthUser) (*entities.User, error) {
104+
func (repository *gormUserRepository) LoadOrStore(ctx context.Context, authUser entities.AuthUser) (*entities.User, bool, error) {
105105
ctx, span := repository.tracer.Start(ctx)
106106
defer span.End()
107107

108108
user, err := repository.Load(ctx, authUser.ID)
109109
if err == nil {
110-
return user, nil
110+
return user, false, nil
111111
}
112112

113113
apiKey, err := repository.generateAPIKey(64)
114114
if err != nil {
115-
return nil, stacktrace.Propagate(err, "cannot generate apiKey")
115+
return nil, false, stacktrace.Propagate(err, "cannot generate apiKey")
116116
}
117117

118118
user = &entities.User{
@@ -124,16 +124,23 @@ func (repository *gormUserRepository) LoadOrStore(ctx context.Context, authUser
124124
UpdatedAt: time.Now().UTC(),
125125
}
126126

127+
isNew := false
127128
err = crdbgorm.ExecuteTx(ctx, repository.db, nil, func(tx *gorm.DB) error {
128-
return tx.WithContext(ctx).Where(entities.User{ID: user.ID}).FirstOrCreate(user).Error
129+
result := tx.WithContext(ctx).Where(entities.User{ID: user.ID}).FirstOrCreate(user)
130+
if result.Error != nil {
131+
return result.Error
132+
}
133+
if result.RowsAffected > 0 {
134+
isNew = true
135+
}
136+
return result.Error
129137
})
130-
131138
if err != nil {
132139
msg := fmt.Sprintf("cannot create user from auth user [%+#v]", authUser)
133-
return user, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
140+
return user, isNew, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
134141
}
135142

136-
return user, nil
143+
return user, isNew, nil
137144
}
138145

139146
// generateRandomBytes returns securely generated random bytes.

api/pkg/repositories/user_repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ type UserRepository interface {
2121
Load(ctx context.Context, userID entities.UserID) (*entities.User, error)
2222

2323
// LoadOrStore an entities.User by entities.AuthUser
24-
LoadOrStore(ctx context.Context, user entities.AuthUser) (*entities.User, error)
24+
LoadOrStore(ctx context.Context, user entities.AuthUser) (*entities.User, bool, error)
2525
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package services
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
"strings"
9+
10+
"firebase.google.com/go/auth"
11+
"github.com/NdoleStudio/httpsms/pkg/entities"
12+
"github.com/NdoleStudio/httpsms/pkg/telemetry"
13+
"github.com/davecgh/go-spew/spew"
14+
"github.com/palantir/stacktrace"
15+
"github.com/sendgrid/sendgrid-go"
16+
)
17+
18+
// MarketingService is handles marketing requests
19+
type MarketingService struct {
20+
logger telemetry.Logger
21+
tracer telemetry.Tracer
22+
authClient *auth.Client
23+
sendgridAPIKey string
24+
sendgridListID string
25+
}
26+
27+
type sendgridContact struct {
28+
FirstName string `json:"first_name"`
29+
LastName string `json:"last_name"`
30+
Email string `json:"email"`
31+
}
32+
33+
type sendgridContactRequest struct {
34+
ListIDs []string `json:"list_ids"`
35+
Contacts []sendgridContact `json:"contacts"`
36+
}
37+
38+
// NewMarketingService creates a new instance of the MarketingService
39+
func NewMarketingService(
40+
logger telemetry.Logger,
41+
tracer telemetry.Tracer,
42+
authClient *auth.Client,
43+
sendgridAPIKey string,
44+
sendgridListID string,
45+
) *MarketingService {
46+
return &MarketingService{
47+
logger: logger.WithService(fmt.Sprintf("%T", &MarketingService{})),
48+
tracer: tracer,
49+
authClient: authClient,
50+
sendgridAPIKey: sendgridAPIKey,
51+
sendgridListID: sendgridListID,
52+
}
53+
}
54+
55+
// AddToList adds a new user on the onboarding automation.
56+
func (service *MarketingService) AddToList(ctx context.Context, user *entities.User) {
57+
ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
58+
defer span.End()
59+
60+
userRecord, err := service.authClient.GetUser(ctx, string(user.ID))
61+
if err != nil {
62+
msg := fmt.Sprintf("cannot get auth user with id [%s]", user.ID)
63+
ctxLogger.Error(stacktrace.Propagate(err, msg))
64+
return
65+
}
66+
67+
id, err := service.addContact(sendgridContactRequest{
68+
ListIDs: []string{service.sendgridListID},
69+
Contacts: []sendgridContact{service.toSendgridContact(userRecord)},
70+
})
71+
if err != nil {
72+
msg := fmt.Sprintf("cannot add user with id [%s] to list [%s]", user.ID, service.sendgridListID)
73+
ctxLogger.Error(stacktrace.Propagate(err, msg))
74+
return
75+
}
76+
77+
ctxLogger.Info(fmt.Sprintf("user [%s] added to list [%s] with job [%s]", user.ID, service.sendgridListID, id))
78+
}
79+
80+
func (service *MarketingService) toSendgridContact(user *auth.UserRecord) sendgridContact {
81+
name := strings.TrimSpace(user.DisplayName)
82+
if name == "" {
83+
return sendgridContact{
84+
FirstName: "",
85+
LastName: "",
86+
Email: user.Email,
87+
}
88+
}
89+
90+
parts := strings.Split(name, " ")
91+
if len(parts) == 1 {
92+
return sendgridContact{
93+
FirstName: name,
94+
LastName: "",
95+
Email: user.Email,
96+
}
97+
}
98+
99+
return sendgridContact{
100+
FirstName: strings.Join(parts[0:len(parts)-1], " "),
101+
LastName: parts[len(parts)-1],
102+
Email: user.Email,
103+
}
104+
}
105+
106+
func (service *MarketingService) addContact(contactRequest sendgridContactRequest) (string, error) {
107+
request := sendgrid.GetRequest(service.sendgridAPIKey, "/v3/marketing/contacts", "https://api.sendgrid.com")
108+
request.Method = "PUT"
109+
110+
body, err := json.Marshal(contactRequest)
111+
if err != nil {
112+
log.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot marshal [%s]", spew.Sdump(contactRequest))))
113+
}
114+
115+
request.Body = body
116+
response, err := sendgrid.API(request)
117+
if err != nil {
118+
return "", stacktrace.Propagate(err, fmt.Sprintf("cannot add contact to sendgrid list [%s]", spew.Sdump(contactRequest)))
119+
}
120+
return response.Body, nil
121+
}

api/pkg/services/user_service.go

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ import (
1717

1818
// UserService is handles user requests
1919
type UserService struct {
20-
logger telemetry.Logger
21-
tracer telemetry.Tracer
22-
emailFactory emails.UserEmailFactory
23-
mailer emails.Mailer
24-
repository repositories.UserRepository
20+
logger telemetry.Logger
21+
tracer telemetry.Tracer
22+
emailFactory emails.UserEmailFactory
23+
mailer emails.Mailer
24+
repository repositories.UserRepository
25+
marketingService *MarketingService
2526
}
2627

2728
// NewUserService creates a new UserService
@@ -31,13 +32,15 @@ func NewUserService(
3132
repository repositories.UserRepository,
3233
mailer emails.Mailer,
3334
emailFactory emails.UserEmailFactory,
35+
marketingService *MarketingService,
3436
) (s *UserService) {
3537
return &UserService{
36-
logger: logger.WithService(fmt.Sprintf("%T", s)),
37-
tracer: tracer,
38-
mailer: mailer,
39-
emailFactory: emailFactory,
40-
repository: repository,
38+
logger: logger.WithService(fmt.Sprintf("%T", s)),
39+
tracer: tracer,
40+
mailer: mailer,
41+
marketingService: marketingService,
42+
emailFactory: emailFactory,
43+
repository: repository,
4144
}
4245
}
4346

@@ -46,12 +49,16 @@ func (service *UserService) Get(ctx context.Context, authUser entities.AuthUser)
4649
ctx, span := service.tracer.Start(ctx)
4750
defer span.End()
4851

49-
user, err := service.repository.LoadOrStore(ctx, authUser)
52+
user, isNew, err := service.repository.LoadOrStore(ctx, authUser)
5053
if err != nil {
5154
msg := fmt.Sprintf("could not get [%T] with from [%+#v]", user, authUser)
5255
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
5356
}
5457

58+
if isNew {
59+
service.marketingService.AddToList(ctx, user)
60+
}
61+
5562
return user, nil
5663
}
5764

@@ -67,12 +74,16 @@ func (service *UserService) Update(ctx context.Context, authUser entities.AuthUs
6774

6875
ctxLogger := service.tracer.CtxLogger(service.logger, span)
6976

70-
user, err := service.repository.LoadOrStore(ctx, authUser)
77+
user, isNew, err := service.repository.LoadOrStore(ctx, authUser)
7178
if err != nil {
7279
msg := fmt.Sprintf("could not get [%T] with from [%+#v]", user, authUser)
7380
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
7481
}
7582

83+
if isNew {
84+
service.marketingService.AddToList(ctx, user)
85+
}
86+
7687
user.ActivePhoneID = &params.ActivePhoneID
7788

7889
if err = service.repository.Update(ctx, user); err != nil {

0 commit comments

Comments
 (0)