account state changes #463

Merged
tech merged 1 commits from alert-444 into main 2026-02-10 19:40:40 +00:00
6 changed files with 226 additions and 1 deletions

View File

@@ -1,8 +1,60 @@
package telegram 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 { 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{ return messageTemplate{
title: "New site request received", title: "New site request received",
emphasize: []string{"site request"}, emphasize: []string{"site request"},

View File

@@ -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)
}
}

View File

@@ -16,6 +16,12 @@ type ContactRequest struct {
Message string `json:"message"` Message string `json:"message"`
} }
const (
ContactRequestTopicSiteContact = "site_contact_request"
ContactRequestTopicSignupCompleted = "signup_completed"
ContactRequestTopicAccountVerificationCompleted = "account_verification_completed"
)
// Normalize trims whitespace from all string fields. // Normalize trims whitespace from all string fields.
func (cr *ContactRequest) Normalize() { func (cr *ContactRequest) Normalize() {
if cr == nil { if cr == nil {

View File

@@ -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)) a.logger.Warn("Failed to save account while verifying account", zap.Error(err))
return response.Internal(a.logger, a.Name(), 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 // TODO: Send verification confirmation email
return response.Success(a.logger) return response.Success(a.logger)

View File

@@ -13,6 +13,7 @@ import (
"github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/db/storable" "github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/merrors" "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/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap" "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 { if err := a.sendWelcomeEmail(newAccount, verificationToken); err != nil {
a.logger.Warn("Failed to send welcome email", zap.Error(err), mzap.StorableRef(newAccount)) 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) 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 { func (a *AccountAPI) signupAvailability(r *http.Request) http.HandlerFunc {
login := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("login"))) login := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("login")))
if login == "" { if login == "" {

View File

@@ -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") return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode contact request payload")
} }
request.Normalize() request.Normalize()
if request.Topic == "" {
request.Topic = model.ContactRequestTopicSiteContact
}
if err := request.Validate(); err != nil { if err := request.Validate(); err != nil {
a.logger.Warn("Contact request validation failed", zap.Error(err)) a.logger.Warn("Contact request validation failed", zap.Error(err))
return response.BadPayload(a.logger, a.Name(), err) return response.BadPayload(a.logger, a.Name(), err)