diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index 8fb57b8..2805652 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -80,7 +80,7 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { return nil, err } - if err := a.Register().Consumer(snotifications.NewSiteRequestProcessor(p.logger, p.onDemoRequest, p.onContactRequest)); err != nil { + if err := a.Register().Consumer(snotifications.NewSiteRequestProcessor(p.logger, p.onDemoRequest, p.onContactRequest, p.onCallRequest)); err != nil { p.logger.Error("Failed to register site request handler", zap.Error(err)) return nil, err } @@ -111,3 +111,15 @@ func (a *NotificationAPI) onContactRequest(ctx context.Context, request *model.C a.logger.Info("Contact request sent via Telegram", zap.String("name", request.Name), zap.String("topic", request.Topic)) return nil } + +func (a *NotificationAPI) onCallRequest(ctx context.Context, request *model.CallRequest) error { + if a.tg == nil { + return merrors.Internal("telegram client is not configured") + } + if err := a.tg.SendCallRequest(ctx, request); err != nil { + a.logger.Warn("Failed to send call request via telegram", zap.Error(err)) + return err + } + a.logger.Info("Call request sent via Telegram", zap.String("phone", request.Phone)) + return nil +} diff --git a/api/notification/internal/server/notificationimp/telegram/call.go b/api/notification/internal/server/notificationimp/telegram/call.go new file mode 100644 index 0000000..7a445c4 --- /dev/null +++ b/api/notification/internal/server/notificationimp/telegram/call.go @@ -0,0 +1,18 @@ +package telegram + +import "github.com/tech/sendico/pkg/model" + +func newCallRequestTemplate(request *model.CallRequest) messageTemplate { + return messageTemplate{ + title: "New call request received", + emphasize: []string{"call request"}, + fields: []messageField{ + {label: "Name", value: request.Name}, + {label: "Phone", value: request.Phone}, + {label: "Email", value: request.Email}, + {label: "Company", value: request.Company}, + {label: "Preferred time", value: request.PreferredTime}, + {label: "Message", value: request.Message}, + }, + } +} diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go index 1fed9b3..aabc0eb 100644 --- a/api/notification/internal/server/notificationimp/telegram/client.go +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -24,6 +24,7 @@ const defaultAPIURL = "https://api.telegram.org" type Client interface { SendDemoRequest(ctx context.Context, request *model.DemoRequest) error SendContactRequest(ctx context.Context, request *model.ContactRequest) error + SendCallRequest(ctx context.Context, request *model.CallRequest) error } type client struct { @@ -161,6 +162,13 @@ func (c *client) SendContactRequest(ctx context.Context, request *model.ContactR return c.sendForm(ctx, newContactRequestTemplate(request)) } +func (c *client) SendCallRequest(ctx context.Context, request *model.CallRequest) error { + if request == nil { + return merrors.InvalidArgument("call request payload is nil", "request") + } + return c.sendForm(ctx, newCallRequestTemplate(request)) +} + func (c *client) sendForm(ctx context.Context, template messageTemplate) error { message := template.Format(c.parseMode) payload := sendMessagePayload{ diff --git a/api/notification/internal/server/notificationimp/telegram/message.go b/api/notification/internal/server/notificationimp/telegram/message.go index e8468dc..8ec7ae9 100644 --- a/api/notification/internal/server/notificationimp/telegram/message.go +++ b/api/notification/internal/server/notificationimp/telegram/message.go @@ -10,8 +10,8 @@ type parseMode string const ( parseModeUnset parseMode = "" - parseModeMarkdown parseMode = "markdown" - parseModeMarkdownV2 parseMode = "markdownV2" + parseModeMarkdown parseMode = "Markdown" + parseModeMarkdownV2 parseMode = "MarkdownV2" parseModeHTML parseMode = "HTML" ) diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go index 47e9ed1..d496272 100644 --- a/api/pkg/messaging/internal/notifications/site/notification.go +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -15,6 +15,7 @@ type SiteRequestNotification struct { requestType gmessaging.SiteRequestEvent_RequestType demoRequest *model.DemoRequest contactRequest *model.ContactRequest + callRequest *model.CallRequest } func (srn *SiteRequestNotification) Serialize() ([]byte, error) { @@ -51,6 +52,20 @@ func (srn *SiteRequestNotification) Serialize() ([]byte, error) { Message: srn.contactRequest.Message, }, } + case gmessaging.SiteRequestEvent_REQUEST_TYPE_CALL: + if srn.callRequest == nil { + return nil, merrors.InvalidArgument("call request payload is empty", "request") + } + msg.Payload = &gmessaging.SiteRequestEvent_Call{ + Call: &gmessaging.SiteCallRequest{ + Name: srn.callRequest.Name, + Phone: srn.callRequest.Phone, + Email: srn.callRequest.Email, + Company: srn.callRequest.Company, + PreferredTime: srn.callRequest.PreferredTime, + Message: srn.callRequest.Message, + }, + } default: return nil, merrors.InvalidArgument("unsupported site request type", "type") } @@ -74,12 +89,17 @@ func NewContactRequestEvent() model.NotificationEvent { return newSiteRequestEvent() } +func NewCallRequestEvent() model.NotificationEvent { + return newSiteRequestEvent() +} + func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging.Envelope { return &SiteRequestNotification{ Envelope: messaging.CreateEnvelope(sender, newSiteRequestEvent()), requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_DEMO, demoRequest: request, contactRequest: nil, + callRequest: nil, } } @@ -89,5 +109,16 @@ func NewContactRequestEnvelope(sender string, request *model.ContactRequest) mes requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_CONTACT, contactRequest: request, demoRequest: nil, + callRequest: nil, + } +} + +func NewCallRequestEnvelope(sender string, request *model.CallRequest) messaging.Envelope { + return &SiteRequestNotification{ + Envelope: messaging.CreateEnvelope(sender, newSiteRequestEvent()), + requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_CALL, + callRequest: request, + demoRequest: nil, + contactRequest: nil, } } diff --git a/api/pkg/messaging/notifications/site/call_request.go b/api/pkg/messaging/notifications/site/call_request.go new file mode 100644 index 0000000..b1d92ad --- /dev/null +++ b/api/pkg/messaging/notifications/site/call_request.go @@ -0,0 +1,11 @@ +package notifications + +import ( + messaging "github.com/tech/sendico/pkg/messaging/envelope" + internalsite "github.com/tech/sendico/pkg/messaging/internal/notifications/site" + "github.com/tech/sendico/pkg/model" +) + +func CallRequest(sender string, request *model.CallRequest) messaging.Envelope { + return internalsite.NewCallRequestEnvelope(sender, request) +} diff --git a/api/pkg/messaging/notifications/site/handler/handler.go b/api/pkg/messaging/notifications/site/handler/handler.go index 4d525a8..54a369c 100644 --- a/api/pkg/messaging/notifications/site/handler/handler.go +++ b/api/pkg/messaging/notifications/site/handler/handler.go @@ -8,3 +8,4 @@ import ( type DemoRequestHandler = func(context.Context, *model.DemoRequest) error type ContactRequestHandler = func(context.Context, *model.ContactRequest) error +type CallRequestHandler = func(context.Context, *model.CallRequest) error diff --git a/api/pkg/messaging/notifications/site/processor.go b/api/pkg/messaging/notifications/site/processor.go index 1cc70f4..5952340 100644 --- a/api/pkg/messaging/notifications/site/processor.go +++ b/api/pkg/messaging/notifications/site/processor.go @@ -18,6 +18,7 @@ type SiteRequestProcessor struct { logger mlogger.Logger demoHandler handler.DemoRequestHandler contactHandler handler.ContactRequestHandler + callHandler handler.CallRequestHandler event model.NotificationEvent } @@ -67,6 +68,25 @@ func (srp *SiteRequestProcessor) Process(ctx context.Context, envelope me.Envelo Message: contact.GetMessage(), } return srp.contactHandler(ctx, request) + case gmessaging.SiteRequestEvent_REQUEST_TYPE_CALL: + if srp.callHandler == nil { + srp.logger.Warn("Call request handler is not configured") + return nil + } + call := msg.GetCall() + if call == nil { + srp.logger.Warn("Call request payload is empty") + return nil + } + request := &model.CallRequest{ + Name: call.GetName(), + Phone: call.GetPhone(), + Email: call.GetEmail(), + Company: call.GetCompany(), + PreferredTime: call.GetPreferredTime(), + Message: call.GetMessage(), + } + return srp.callHandler(ctx, request) default: srp.logger.Warn("Received site request with unsupported type", zap.Any("type", msg.GetType())) return nil @@ -77,11 +97,12 @@ func (srp *SiteRequestProcessor) GetSubject() model.NotificationEvent { return srp.event } -func NewSiteRequestProcessor(logger mlogger.Logger, demo handler.DemoRequestHandler, contact handler.ContactRequestHandler) np.EnvelopeProcessor { +func NewSiteRequestProcessor(logger mlogger.Logger, demo handler.DemoRequestHandler, contact handler.ContactRequestHandler, call handler.CallRequestHandler) np.EnvelopeProcessor { return &SiteRequestProcessor{ logger: logger.Named("site_request_processor"), demoHandler: demo, contactHandler: contact, + callHandler: call, event: internalsite.NewDemoRequestEvent(), } } diff --git a/api/pkg/model/callrequest.go b/api/pkg/model/callrequest.go new file mode 100644 index 0000000..68fee09 --- /dev/null +++ b/api/pkg/model/callrequest.go @@ -0,0 +1,41 @@ +package model + +import ( + "strings" + + "github.com/tech/sendico/pkg/merrors" +) + +// CallRequest represents a request to schedule a call from the marketing site. +type CallRequest struct { + Name string `json:"name"` + Phone string `json:"phone"` + Email string `json:"email"` + Company string `json:"company"` + PreferredTime string `json:"preferredTime"` + Message string `json:"message"` +} + +// Normalize trims whitespace from all string fields. +func (cr *CallRequest) Normalize() { + if cr == nil { + return + } + cr.Name = strings.TrimSpace(cr.Name) + cr.Phone = strings.TrimSpace(cr.Phone) + cr.Email = strings.TrimSpace(cr.Email) + cr.Company = strings.TrimSpace(cr.Company) + cr.PreferredTime = strings.TrimSpace(cr.PreferredTime) + cr.Message = strings.TrimSpace(cr.Message) +} + +// Validate ensures required call request fields are present. +func (cr *CallRequest) Validate() error { + if cr == nil { + return merrors.InvalidArgument("request payload is empty", "request") + } + if cr.Phone == "" { + return merrors.InvalidArgument("phone must not be empty", "request.phone") + } + return nil +} diff --git a/api/proto/site_request.proto b/api/proto/site_request.proto index 9187d17..98db4e6 100644 --- a/api/proto/site_request.proto +++ b/api/proto/site_request.proto @@ -7,6 +7,7 @@ message SiteRequestEvent { REQUEST_TYPE_UNSPECIFIED = 0; REQUEST_TYPE_DEMO = 1; REQUEST_TYPE_CONTACT = 2; + REQUEST_TYPE_CALL = 3; } RequestType type = 1; @@ -14,6 +15,7 @@ message SiteRequestEvent { oneof payload { SiteDemoRequest demo = 2; SiteContactRequest contact = 3; + SiteCallRequest call = 4; } } @@ -34,3 +36,12 @@ message SiteContactRequest { string topic = 5; string message = 6; } + +message SiteCallRequest { + string name = 1; + string phone = 2; + string email = 3; + string company = 4; + string preferred_time = 5; + string message = 6; +} diff --git a/api/server/internal/server/siteimp/call.go b/api/server/internal/server/siteimp/call.go new file mode 100644 index 0000000..2e7a5fd --- /dev/null +++ b/api/server/internal/server/siteimp/call.go @@ -0,0 +1,29 @@ +package siteimp + +import ( + "encoding/json" + "net/http" + + "github.com/tech/sendico/pkg/api/http/response" + snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" +) + +func (a *SiteAPI) callRequest(r *http.Request) http.HandlerFunc { + var request model.CallRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + a.logger.Warn("Failed to decode call request payload", zap.Error(err)) + return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode call request payload") + } + request.Normalize() + if err := request.Validate(); err != nil { + a.logger.Warn("Call request validation failed", zap.Error(err)) + return response.BadPayload(a.logger, a.Name(), err) + } + if err := a.producer.SendMessage(snotifications.CallRequest(a.Name(), &request)); err != nil { + a.logger.Warn("Failed to enqueue call request notification", zap.Error(err)) + return response.Internal(a.logger, a.Name(), err) + } + return a.acceptedQueued() +} diff --git a/api/server/internal/server/siteimp/contact.go b/api/server/internal/server/siteimp/contact.go index 13641bc..ab0ceb9 100644 --- a/api/server/internal/server/siteimp/contact.go +++ b/api/server/internal/server/siteimp/contact.go @@ -25,5 +25,5 @@ func (a *SiteAPI) contactRequest(r *http.Request) http.HandlerFunc { a.logger.Warn("Failed to enqueue contact request notification", zap.Error(err)) return response.Internal(a.logger, a.Name(), err) } - return response.Accepted(a.logger, map[string]string{"status": "queued"}) + return a.acceptedQueued() } diff --git a/api/server/internal/server/siteimp/demo.go b/api/server/internal/server/siteimp/demo.go index 65e54c8..99cb590 100644 --- a/api/server/internal/server/siteimp/demo.go +++ b/api/server/internal/server/siteimp/demo.go @@ -27,5 +27,5 @@ func (a *SiteAPI) demoRequest(r *http.Request) http.HandlerFunc { return response.Internal(a.logger, a.Name(), err) } - return response.Accepted(a.logger, map[string]string{"status": "queued"}) + return a.acceptedQueued() } diff --git a/api/server/internal/server/siteimp/response.go b/api/server/internal/server/siteimp/response.go new file mode 100644 index 0000000..739d9b5 --- /dev/null +++ b/api/server/internal/server/siteimp/response.go @@ -0,0 +1,19 @@ +package siteimp + +import ( + "net/http" + + "github.com/tech/sendico/pkg/api/http/response" +) + +type enqueueResponse struct { + Status string `json:"status"` +} + +func newEnqueueResponse() enqueueResponse { + return enqueueResponse{Status: "queued"} +} + +func (a *SiteAPI) acceptedQueued() http.HandlerFunc { + return response.Accepted(a.logger, newEnqueueResponse()) +} diff --git a/api/server/internal/server/siteimp/service.go b/api/server/internal/server/siteimp/service.go index 7a59c5a..0fd68ef 100644 --- a/api/server/internal/server/siteimp/service.go +++ b/api/server/internal/server/siteimp/service.go @@ -31,5 +31,6 @@ func CreateAPI(a eapi.API) (*SiteAPI, error) { a.Register().Handler(mservice.Site, "/request/demo", api.Post, p.demoRequest) a.Register().Handler(mservice.Site, "/request/contact", api.Post, p.contactRequest) + a.Register().Handler(mservice.Site, "/request/call", api.Post, p.callRequest) return p, nil }