From e08eb742e4f96e6624568577cc3075a206d4e7ae Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 19 Nov 2025 14:42:38 +0100 Subject: [PATCH] + contact requests --- .../server/notificationimp/notification.go | 17 +++ .../server/notificationimp/telegram/client.go | 106 +++--------------- .../notificationimp/telegram/contact.go | 17 +++ .../server/notificationimp/telegram/demo.go | 24 ++++ .../notificationimp/telegram/message.go | 100 +++++++++++++++++ .../notifications/site/notification.go | 35 ++++++ .../notifications/site/contact_request.go | 11 ++ .../notifications/site/handler/handler.go | 1 + .../messaging/notifications/site/processor.go | 35 ++++++ api/pkg/model/contactrequest.go | 41 +++++++ api/pkg/model/contactrequest_test.go | 31 +++++ api/pkg/model/demorequest.go | 16 +-- api/proto/contact_request.proto | 12 ++ api/server/internal/server/siteimp/contact.go | 29 +++++ api/server/internal/server/siteimp/demo.go | 31 +++++ api/server/internal/server/siteimp/service.go | 29 +---- 16 files changed, 403 insertions(+), 132 deletions(-) create mode 100644 api/notification/internal/server/notificationimp/telegram/contact.go create mode 100644 api/notification/internal/server/notificationimp/telegram/demo.go create mode 100644 api/notification/internal/server/notificationimp/telegram/message.go create mode 100644 api/pkg/messaging/notifications/site/contact_request.go create mode 100644 api/pkg/model/contactrequest.go create mode 100644 api/pkg/model/contactrequest_test.go create mode 100644 api/proto/contact_request.proto create mode 100644 api/server/internal/server/siteimp/contact.go create mode 100644 api/server/internal/server/siteimp/demo.go diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index b6a752e..41b3e01 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -85,6 +85,11 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { return nil, err } + if err := a.Register().Consumer(snotifications.NewContactRequestProcessor(p.logger, p.onContactRequest)); err != nil { + p.logger.Error("Failed to register contact request handler", zap.Error(err)) + return nil, err + } + return p, nil } @@ -99,3 +104,15 @@ func (a *NotificationAPI) onDemoRequest(ctx context.Context, request *model.Demo a.logger.Info("Demo request sent via Telegram", zap.String("name", request.Name), zap.String("organization", request.OrganizationName)) return nil } + +func (a *NotificationAPI) onContactRequest(ctx context.Context, request *model.ContactRequest) error { + if a.tg == nil { + return merrors.Internal("telegram client is not configured") + } + if err := a.tg.SendContactRequest(ctx, request); err != nil { + a.logger.Warn("Failed to send contact request via telegram", zap.Error(err)) + return err + } + a.logger.Info("Contact request sent via Telegram", zap.String("name", request.Name), zap.String("topic", request.Topic)) + return nil +} diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go index fda0c4c..8b8e68c 100644 --- a/api/notification/internal/server/notificationimp/telegram/client.go +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "html" "io" "net/http" "os" @@ -24,6 +23,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 } type client struct { @@ -102,15 +102,7 @@ func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest if request == nil { return merrors.InvalidArgument("demo request payload is nil", "request") } - message := buildMessage(request, c.parseMode) - payload := sendMessagePayload{ - ChatID: c.chatID, - Text: message, - ParseMode: c.parseMode, - ThreadID: c.threadID, - DisablePreview: true, - } - return c.sendMessage(ctx, payload) + return c.sendForm(ctx, newDemoRequestTemplate(request)) } func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) error { @@ -162,89 +154,21 @@ func (c *client) endpoint() string { return fmt.Sprintf("%s/bot%s/sendMessage", c.apiURL, c.botToken) } -func buildMessage(req *model.DemoRequest, parseMode string) string { - var builder strings.Builder - builder.WriteString("New demo request received\n") - builder.WriteString("-----------------------------\n") - - formatter := selectValueFormatter(parseMode) - appendMessageField(&builder, "Name", req.Name, formatter) - appendMessageField(&builder, "Organization", req.OrganizationName, formatter) - appendMessageField(&builder, "Phone", req.Phone, formatter) - appendMessageField(&builder, "Work email", req.WorkEmail, formatter) - appendMessageField(&builder, "Payout volume", req.PayoutVolume, formatter) - if strings.TrimSpace(req.Comment) != "" { - appendMessageField(&builder, "Comment", req.Comment, formatter) +func (c *client) SendContactRequest(ctx context.Context, request *model.ContactRequest) error { + if request == nil { + return merrors.InvalidArgument("contact request payload is nil", "request") } - return builder.String() + return c.sendForm(ctx, newContactRequestTemplate(request)) } -type valueFormatter func(string) string - -func appendMessageField(builder *strings.Builder, label, value string, formatter valueFormatter) { - value = strings.TrimSpace(value) - if value == "" { - value = "—" - } else if formatter != nil { - value = formatter(value) +func (c *client) sendForm(ctx context.Context, template messageTemplate) error { + message := template.Format(c.parseMode) + payload := sendMessagePayload{ + ChatID: c.chatID, + Text: message, + ParseMode: c.parseMode, + ThreadID: c.threadID, + DisablePreview: true, } - fmt.Fprintf(builder, "• %s: %s\n", label, value) -} - -func selectValueFormatter(parseMode string) valueFormatter { - switch strings.ToLower(parseMode) { - case "markdown": - return func(value string) string { - return fmt.Sprintf("*%s*", escapeMarkdown(value)) - } - case "markdownv2": - return func(value string) string { - return fmt.Sprintf("*%s*", escapeMarkdownV2(value)) - } - case "html": - return func(value string) string { - return fmt.Sprintf("%s", html.EscapeString(value)) - } - default: - return nil - } -} - -var markdownEscaper = strings.NewReplacer( - "*", "\\*", - "_", "\\_", - "[", "\\[", - "]", "\\]", - "(", "\\(", - ")", "\\)", - "`", "\\`", -) - -var markdownV2Escaper = strings.NewReplacer( - "_", "\\_", - "*", "\\*", - "[", "\\[", - "]", "\\]", - "(", "\\(", - ")", "\\)", - "~", "\\~", - "`", "\\`", - ">", "\\>", - "#", "\\#", - "+", "\\+", - "-", "\\-", - "=", "\\=", - "|", "\\|", - "{", "\\{", - "}", "\\}", - ".", "\\.", - "!", "\\!", -) - -func escapeMarkdown(value string) string { - return markdownEscaper.Replace(value) -} - -func escapeMarkdownV2(value string) string { - return markdownV2Escaper.Replace(value) + return c.sendMessage(ctx, payload) } diff --git a/api/notification/internal/server/notificationimp/telegram/contact.go b/api/notification/internal/server/notificationimp/telegram/contact.go new file mode 100644 index 0000000..7f94750 --- /dev/null +++ b/api/notification/internal/server/notificationimp/telegram/contact.go @@ -0,0 +1,17 @@ +package telegram + +import "github.com/tech/sendico/pkg/model" + +func newContactRequestTemplate(request *model.ContactRequest) messageTemplate { + return messageTemplate{ + title: "New contact request received", + fields: []messageField{ + {label: "Name", value: request.Name}, + {label: "Email", value: request.Email}, + {label: "Phone", value: request.Phone}, + {label: "Company", value: request.Company}, + {label: "Topic", value: request.Topic}, + {label: "Message", value: request.Message}, + }, + } +} diff --git a/api/notification/internal/server/notificationimp/telegram/demo.go b/api/notification/internal/server/notificationimp/telegram/demo.go new file mode 100644 index 0000000..d2afc20 --- /dev/null +++ b/api/notification/internal/server/notificationimp/telegram/demo.go @@ -0,0 +1,24 @@ +package telegram + +import ( + "strings" + + "github.com/tech/sendico/pkg/model" +) + +func newDemoRequestTemplate(request *model.DemoRequest) messageTemplate { + fields := []messageField{ + {label: "Name", value: request.Name}, + {label: "Organization", value: request.OrganizationName}, + {label: "Phone", value: request.Phone}, + {label: "Work email", value: request.WorkEmail}, + {label: "Payout volume", value: request.PayoutVolume}, + } + if strings.TrimSpace(request.Comment) != "" { + fields = append(fields, messageField{label: "Comment", value: request.Comment}) + } + return messageTemplate{ + title: "New demo request received", + fields: fields, + } +} diff --git a/api/notification/internal/server/notificationimp/telegram/message.go b/api/notification/internal/server/notificationimp/telegram/message.go new file mode 100644 index 0000000..db2c515 --- /dev/null +++ b/api/notification/internal/server/notificationimp/telegram/message.go @@ -0,0 +1,100 @@ +package telegram + +import ( + "fmt" + "html" + "strings" +) + +type messageField struct { + label string + value string +} + +type messageTemplate struct { + title string + fields []messageField +} + +func (mt messageTemplate) Format(parseMode string) string { + var builder strings.Builder + builder.WriteString(mt.title) + builder.WriteString("\n") + builder.WriteString("-----------------------------\n") + + formatter := selectValueFormatter(parseMode) + for _, field := range mt.fields { + appendMessageField(&builder, field.label, field.value, formatter) + } + return builder.String() +} + +type valueFormatter func(string) string + +func appendMessageField(builder *strings.Builder, label, value string, formatter valueFormatter) { + value = strings.TrimSpace(value) + if value == "" { + value = "—" + } else if formatter != nil { + value = formatter(value) + } + fmt.Fprintf(builder, "• %s: %s\n", label, value) +} + +func selectValueFormatter(parseMode string) valueFormatter { + switch strings.ToLower(parseMode) { + case "markdown": + return func(value string) string { + return fmt.Sprintf("*%s*", escapeMarkdown(value)) + } + case "markdownv2": + return func(value string) string { + return fmt.Sprintf("*%s*", escapeMarkdownV2(value)) + } + case "html": + return func(value string) string { + return fmt.Sprintf("%s", html.EscapeString(value)) + } + default: + return nil + } +} + +var markdownEscaper = strings.NewReplacer( + "*", "\\*", + "_", "\\_", + "[", "\\[", + "]", "\\]", + "(", "\\(", + ")", "\\)", + "`", "\\`", +) + +var markdownV2Escaper = strings.NewReplacer( + "_", "\\_", + "*", "\\*", + "[", "\\[", + "]", "\\]", + "(", "\\(", + ")", "\\)", + "~", "\\~", + "`", "\\`", + ">", "\\>", + "#", "\\#", + "+", "\\+", + "-", "\\-", + "=", "\\=", + "|", "\\|", + "{", "\\{", + "}", "\\}", + ".", "\\.", + "!", "\\!", +) + +func escapeMarkdown(value string) string { + return markdownEscaper.Replace(value) +} + +func escapeMarkdownV2(value string) string { + return markdownV2Escaper.Replace(value) +} diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go index c0b92ec..9387fc6 100644 --- a/api/pkg/messaging/internal/notifications/site/notification.go +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -44,3 +44,38 @@ func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging request: request, } } + +type ContactRequestNotification struct { + messaging.Envelope + request *model.ContactRequest +} + +func (crn *ContactRequestNotification) Serialize() ([]byte, error) { + if crn.request == nil { + return nil, merrors.InvalidArgument("contact request payload is empty", "request") + } + msg := gmessaging.ContactRequestEvent{ + Name: crn.request.Name, + Email: crn.request.Email, + Phone: crn.request.Phone, + Company: crn.request.Company, + Topic: crn.request.Topic, + Message: crn.request.Message, + } + data, err := proto.Marshal(&msg) + if err != nil { + return nil, err + } + return crn.Envelope.Wrap(data) +} + +func NewContactRequestEvent() model.NotificationEvent { + return model.NewNotification(mservice.Site, nm.NACreated) +} + +func NewContactRequestEnvelope(sender string, request *model.ContactRequest) messaging.Envelope { + return &ContactRequestNotification{ + Envelope: messaging.CreateEnvelope(sender, NewContactRequestEvent()), + request: request, + } +} diff --git a/api/pkg/messaging/notifications/site/contact_request.go b/api/pkg/messaging/notifications/site/contact_request.go new file mode 100644 index 0000000..fbf8c6d --- /dev/null +++ b/api/pkg/messaging/notifications/site/contact_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 ContactRequest(sender string, request *model.ContactRequest) messaging.Envelope { + return internalsite.NewContactRequestEnvelope(sender, request) +} diff --git a/api/pkg/messaging/notifications/site/handler/handler.go b/api/pkg/messaging/notifications/site/handler/handler.go index ef1b3ef..4d525a8 100644 --- a/api/pkg/messaging/notifications/site/handler/handler.go +++ b/api/pkg/messaging/notifications/site/handler/handler.go @@ -7,3 +7,4 @@ import ( ) type DemoRequestHandler = func(context.Context, *model.DemoRequest) error +type ContactRequestHandler = func(context.Context, *model.ContactRequest) error diff --git a/api/pkg/messaging/notifications/site/processor.go b/api/pkg/messaging/notifications/site/processor.go index 33e5eab..a58ea29 100644 --- a/api/pkg/messaging/notifications/site/processor.go +++ b/api/pkg/messaging/notifications/site/processor.go @@ -48,3 +48,38 @@ func NewDemoRequestProcessor(logger mlogger.Logger, handler handler.DemoRequestH event: internalsite.NewDemoRequestEvent(), } } + +type ContactRequestProcessor struct { + logger mlogger.Logger + handler handler.ContactRequestHandler + event model.NotificationEvent +} + +func (crp *ContactRequestProcessor) Process(ctx context.Context, envelope me.Envelope) error { + var msg gmessaging.ContactRequestEvent + if err := proto.Unmarshal(envelope.GetData(), &msg); err != nil { + crp.logger.Warn("Failed to decode contact request envelope", zap.Error(err), zap.String("topic", crp.event.ToString())) + return err + } + request := &model.ContactRequest{ + Name: msg.GetName(), + Email: msg.GetEmail(), + Phone: msg.GetPhone(), + Company: msg.GetCompany(), + Topic: msg.GetTopic(), + Message: msg.GetMessage(), + } + return crp.handler(ctx, request) +} + +func (crp *ContactRequestProcessor) GetSubject() model.NotificationEvent { + return crp.event +} + +func NewContactRequestProcessor(logger mlogger.Logger, handler handler.ContactRequestHandler) np.EnvelopeProcessor { + return &ContactRequestProcessor{ + logger: logger.Named("contact_request_processor"), + handler: handler, + event: internalsite.NewContactRequestEvent(), + } +} diff --git a/api/pkg/model/contactrequest.go b/api/pkg/model/contactrequest.go new file mode 100644 index 0000000..30252ab --- /dev/null +++ b/api/pkg/model/contactrequest.go @@ -0,0 +1,41 @@ +package model + +import ( + "strings" + + "github.com/tech/sendico/pkg/merrors" +) + +// ContactRequest represents a contact form submission from the marketing site. +type ContactRequest struct { + Name string `json:"name"` + Email string `json:"email"` + Phone string `json:"phone"` + Company string `json:"company"` + Topic string `json:"topic"` + Message string `json:"message"` +} + +// Normalize trims whitespace from all string fields. +func (cr *ContactRequest) Normalize() { + if cr == nil { + return + } + cr.Name = strings.TrimSpace(cr.Name) + cr.Email = strings.TrimSpace(cr.Email) + cr.Phone = strings.TrimSpace(cr.Phone) + cr.Company = strings.TrimSpace(cr.Company) + cr.Topic = strings.TrimSpace(cr.Topic) + cr.Message = strings.TrimSpace(cr.Message) +} + +// Validate ensures required contact request fields are present. +func (cr *ContactRequest) Validate() error { + if cr == nil { + return merrors.InvalidArgument("request payload is empty", "request") + } + if (cr.Email == "") || (cr.Phone == "") { + return merrors.InvalidArgument("email or phone must not be empty", "request.email", "request.phone") + } + return nil +} diff --git a/api/pkg/model/contactrequest_test.go b/api/pkg/model/contactrequest_test.go new file mode 100644 index 0000000..0dc49cc --- /dev/null +++ b/api/pkg/model/contactrequest_test.go @@ -0,0 +1,31 @@ +package model + +import "testing" + +func TestContactRequestNormalizeAndValidate(t *testing.T) { + req := &ContactRequest{ + Name: " Alice ", + Email: " alice@example.com ", + Phone: " +1 234 ", + Company: " Sendico ", + Topic: " General question ", + Message: " Hello team ", + } + + req.Normalize() + if err := req.Validate(); err != nil { + t.Fatalf("expected request to be valid, got error: %v", err) + } + + if req.Name != "Alice" || req.Email != "alice@example.com" || req.Phone != "+1 234" || req.Company != "Sendico" || req.Topic != "General question" || req.Message != "Hello team" { + t.Fatalf("normalize failed: %+v", req) + } +} + +func TestContactRequestValidateMissing(t *testing.T) { + req := &ContactRequest{} + req.Normalize() + if err := req.Validate(); err == nil { + t.Fatalf("expected validation error for empty request") + } +} diff --git a/api/pkg/model/demorequest.go b/api/pkg/model/demorequest.go index c7c7e54..709447c 100644 --- a/api/pkg/model/demorequest.go +++ b/api/pkg/model/demorequest.go @@ -34,20 +34,8 @@ func (dr *DemoRequest) Validate() error { if dr == nil { return merrors.InvalidArgument("request payload is empty", "request") } - if dr.Name == "" { - return merrors.InvalidArgument("name must not be empty", "request.name") - } - if dr.OrganizationName == "" { - return merrors.InvalidArgument("organization name must not be empty", "request.organizationName") - } - if dr.Phone == "" { - return merrors.InvalidArgument("phone must not be empty", "request.phone") - } - if dr.WorkEmail == "" { - return merrors.InvalidArgument("work email must not be empty", "request.workEmail") - } - if dr.PayoutVolume == "" { - return merrors.InvalidArgument("payout volume must not be empty", "request.payoutVolume") + if (dr.WorkEmail == "") || (dr.Phone == "") { + return merrors.InvalidArgument("work email or phone must not be empty", "request.workEmail", "request.phone") } return nil } diff --git a/api/proto/contact_request.proto b/api/proto/contact_request.proto new file mode 100644 index 0000000..08f219a --- /dev/null +++ b/api/proto/contact_request.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +option go_package = "github.com/tech/sendico/pkg/generated/gmessaging"; + +message ContactRequestEvent { + string Name = 1; + string Email = 2; + string Phone = 3; + string Company = 4; + string Topic = 5; + string Message = 6; +} diff --git a/api/server/internal/server/siteimp/contact.go b/api/server/internal/server/siteimp/contact.go new file mode 100644 index 0000000..13641bc --- /dev/null +++ b/api/server/internal/server/siteimp/contact.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) contactRequest(r *http.Request) http.HandlerFunc { + var request model.ContactRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + a.logger.Warn("Failed to decode contact request payload", zap.Error(err)) + return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode contact request payload") + } + request.Normalize() + if err := request.Validate(); err != nil { + a.logger.Warn("Contact request validation failed", zap.Error(err)) + return response.BadPayload(a.logger, a.Name(), err) + } + if err := a.producer.SendMessage(snotifications.ContactRequest(a.Name(), &request)); err != nil { + 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"}) +} diff --git a/api/server/internal/server/siteimp/demo.go b/api/server/internal/server/siteimp/demo.go new file mode 100644 index 0000000..65e54c8 --- /dev/null +++ b/api/server/internal/server/siteimp/demo.go @@ -0,0 +1,31 @@ +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) demoRequest(r *http.Request) http.HandlerFunc { + var request model.DemoRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + a.logger.Warn("Failed to decode demo request payload", zap.Error(err)) + return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode demo request payload") + } + request.Normalize() + if err := request.Validate(); err != nil { + a.logger.Warn("Demo request validation failed", zap.Error(err)) + return response.BadPayload(a.logger, a.Name(), err) + } + + if err := a.producer.SendMessage(snotifications.DemoRequest(a.Name(), &request)); err != nil { + a.logger.Warn("Failed to enqueue demo request notification", zap.Error(err)) + return response.Internal(a.logger, a.Name(), err) + } + + return response.Accepted(a.logger, map[string]string{"status": "queued"}) +} diff --git a/api/server/internal/server/siteimp/service.go b/api/server/internal/server/siteimp/service.go index fce8434..7a59c5a 100644 --- a/api/server/internal/server/siteimp/service.go +++ b/api/server/internal/server/siteimp/service.go @@ -2,18 +2,12 @@ package siteimp import ( "context" - "encoding/json" - "net/http" api "github.com/tech/sendico/pkg/api/http" - "github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/messaging" - snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" "github.com/tech/sendico/pkg/mlogger" - "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" eapi "github.com/tech/sendico/server/interface/api" - "go.uber.org/zap" ) type SiteAPI struct { @@ -29,32 +23,13 @@ func (a *SiteAPI) Finish(_ context.Context) error { return nil } -func (a *SiteAPI) demoRequest(r *http.Request) http.HandlerFunc { - var request model.DemoRequest - if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - a.logger.Warn("Failed to decode demo request payload", zap.Error(err)) - return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode demo request payload") - } - request.Normalize() - if err := request.Validate(); err != nil { - a.logger.Warn("Demo request validation failed", zap.Error(err)) - return response.BadPayload(a.logger, a.Name(), err) - } - - if err := a.producer.SendMessage(snotifications.DemoRequest(a.Name(), &request)); err != nil { - a.logger.Warn("Failed to enqueue demo request notification", zap.Error(err)) - return response.Internal(a.logger, a.Name(), err) - } - - return response.Accepted(a.logger, map[string]string{"status": "queued"}) -} - func CreateAPI(a eapi.API) (*SiteAPI, error) { p := &SiteAPI{ logger: a.Logger().Named(mservice.Site), producer: a.Register().Messaging().Producer(), } - a.Register().Handler(mservice.Site, "/demo/request", api.Post, p.demoRequest) + a.Register().Handler(mservice.Site, "/request/demo", api.Post, p.demoRequest) + a.Register().Handler(mservice.Site, "/request/contact", api.Post, p.contactRequest) return p, nil }