+ contact requests
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
This commit is contained in:
@@ -85,6 +85,11 @@ func CreateAPI(a api.API) (*NotificationAPI, error) {
|
|||||||
return nil, err
|
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
|
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))
|
a.logger.Info("Demo request sent via Telegram", zap.String("name", request.Name), zap.String("organization", request.OrganizationName))
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -24,6 +23,7 @@ const defaultAPIURL = "https://api.telegram.org"
|
|||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
SendDemoRequest(ctx context.Context, request *model.DemoRequest) error
|
SendDemoRequest(ctx context.Context, request *model.DemoRequest) error
|
||||||
|
SendContactRequest(ctx context.Context, request *model.ContactRequest) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
@@ -102,15 +102,7 @@ func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest
|
|||||||
if request == nil {
|
if request == nil {
|
||||||
return merrors.InvalidArgument("demo request payload is nil", "request")
|
return merrors.InvalidArgument("demo request payload is nil", "request")
|
||||||
}
|
}
|
||||||
message := buildMessage(request, c.parseMode)
|
return c.sendForm(ctx, newDemoRequestTemplate(request))
|
||||||
payload := sendMessagePayload{
|
|
||||||
ChatID: c.chatID,
|
|
||||||
Text: message,
|
|
||||||
ParseMode: c.parseMode,
|
|
||||||
ThreadID: c.threadID,
|
|
||||||
DisablePreview: true,
|
|
||||||
}
|
|
||||||
return c.sendMessage(ctx, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) error {
|
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)
|
return fmt.Sprintf("%s/bot%s/sendMessage", c.apiURL, c.botToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMessage(req *model.DemoRequest, parseMode string) string {
|
func (c *client) SendContactRequest(ctx context.Context, request *model.ContactRequest) error {
|
||||||
var builder strings.Builder
|
if request == nil {
|
||||||
builder.WriteString("New demo request received\n")
|
return merrors.InvalidArgument("contact request payload is nil", "request")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
return builder.String()
|
return c.sendForm(ctx, newContactRequestTemplate(request))
|
||||||
}
|
}
|
||||||
|
|
||||||
type valueFormatter func(string) string
|
func (c *client) sendForm(ctx context.Context, template messageTemplate) error {
|
||||||
|
message := template.Format(c.parseMode)
|
||||||
func appendMessageField(builder *strings.Builder, label, value string, formatter valueFormatter) {
|
payload := sendMessagePayload{
|
||||||
value = strings.TrimSpace(value)
|
ChatID: c.chatID,
|
||||||
if value == "" {
|
Text: message,
|
||||||
value = "—"
|
ParseMode: c.parseMode,
|
||||||
} else if formatter != nil {
|
ThreadID: c.threadID,
|
||||||
value = formatter(value)
|
DisablePreview: true,
|
||||||
}
|
}
|
||||||
fmt.Fprintf(builder, "• %s: %s\n", label, value)
|
return c.sendMessage(ctx, payload)
|
||||||
}
|
|
||||||
|
|
||||||
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("<b>%s</b>", 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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<b>%s</b>", 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)
|
||||||
|
}
|
||||||
@@ -44,3 +44,38 @@ func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging
|
|||||||
request: request,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
11
api/pkg/messaging/notifications/site/contact_request.go
Normal file
11
api/pkg/messaging/notifications/site/contact_request.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -7,3 +7,4 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DemoRequestHandler = func(context.Context, *model.DemoRequest) error
|
type DemoRequestHandler = func(context.Context, *model.DemoRequest) error
|
||||||
|
type ContactRequestHandler = func(context.Context, *model.ContactRequest) error
|
||||||
|
|||||||
@@ -48,3 +48,38 @@ func NewDemoRequestProcessor(logger mlogger.Logger, handler handler.DemoRequestH
|
|||||||
event: internalsite.NewDemoRequestEvent(),
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
41
api/pkg/model/contactrequest.go
Normal file
41
api/pkg/model/contactrequest.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
31
api/pkg/model/contactrequest_test.go
Normal file
31
api/pkg/model/contactrequest_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,20 +34,8 @@ func (dr *DemoRequest) Validate() error {
|
|||||||
if dr == nil {
|
if dr == nil {
|
||||||
return merrors.InvalidArgument("request payload is empty", "request")
|
return merrors.InvalidArgument("request payload is empty", "request")
|
||||||
}
|
}
|
||||||
if dr.Name == "" {
|
if (dr.WorkEmail == "") || (dr.Phone == "") {
|
||||||
return merrors.InvalidArgument("name must not be empty", "request.name")
|
return merrors.InvalidArgument("work email or phone must not be empty", "request.workEmail", "request.phone")
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
12
api/proto/contact_request.proto
Normal file
12
api/proto/contact_request.proto
Normal file
@@ -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;
|
||||||
|
}
|
||||||
29
api/server/internal/server/siteimp/contact.go
Normal file
29
api/server/internal/server/siteimp/contact.go
Normal file
@@ -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"})
|
||||||
|
}
|
||||||
31
api/server/internal/server/siteimp/demo.go
Normal file
31
api/server/internal/server/siteimp/demo.go
Normal file
@@ -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"})
|
||||||
|
}
|
||||||
@@ -2,18 +2,12 @@ package siteimp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
api "github.com/tech/sendico/pkg/api/http"
|
api "github.com/tech/sendico/pkg/api/http"
|
||||||
"github.com/tech/sendico/pkg/api/http/response"
|
|
||||||
"github.com/tech/sendico/pkg/messaging"
|
"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/mlogger"
|
||||||
"github.com/tech/sendico/pkg/model"
|
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
eapi "github.com/tech/sendico/server/interface/api"
|
eapi "github.com/tech/sendico/server/interface/api"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SiteAPI struct {
|
type SiteAPI struct {
|
||||||
@@ -29,32 +23,13 @@ func (a *SiteAPI) Finish(_ context.Context) error {
|
|||||||
return nil
|
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) {
|
func CreateAPI(a eapi.API) (*SiteAPI, error) {
|
||||||
p := &SiteAPI{
|
p := &SiteAPI{
|
||||||
logger: a.Logger().Named(mservice.Site),
|
logger: a.Logger().Named(mservice.Site),
|
||||||
producer: a.Register().Messaging().Producer(),
|
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
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user