From cfb219e20663d048fc4c4dd50b246edcfebe9ffa Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 10 Feb 2026 20:40:23 +0100 Subject: [PATCH] account state changes --- .../notificationimp/telegram/contact.go | 54 +++++++- .../notificationimp/telegram/contact_test.go | 118 ++++++++++++++++++ api/pkg/model/contactrequest.go | 6 + .../internal/server/accountapiimp/email.go | 3 + .../internal/server/accountapiimp/signup.go | 43 +++++++ api/server/internal/server/siteimp/contact.go | 3 + 6 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 api/notification/internal/server/notificationimp/telegram/contact_test.go diff --git a/api/notification/internal/server/notificationimp/telegram/contact.go b/api/notification/internal/server/notificationimp/telegram/contact.go index fc73d22a..025b5e10 100644 --- a/api/notification/internal/server/notificationimp/telegram/contact.go +++ b/api/notification/internal/server/notificationimp/telegram/contact.go @@ -1,8 +1,60 @@ package telegram -import "github.com/tech/sendico/pkg/model" +import ( + "strings" + + "github.com/tech/sendico/pkg/model" +) + +const legacyContactRequestTopicSignup = "signup_request" func newContactRequestTemplate(request *model.ContactRequest) messageTemplate { + if request == nil { + request = &model.ContactRequest{} + } + + switch normalizeContactRequestTopic(request.Topic) { + case model.ContactRequestTopicAccountVerificationCompleted: + return newAccountVerificationCompletedTemplate(request) + case model.ContactRequestTopicSignupCompleted: + return newSignupCompletedTemplate(request) + default: + return newSiteContactTemplate(request) + } +} + +func normalizeContactRequestTopic(topic string) string { + normalized := strings.ToLower(strings.TrimSpace(topic)) + if normalized == legacyContactRequestTopicSignup { + return model.ContactRequestTopicSignupCompleted + } + return normalized +} + +func newAccountVerificationCompletedTemplate(request *model.ContactRequest) messageTemplate { + return messageTemplate{ + title: "Account verification completed", + emphasize: []string{"verification completed"}, + fields: []messageField{ + {label: "Name", value: request.Name}, + {label: "Email", value: request.Email}, + }, + } +} + +func newSignupCompletedTemplate(request *model.ContactRequest) messageTemplate { + return messageTemplate{ + title: "New signup completed", + emphasize: []string{"signup completed"}, + fields: []messageField{ + {label: "Organization", value: request.Company}, + {label: "Name", value: request.Name}, + {label: "Email", value: request.Email}, + }, + } +} + +func newSiteContactTemplate(request *model.ContactRequest) messageTemplate { return messageTemplate{ title: "New site request received", emphasize: []string{"site request"}, diff --git a/api/notification/internal/server/notificationimp/telegram/contact_test.go b/api/notification/internal/server/notificationimp/telegram/contact_test.go new file mode 100644 index 00000000..a144383f --- /dev/null +++ b/api/notification/internal/server/notificationimp/telegram/contact_test.go @@ -0,0 +1,118 @@ +package telegram + +import ( + "reflect" + "testing" + + "github.com/tech/sendico/pkg/model" +) + +func TestNewContactRequestTemplate_SignupTopic(t *testing.T) { + template := newContactRequestTemplate(&model.ContactRequest{ + Name: "Alice Example", + Email: "alice@example.com", + Company: "Acme Inc", + Topic: model.ContactRequestTopicSignupCompleted, + }) + + if template.title != "New signup completed" { + t.Fatalf("unexpected title: %s", template.title) + } + if !reflect.DeepEqual(template.emphasize, []string{"signup completed"}) { + t.Fatalf("unexpected emphasize words: %#v", template.emphasize) + } + + expectedFields := []messageField{ + {label: "Organization", value: "Acme Inc"}, + {label: "Name", value: "Alice Example"}, + {label: "Email", value: "alice@example.com"}, + } + if !reflect.DeepEqual(template.fields, expectedFields) { + t.Fatalf("unexpected fields: %#v", template.fields) + } +} + +func TestNewContactRequestTemplate_LegacySignupTopic(t *testing.T) { + template := newContactRequestTemplate(&model.ContactRequest{ + Name: "Alice Example", + Email: "alice@example.com", + Company: "Acme Inc", + Topic: "signup_request", + }) + + if template.title != "New signup completed" { + t.Fatalf("unexpected title: %s", template.title) + } +} + +func TestNewContactRequestTemplate_DefaultTopic(t *testing.T) { + template := newContactRequestTemplate(&model.ContactRequest{ + Name: "Alice Example", + Email: "alice@example.com", + Phone: "+123456", + Company: "Acme Inc", + Topic: "partnership", + Message: "Hi there", + }) + + if template.title != "New site request received" { + t.Fatalf("unexpected title: %s", template.title) + } + if !reflect.DeepEqual(template.emphasize, []string{"site request"}) { + t.Fatalf("unexpected emphasize words: %#v", template.emphasize) + } + + expectedFields := []messageField{ + {label: "Name", value: "Alice Example"}, + {label: "Email", value: "alice@example.com"}, + {label: "Phone", value: "+123456"}, + {label: "Company", value: "Acme Inc"}, + {label: "Topic", value: "partnership"}, + {label: "Message", value: "Hi there"}, + } + if !reflect.DeepEqual(template.fields, expectedFields) { + t.Fatalf("unexpected fields: %#v", template.fields) + } +} + +func TestNewContactRequestTemplate_AccountVerificationCompletedTopic(t *testing.T) { + template := newContactRequestTemplate(&model.ContactRequest{ + Name: "Alice Example", + Email: "alice@example.com", + Topic: model.ContactRequestTopicAccountVerificationCompleted, + }) + + if template.title != "Account verification completed" { + t.Fatalf("unexpected title: %s", template.title) + } + if !reflect.DeepEqual(template.emphasize, []string{"verification completed"}) { + t.Fatalf("unexpected emphasize words: %#v", template.emphasize) + } + + expectedFields := []messageField{ + {label: "Name", value: "Alice Example"}, + {label: "Email", value: "alice@example.com"}, + } + if !reflect.DeepEqual(template.fields, expectedFields) { + t.Fatalf("unexpected fields: %#v", template.fields) + } +} + +func TestNewContactRequestTemplate_NilRequest(t *testing.T) { + template := newContactRequestTemplate(nil) + if template.title != "New site request received" { + t.Fatalf("unexpected title: %s", template.title) + } +} + +func TestNewContactRequestTemplate_LegacySignupTopicCaseInsensitive(t *testing.T) { + template := newContactRequestTemplate(&model.ContactRequest{ + Name: "Alice Example", + Email: "alice@example.com", + Company: "Acme Inc", + Topic: " SIGNUP_REQUEST ", + }) + if template.title != "New signup completed" { + t.Fatalf("unexpected title: %s", template.title) + } +} diff --git a/api/pkg/model/contactrequest.go b/api/pkg/model/contactrequest.go index 3bdc5133..baf81d16 100644 --- a/api/pkg/model/contactrequest.go +++ b/api/pkg/model/contactrequest.go @@ -16,6 +16,12 @@ type ContactRequest struct { Message string `json:"message"` } +const ( + ContactRequestTopicSiteContact = "site_contact_request" + ContactRequestTopicSignupCompleted = "signup_completed" + ContactRequestTopicAccountVerificationCompleted = "account_verification_completed" +) + // Normalize trims whitespace from all string fields. func (cr *ContactRequest) Normalize() { if cr == nil { diff --git a/api/server/internal/server/accountapiimp/email.go b/api/server/internal/server/accountapiimp/email.go index 2eb128cf..ebbdbe28 100644 --- a/api/server/internal/server/accountapiimp/email.go +++ b/api/server/internal/server/accountapiimp/email.go @@ -44,6 +44,9 @@ func (a *AccountAPI) verify(r *http.Request) http.HandlerFunc { a.logger.Warn("Failed to save account while verifying account", zap.Error(err)) return response.Internal(a.logger, a.Name(), err) } + if err := a.sendAccountVerificationCompletedNotification(&user); err != nil { + a.logger.Warn("Failed to enqueue account verification notification", zap.Error(err), zap.String("email", user.Login)) + } // TODO: Send verification confirmation email return response.Success(a.logger) diff --git a/api/server/internal/server/accountapiimp/signup.go b/api/server/internal/server/accountapiimp/signup.go index 532cf703..1f850bba 100644 --- a/api/server/internal/server/accountapiimp/signup.go +++ b/api/server/internal/server/accountapiimp/signup.go @@ -13,6 +13,7 @@ import ( "github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/db/storable" "github.com/tech/sendico/pkg/merrors" + snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mutil/mzap" @@ -104,10 +105,52 @@ func (a *AccountAPI) signup(r *http.Request) http.HandlerFunc { if err := a.sendWelcomeEmail(newAccount, verificationToken); err != nil { a.logger.Warn("Failed to send welcome email", zap.Error(err), mzap.StorableRef(newAccount)) } + if err := a.sendSignupNotification(newAccount, &sr); err != nil { + a.logger.Warn("Failed to enqueue signup notification", zap.Error(err), zap.String("login", newAccount.Login)) + } return sresponse.SignUp(a.logger, newAccount) } +func (a *AccountAPI) sendSignupNotification(account *model.Account, request *srequest.Signup) error { + if account == nil || request == nil { + return merrors.InvalidArgument("signup notification payload is empty") + } + + signupNotification := &model.ContactRequest{ + Name: accountNotificationName(account), + Email: strings.TrimSpace(account.Login), + Company: strings.TrimSpace(request.Organization.Name), + Topic: model.ContactRequestTopicSignupCompleted, + } + + return a.producer.SendMessage(snotifications.ContactRequest(a.Name(), signupNotification)) +} + +func (a *AccountAPI) sendAccountVerificationCompletedNotification(account *model.Account) error { + if account == nil { + return merrors.InvalidArgument("account verification notification payload is empty", "account") + } + + notification := &model.ContactRequest{ + Name: accountNotificationName(account), + Email: strings.TrimSpace(account.Login), + Topic: model.ContactRequestTopicAccountVerificationCompleted, + } + + return a.producer.SendMessage(snotifications.ContactRequest(a.Name(), notification)) +} + +func accountNotificationName(account *model.Account) string { + if account == nil { + return "" + } + return strings.TrimSpace(strings.Join([]string{ + strings.TrimSpace(account.Name), + strings.TrimSpace(account.LastName), + }, " ")) +} + func (a *AccountAPI) signupAvailability(r *http.Request) http.HandlerFunc { login := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("login"))) if login == "" { diff --git a/api/server/internal/server/siteimp/contact.go b/api/server/internal/server/siteimp/contact.go index ab0ceb9d..50607128 100644 --- a/api/server/internal/server/siteimp/contact.go +++ b/api/server/internal/server/siteimp/contact.go @@ -17,6 +17,9 @@ func (a *SiteAPI) contactRequest(r *http.Request) http.HandlerFunc { return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode contact request payload") } request.Normalize() + if request.Topic == "" { + request.Topic = model.ContactRequestTopicSiteContact + } if err := request.Validate(); err != nil { a.logger.Warn("Contact request validation failed", zap.Error(err)) return response.BadPayload(a.logger, a.Name(), err)