From 760e7af1c2087a5b8f7df453830ddd555d212730 Mon Sep 17 00:00:00 2001 From: Paulo Bernardo Date: Fri, 4 Oct 2024 15:41:00 -0300 Subject: [PATCH 1/4] feat: add support to wpp message order details --- flows/actions/send_wpp_message.go | 133 ++++++++++++++++++++++++++---- flows/msg_wpp.go | 120 +++++++++++++++++++++------ flows/msg_wpp_test.go | 2 + 3 files changed, 215 insertions(+), 40 deletions(-) diff --git a/flows/actions/send_wpp_message.go b/flows/actions/send_wpp_message.go index 9052c4dee..a1fab37b2 100644 --- a/flows/actions/send_wpp_message.go +++ b/flows/actions/send_wpp_message.go @@ -2,6 +2,7 @@ package actions import ( "encoding/json" + "strconv" "github.com/nyaruka/gocommon/urns" "github.com/nyaruka/goflow/assets" @@ -26,20 +27,21 @@ type SendWppMsgAction struct { } type createWppMsgAction struct { - HeaderType string `json:"header_type,omitempty"` - HeaderText string `json:"header_text,omitempty"` - Attachment string `json:"attachment,omitempty"` - Text string `json:"text,omitempty"` - Footer string `json:"footer,omitempty"` - ListItems []flows.ListItems `json:"list_items,omitempty"` - ButtonText string `json:"button_text,omitempty"` - QuickReplies []string `json:"quick_replies,omitempty"` - InteractionType string `json:"interaction_type,omitempty"` - ActionURL string `json:"action_url,omitempty"` - FlowID string `json:"flow_id,omitempty"` - FlowData flows.FlowData `json:"flow_data,omitempty"` - FlowScreen string `json:"flow_screen,omitempty"` - FlowMode string `json:"flow_mode,omitempty"` + HeaderType string `json:"header_type,omitempty"` + HeaderText string `json:"header_text,omitempty"` + Attachment string `json:"attachment,omitempty"` + Text string `json:"text,omitempty"` + Footer string `json:"footer,omitempty"` + ListItems []flows.ListItems `json:"list_items,omitempty"` + ButtonText string `json:"button_text,omitempty"` + QuickReplies []string `json:"quick_replies,omitempty"` + InteractionType string `json:"interaction_type,omitempty"` + ActionURL string `json:"action_url,omitempty"` + FlowID string `json:"flow_id,omitempty"` + FlowData flows.FlowData `json:"flow_data,omitempty"` + FlowScreen string `json:"flow_screen,omitempty"` + FlowMode string `json:"flow_mode,omitempty"` + OrderDetails flows.OrderDetails `json:"order_details,omitempty"` } type Header struct { @@ -65,6 +67,7 @@ func NewSendWppMsg( flowData flows.FlowData, flowScreen string, flowMode string, + orderDetails flows.OrderDetails, allURNs bool) *SendWppMsgAction { return &SendWppMsgAction{ baseAction: newBaseAction(TypeSendWppMsg, uuid), @@ -83,6 +86,7 @@ func NewSendWppMsg( FlowData: flowData, FlowScreen: flowScreen, FlowMode: flowMode, + OrderDetails: orderDetails, }, AllURNs: allURNs, } @@ -142,6 +146,103 @@ func (a *SendWppMsgAction) Execute(run flows.FlowRun, step flows.Step, logModifi } } + orderDetailsMessage := flows.OrderDetailsMessage{} + if a.InteractionType == "order_details" { + evaluatedReferenceID, _ := run.EvaluateTemplate(a.OrderDetails.ReferenceID) + evaluatedOrderItems, _ := run.EvaluateTemplate(a.OrderDetails.Items) + + orderItems := []flows.MessageOrderItem{} + tempOrderItems := []map[string]interface{}{} + err := json.Unmarshal([]byte(evaluatedOrderItems), &tempOrderItems) + if err != nil { + logEvent(events.NewErrorf("error unmarshalling order items: %v", err)) + return nil + } + + evaluatedOrderTax, _ := run.EvaluateTemplate(a.OrderDetails.Tax.Value) + evaluatedOrderTaxDescription, _ := run.EvaluateTemplate(a.OrderDetails.Tax.Description) + + evaluatedOrderShipping, _ := run.EvaluateTemplate(a.OrderDetails.Shipping.Value) + evaluatedOrderShippingDescription, _ := run.EvaluateTemplate(a.OrderDetails.Shipping.Description) + + evaluatedOrderDiscount, _ := run.EvaluateTemplate(a.OrderDetails.Discount.Value) + evaluatedOrderDiscountDescription, _ := run.EvaluateTemplate(a.OrderDetails.Discount.Description) + evaluatedOrderDiscountProgramName, _ := run.EvaluateTemplate(a.OrderDetails.Discount.ProgramName) + + evaluatedOrderPaymentType, _ := run.EvaluateTemplate(a.OrderDetails.PaymentSettings.Type) + evaluatedOrderPaymentLink, _ := run.EvaluateTemplate(a.OrderDetails.PaymentSettings.PaymentLink) + + evaluatedOrderPixKey, _ := run.EvaluateTemplate(a.OrderDetails.PaymentSettings.PixConfig.Key) + evaluatedOrderPixKeyType, _ := run.EvaluateTemplate(a.OrderDetails.PaymentSettings.PixConfig.KeyType) + evaluatedOrderPixMerchantName, _ := run.EvaluateTemplate(a.OrderDetails.PaymentSettings.PixConfig.MerchantName) + evaluatedOrderPixCode, _ := run.EvaluateTemplate(a.OrderDetails.PaymentSettings.PixConfig.Code) + + convertedOrderTax, err := strconv.ParseFloat(evaluatedOrderTax, 64) + if err != nil { + logEvent(events.NewErrorf("error converting order tax %s to int: %v", evaluatedOrderTax, err)) + return nil + } + + convertedOrderShipping, err := strconv.ParseFloat(evaluatedOrderShipping, 64) + if err != nil { + logEvent(events.NewErrorf("error converting order shipping %s to int: %v", evaluatedOrderShipping, err)) + return nil + } + + convertedOrderDiscount, err := strconv.ParseFloat(evaluatedOrderDiscount, 64) + if err != nil { + logEvent(events.NewErrorf("error converting order discount %s to int: %v", evaluatedOrderDiscount, err)) + return nil + } + + subTotalValue := 0 + for _, item := range orderItems { + if item.SaleAmount != 0 { + subTotalValue += item.SaleAmount * item.Quantity + } else { + subTotalValue += item.Amount * item.Quantity + } + } + + taxValue := int(convertedOrderTax * 100) + shippingValue := int(convertedOrderShipping * 100) + discountValue := int(convertedOrderDiscount * 100) + totalValue := subTotalValue + taxValue + shippingValue - discountValue + + orderDetailsMessage = flows.OrderDetailsMessage{ + ReferenceID: evaluatedReferenceID, + PaymentSettings: &flows.OrderPaymentSettings{ + Type: evaluatedOrderPaymentType, + PaymentLink: evaluatedOrderPaymentLink, + PixConfig: &flows.OrderPixConfig{ + Key: evaluatedOrderPixKey, + KeyType: evaluatedOrderPixKeyType, + MerchantName: evaluatedOrderPixMerchantName, + Code: evaluatedOrderPixCode, + }, + }, + TotalAmount: totalValue, + Order: &flows.MessageOrder{ + Items: &orderItems, + Subtotal: subTotalValue, + Tax: &flows.MessageOrderAmountWithDescription{ + Value: taxValue, + Description: evaluatedOrderTaxDescription, + }, + Shipping: &flows.MessageOrderAmountWithDescription{ + Value: shippingValue, + Description: evaluatedOrderShippingDescription, + }, + Discount: &flows.MessageOrderDiscount{ + Value: discountValue, + Description: evaluatedOrderDiscountDescription, + ProgramName: evaluatedOrderDiscountProgramName, + }, + }, + } + + } + destinations := run.Contact().ResolveDestinations(a.AllURNs) for _, dest := range destinations { @@ -150,14 +251,14 @@ func (a *SendWppMsgAction) Execute(run flows.FlowRun, step flows.Step, logModifi channelRef = assets.NewChannelReference(dest.Channel.UUID(), dest.Channel.Name()) } - msg := flows.NewMsgWppOut(dest.URN.URN(), channelRef, a.InteractionType, a.HeaderType, evaluatedHeaderText, evaluatedText, evaluatedFooter, ctaMessage, listMessage, flowMessage, evaluatedAttachments, evaluatedReplyMessage, a.Topic) + msg := flows.NewMsgWppOut(dest.URN.URN(), channelRef, a.InteractionType, a.HeaderType, evaluatedHeaderText, evaluatedText, evaluatedFooter, ctaMessage, listMessage, flowMessage, orderDetailsMessage, evaluatedAttachments, evaluatedReplyMessage, a.Topic) logEvent(events.NewMsgWppCreated(msg)) } // if we couldn't find a destination, create a msg without a URN or channel and it's up to the caller // to handle that as they want if len(destinations) == 0 { - msg := flows.NewMsgWppOut(urns.NilURN, nil, a.InteractionType, a.HeaderType, evaluatedHeaderText, evaluatedText, evaluatedFooter, ctaMessage, listMessage, flowMessage, evaluatedAttachments, evaluatedReplyMessage, flows.NilMsgTopic) + msg := flows.NewMsgWppOut(urns.NilURN, nil, a.InteractionType, a.HeaderType, evaluatedHeaderText, evaluatedText, evaluatedFooter, ctaMessage, listMessage, flowMessage, orderDetailsMessage, evaluatedAttachments, evaluatedReplyMessage, flows.NilMsgTopic) logEvent(events.NewMsgWppCreated(msg)) } diff --git a/flows/msg_wpp.go b/flows/msg_wpp.go index 93e000a1c..90a6bd218 100644 --- a/flows/msg_wpp.go +++ b/flows/msg_wpp.go @@ -10,18 +10,19 @@ import ( type MsgWppOut struct { BaseMsg - InteractionType_ string `json:"interaction_type,omitempty"` - HeaderType_ string `json:"header_type,omitempty"` - HeaderText_ string `json:"header_text,omitempty"` - Text_ string `json:"text,omitempty"` - Footer_ string `json:"footer,omitempty"` - Topic_ MsgTopic `json:"topic,omitempty"` - ListMessage_ ListMessage `json:"list_message,omitempty"` - Attachments_ []utils.Attachment `json:"attachments,omitempty"` - QuickReplies_ []string `json:"quick_replies,omitempty"` - TextLanguage envs.Language `json:"text_language,omitempty"` - CTAMessage_ CTAMessage `json:"cta_message,omitempty"` - FlowMessage_ FlowMessage `json:"flow_message,omitempty"` + InteractionType_ string `json:"interaction_type,omitempty"` + HeaderType_ string `json:"header_type,omitempty"` + HeaderText_ string `json:"header_text,omitempty"` + Text_ string `json:"text,omitempty"` + Footer_ string `json:"footer,omitempty"` + Topic_ MsgTopic `json:"topic,omitempty"` + ListMessage_ ListMessage `json:"list_message,omitempty"` + Attachments_ []utils.Attachment `json:"attachments,omitempty"` + QuickReplies_ []string `json:"quick_replies,omitempty"` + TextLanguage envs.Language `json:"text_language,omitempty"` + CTAMessage_ CTAMessage `json:"cta_message,omitempty"` + FlowMessage_ FlowMessage `json:"flow_message,omitempty"` + OrderDetailsMessage_ OrderDetailsMessage `json:"order_details_message,omitempty"` } type ListMessage struct { @@ -50,24 +51,93 @@ type ListItems struct { UUID string `json:"uuid,omitempty"` } -func NewMsgWppOut(urn urns.URN, channel *assets.ChannelReference, interactionType, headerType, headerText, text, footer string, ctaMessage CTAMessage, listMessage ListMessage, flowMessage FlowMessage, attachments []utils.Attachment, replyButtons []string, topic MsgTopic) *MsgWppOut { +// Message order details structs, with string-like attributes to be evaluated and calculated +type OrderAmountWithDescription struct { + Value string `json:"value,omitempty"` + Description string `json:"description,omitempty"` +} + +type OrderDiscount struct { + Value string `json:"value,omitempty"` + Description string `json:"description,omitempty"` + ProgramName string `json:"program_name,omitempty"` +} + +type OrderPixConfig struct { + Key string `json:"key,omitempty"` + KeyType string `json:"key_type,omitempty"` + MerchantName string `json:"merchant_name,omitempty"` + Code string `json:"code,omitempty"` +} + +type OrderPaymentSettings struct { + Type string `json:"type,omitempty"` + PaymentLink string `json:"payment_link,omitempty"` + PixConfig *OrderPixConfig `json:"pix_config,omitempty"` +} + +type OrderDetails struct { + ReferenceID string `json:"re ference_id"` + Items string `json:"item_list"` + Tax *OrderAmountWithDescription `json:"tax"` + Shipping *OrderAmountWithDescription `json:"shipping"` + Discount *OrderDiscount `json:"discount"` + PaymentSettings *OrderPaymentSettings `json:"payment_settings"` +} + +// Message for order details, with attribute types defined such as int values +type OrderDetailsMessage struct { + ReferenceID string `json:"reference_id,omitempty"` + PaymentSettings *OrderPaymentSettings `json:"payment_settings,omitempty"` + TotalAmount int `json:"total_amount,omitempty"` + Order *MessageOrder `json:"order,omitempty"` +} + +type MessageOrder struct { + Items *[]MessageOrderItem `json:"items,omitempty"` + Subtotal int `json:"subtotal,omitempty"` + Tax *MessageOrderAmountWithDescription `json:"tax,omitempty"` + Shipping *MessageOrderAmountWithDescription `json:"shipping,omitempty"` + Discount *MessageOrderDiscount `json:"discount,omitempty"` +} + +type MessageOrderItem struct { + RetailerID string `json:"retailer_id"` + Name string `json:"name"` + Amount int `json:"amount"` + Quantity int `json:"quantity"` + SaleAmount int `json:"sale_amount,omitempty"` +} +type MessageOrderAmountWithDescription struct { + Value int `json:"value,omitempty"` + Description string `json:"description,omitempty"` +} + +type MessageOrderDiscount struct { + Value int `json:"value,omitempty"` + Description string `json:"description,omitempty"` + ProgramName string `json:"program_name,omitempty"` +} + +func NewMsgWppOut(urn urns.URN, channel *assets.ChannelReference, interactionType, headerType, headerText, text, footer string, ctaMessage CTAMessage, listMessage ListMessage, flowMessage FlowMessage, orderDetailsMessage OrderDetailsMessage, attachments []utils.Attachment, replyButtons []string, topic MsgTopic) *MsgWppOut { return &MsgWppOut{ BaseMsg: BaseMsg{ UUID_: MsgUUID(uuids.New()), URN_: urn, Channel_: channel, }, - HeaderType_: headerType, - InteractionType_: interactionType, - HeaderText_: headerText, - Text_: text, - Footer_: footer, - ListMessage_: listMessage, - Attachments_: attachments, - QuickReplies_: replyButtons, - Topic_: topic, - CTAMessage_: ctaMessage, - FlowMessage_: flowMessage, + HeaderType_: headerType, + InteractionType_: interactionType, + HeaderText_: headerText, + Text_: text, + Footer_: footer, + ListMessage_: listMessage, + Attachments_: attachments, + QuickReplies_: replyButtons, + Topic_: topic, + CTAMessage_: ctaMessage, + FlowMessage_: flowMessage, + OrderDetailsMessage_: orderDetailsMessage, } } @@ -92,3 +162,5 @@ func (m *MsgWppOut) QuickReplies() []string { return m.QuickReplies_ } func (m *MsgWppOut) CTAMessage() CTAMessage { return m.CTAMessage_ } func (m *MsgWppOut) FlowMessage() FlowMessage { return m.FlowMessage_ } + +func (m *MsgWppOut) OrderDetailsMessage() OrderDetailsMessage { return m.OrderDetailsMessage_ } diff --git a/flows/msg_wpp_test.go b/flows/msg_wpp_test.go index d6e393611..f5e6bbb28 100644 --- a/flows/msg_wpp_test.go +++ b/flows/msg_wpp_test.go @@ -28,6 +28,7 @@ func TestMsgWppOut(t *testing.T) { flows.CTAMessage{}, flows.ListMessage{}, flows.FlowMessage{}, + flows.OrderDetailsMessage{}, []utils.Attachment{ utils.Attachment("image/jpeg:https://example.com/test.jpg"), utils.Attachment("audio/mp3:https://example.com/test.mp3"), @@ -49,6 +50,7 @@ func TestMsgWppOut(t *testing.T) { "cta_message": {}, "list_message": {}, "flow_message": {}, + "order_details_message": {}, "attachments": ["image/jpeg:https://example.com/test.jpg", "audio/mp3:https://example.com/test.mp3"], "topic": "agent" }`), marshaled, "JSON mismatch") From 94e43ef72995c2f1a77b74463f757280bec04db5 Mon Sep 17 00:00:00 2001 From: Paulo Bernardo Date: Fri, 4 Oct 2024 16:11:54 -0300 Subject: [PATCH 2/4] fix: order details empty results --- flows/actions/send_wpp_message.go | 32 +++++++++---------- flows/actions/testdata/send_whatsapp_msg.json | 9 ++++-- flows/msg_wpp.go | 20 ++++++------ 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/flows/actions/send_wpp_message.go b/flows/actions/send_wpp_message.go index a1fab37b2..8a23c27a3 100644 --- a/flows/actions/send_wpp_message.go +++ b/flows/actions/send_wpp_message.go @@ -27,21 +27,21 @@ type SendWppMsgAction struct { } type createWppMsgAction struct { - HeaderType string `json:"header_type,omitempty"` - HeaderText string `json:"header_text,omitempty"` - Attachment string `json:"attachment,omitempty"` - Text string `json:"text,omitempty"` - Footer string `json:"footer,omitempty"` - ListItems []flows.ListItems `json:"list_items,omitempty"` - ButtonText string `json:"button_text,omitempty"` - QuickReplies []string `json:"quick_replies,omitempty"` - InteractionType string `json:"interaction_type,omitempty"` - ActionURL string `json:"action_url,omitempty"` - FlowID string `json:"flow_id,omitempty"` - FlowData flows.FlowData `json:"flow_data,omitempty"` - FlowScreen string `json:"flow_screen,omitempty"` - FlowMode string `json:"flow_mode,omitempty"` - OrderDetails flows.OrderDetails `json:"order_details,omitempty"` + HeaderType string `json:"header_type,omitempty"` + HeaderText string `json:"header_text,omitempty"` + Attachment string `json:"attachment,omitempty"` + Text string `json:"text,omitempty"` + Footer string `json:"footer,omitempty"` + ListItems []flows.ListItems `json:"list_items,omitempty"` + ButtonText string `json:"button_text,omitempty"` + QuickReplies []string `json:"quick_replies,omitempty"` + InteractionType string `json:"interaction_type,omitempty"` + ActionURL string `json:"action_url,omitempty"` + FlowID string `json:"flow_id,omitempty"` + FlowData flows.FlowData `json:"flow_data,omitempty"` + FlowScreen string `json:"flow_screen,omitempty"` + FlowMode string `json:"flow_mode,omitempty"` + OrderDetails *flows.OrderDetails `json:"order_details,omitempty"` } type Header struct { @@ -67,7 +67,7 @@ func NewSendWppMsg( flowData flows.FlowData, flowScreen string, flowMode string, - orderDetails flows.OrderDetails, + orderDetails *flows.OrderDetails, allURNs bool) *SendWppMsgAction { return &SendWppMsgAction{ baseAction: newBaseAction(TypeSendWppMsg, uuid), diff --git a/flows/actions/testdata/send_whatsapp_msg.json b/flows/actions/testdata/send_whatsapp_msg.json index b19a4fe0e..efa031a39 100644 --- a/flows/actions/testdata/send_whatsapp_msg.json +++ b/flows/actions/testdata/send_whatsapp_msg.json @@ -63,7 +63,8 @@ "quick_replies": [ "Si", "No" - ] + ], + "order_details_message": {} } } ], @@ -109,7 +110,8 @@ "display_text": "Button text" }, "list_message": {}, - "flow_message": {} + "flow_message": {}, + "order_details_message": {} } } ], @@ -172,7 +174,8 @@ } }, "flow_cta": "Button text" - } + }, + "order_details_message": {} } } ], diff --git a/flows/msg_wpp.go b/flows/msg_wpp.go index 90a6bd218..5e4d49bf8 100644 --- a/flows/msg_wpp.go +++ b/flows/msg_wpp.go @@ -77,12 +77,12 @@ type OrderPaymentSettings struct { } type OrderDetails struct { - ReferenceID string `json:"re ference_id"` - Items string `json:"item_list"` - Tax *OrderAmountWithDescription `json:"tax"` - Shipping *OrderAmountWithDescription `json:"shipping"` - Discount *OrderDiscount `json:"discount"` - PaymentSettings *OrderPaymentSettings `json:"payment_settings"` + ReferenceID string `json:"reference_id,omitempty"` + Items string `json:"item_list,omitempty"` + Tax *OrderAmountWithDescription `json:"tax,omitempty"` + Shipping *OrderAmountWithDescription `json:"shipping,omitempty"` + Discount *OrderDiscount `json:"discount,omitempty"` + PaymentSettings *OrderPaymentSettings `json:"payment_settings,omitempty"` } // Message for order details, with attribute types defined such as int values @@ -102,10 +102,10 @@ type MessageOrder struct { } type MessageOrderItem struct { - RetailerID string `json:"retailer_id"` - Name string `json:"name"` - Amount int `json:"amount"` - Quantity int `json:"quantity"` + RetailerID string `json:"retailer_id,omitempty"` + Name string `json:"name,omitempty"` + Amount int `json:"amount,omitempty"` + Quantity int `json:"quantity,omitempty"` SaleAmount int `json:"sale_amount,omitempty"` } type MessageOrderAmountWithDescription struct { From 8c776552664ac1ce6c06c51b3ad6ca45cfe760a8 Mon Sep 17 00:00:00 2001 From: Paulo Bernardo Date: Fri, 4 Oct 2024 17:57:32 -0300 Subject: [PATCH 3/4] fix: order items evaluation --- flows/actions/send_wpp_message.go | 39 ++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/flows/actions/send_wpp_message.go b/flows/actions/send_wpp_message.go index 8a23c27a3..1604ba0f2 100644 --- a/flows/actions/send_wpp_message.go +++ b/flows/actions/send_wpp_message.go @@ -151,7 +151,11 @@ func (a *SendWppMsgAction) Execute(run flows.FlowRun, step flows.Step, logModifi evaluatedReferenceID, _ := run.EvaluateTemplate(a.OrderDetails.ReferenceID) evaluatedOrderItems, _ := run.EvaluateTemplate(a.OrderDetails.Items) - orderItems := []flows.MessageOrderItem{} + if evaluatedOrderItems == "" { + logEvent(events.NewErrorf("order items evaluated to empty string")) + return nil + } + tempOrderItems := []map[string]interface{}{} err := json.Unmarshal([]byte(evaluatedOrderItems), &tempOrderItems) if err != nil { @@ -159,6 +163,39 @@ func (a *SendWppMsgAction) Execute(run flows.FlowRun, step flows.Step, logModifi return nil } + orderItems := []flows.MessageOrderItem{} + for _, item := range tempOrderItems { + convertedQuantity, err := strconv.ParseFloat(item["quantity"].(string), 64) + if err != nil { + logEvent(events.NewErrorf("error converting order item quantity %s to int: %v", item["quantity"], err)) + return nil + } + + convertedAmount, err := strconv.ParseFloat(item["amount"].(string), 64) + if err != nil { + logEvent(events.NewErrorf("error converting order item amount %s to int: %v", item["amount"], err)) + return nil + } + + orderItem := flows.MessageOrderItem{ + Name: item["name"].(string), + Quantity: int(convertedQuantity), + Amount: int(convertedAmount), + } + + if item["sale_amount"] == nil { + convertedSaleAmount, err := strconv.ParseFloat(item["sale_amount"].(string), 64) + if err != nil { + logEvent(events.NewErrorf("error converting order item sale amount %s to int: %v", item["sale_amount"], err)) + return nil + } + + orderItem.SaleAmount = int(convertedSaleAmount) + } + + orderItems = append(orderItems, orderItem) + } + evaluatedOrderTax, _ := run.EvaluateTemplate(a.OrderDetails.Tax.Value) evaluatedOrderTaxDescription, _ := run.EvaluateTemplate(a.OrderDetails.Tax.Description) From bac165debd9a7929bae127d2e04104858480f5ef Mon Sep 17 00:00:00 2001 From: Paulo Bernardo Date: Wed, 9 Oct 2024 16:54:22 -0300 Subject: [PATCH 4/4] feat: better order details fields parsing and validation --- flows/actions/send_wpp_message.go | 119 +++++++++++++----- flows/actions/testdata/send_whatsapp_msg.json | 116 +++++++++++++++++ flows/msg_wpp.go | 16 ++- 3 files changed, 217 insertions(+), 34 deletions(-) diff --git a/flows/actions/send_wpp_message.go b/flows/actions/send_wpp_message.go index 1604ba0f2..645d0f959 100644 --- a/flows/actions/send_wpp_message.go +++ b/flows/actions/send_wpp_message.go @@ -3,6 +3,7 @@ package actions import ( "encoding/json" "strconv" + "strings" "github.com/nyaruka/gocommon/urns" "github.com/nyaruka/goflow/assets" @@ -163,34 +164,81 @@ func (a *SendWppMsgAction) Execute(run flows.FlowRun, step flows.Step, logModifi return nil } + if len(tempOrderItems) == 0 { + logEvent(events.NewErrorf("order items evaluated to empty array")) + return nil + } + orderItems := []flows.MessageOrderItem{} for _, item := range tempOrderItems { - convertedQuantity, err := strconv.ParseFloat(item["quantity"].(string), 64) - if err != nil { - logEvent(events.NewErrorf("error converting order item quantity %s to int: %v", item["quantity"], err)) + if item["quantity"] == nil { + logEvent(events.NewErrorf("order item quantity is nil")) + return nil + } + convertedQuantity, isFloat := item["quantity"].(float64) + if !isFloat { + logEvent(events.NewErrorf("error reading order item quantity: %v", item["quantity"])) return nil } - convertedAmount, err := strconv.ParseFloat(item["amount"].(string), 64) - if err != nil { - logEvent(events.NewErrorf("error converting order item amount %s to int: %v", item["amount"], err)) + if item["amount"] == nil { + logEvent(events.NewErrorf("order item amount is required")) + return nil + } + var convertedAmount float64 + var convertedOffset float64 + itemAmount, ok := item["amount"].(map[string]interface{}) + if !ok { + logEvent(events.NewErrorf("error reading order item amount: %v", item["amount"])) + return nil + } + convertedAmount, isFloat = itemAmount["value"].(float64) + if !isFloat { + logEvent(events.NewErrorf("error reading order item amount: %v", itemAmount["value"])) + return nil + } + + convertedOffset, isFloat = itemAmount["offset"].(float64) + if !isFloat { + logEvent(events.NewErrorf("error reading order item amount offset: %v", itemAmount["offset"])) return nil } orderItem := flows.MessageOrderItem{ - Name: item["name"].(string), - Quantity: int(convertedQuantity), - Amount: int(convertedAmount), + RetailerID: item["retailer_id"].(string), + Name: item["name"].(string), + Quantity: int(convertedQuantity), + Amount: flows.MessageOrderAmountWithOffset{ + Value: int(convertedAmount), + Offset: int(convertedOffset), + }, } - if item["sale_amount"] == nil { - convertedSaleAmount, err := strconv.ParseFloat(item["sale_amount"].(string), 64) - if err != nil { - logEvent(events.NewErrorf("error converting order item sale amount %s to int: %v", item["sale_amount"], err)) + if item["sale_amount"] != nil { + itemSaleAmount, ok := item["sale_amount"].(map[string]interface{}) + if !ok { + logEvent(events.NewErrorf("error reading order item sale amount: %v", item["sale_amount"])) + return nil + } + convertedSaleAmount, isFloat := itemSaleAmount["value"].(float64) + if !isFloat { + logEvent(events.NewErrorf("error converting order item sale amount %s: %v", itemSaleAmount["value"], err)) return nil } - orderItem.SaleAmount = int(convertedSaleAmount) + convertedSaleAmountOffset, isFloat := itemSaleAmount["offset"].(float64) + if !isFloat { + logEvent(events.NewErrorf("error converting order item sale amount offset %s: %v", itemSaleAmount["offset"], err)) + return nil + } + + if convertedSaleAmount > 0 { + orderItem.SaleAmount = &flows.MessageOrderAmountWithOffset{ + Value: int(convertedSaleAmount), + Offset: int(convertedSaleAmountOffset), + } + } + } orderItems = append(orderItems, orderItem) @@ -214,30 +262,43 @@ func (a *SendWppMsgAction) Execute(run flows.FlowRun, step flows.Step, logModifi evaluatedOrderPixMerchantName, _ := run.EvaluateTemplate(a.OrderDetails.PaymentSettings.PixConfig.MerchantName) evaluatedOrderPixCode, _ := run.EvaluateTemplate(a.OrderDetails.PaymentSettings.PixConfig.Code) - convertedOrderTax, err := strconv.ParseFloat(evaluatedOrderTax, 64) - if err != nil { - logEvent(events.NewErrorf("error converting order tax %s to int: %v", evaluatedOrderTax, err)) - return nil + var convertedOrderTax float64 + var convertedOrderShipping float64 + var convertedOrderDiscount float64 + err = nil + if evaluatedOrderTax != "" { + taxInRealFloatRepresentation := strings.Replace(evaluatedOrderTax, ",", ".", -1) + convertedOrderTax, err = strconv.ParseFloat(taxInRealFloatRepresentation, 64) + if err != nil { + logEvent(events.NewErrorf("error converting order tax %s to int: %v", evaluatedOrderTax, err)) + return nil + } } - convertedOrderShipping, err := strconv.ParseFloat(evaluatedOrderShipping, 64) - if err != nil { - logEvent(events.NewErrorf("error converting order shipping %s to int: %v", evaluatedOrderShipping, err)) - return nil + if evaluatedOrderShipping != "" { + shippingInRealFloatRepresentation := strings.Replace(evaluatedOrderShipping, ",", ".", -1) + convertedOrderShipping, err = strconv.ParseFloat(shippingInRealFloatRepresentation, 64) + if err != nil { + logEvent(events.NewErrorf("error converting order shipping %s to int: %v", evaluatedOrderShipping, err)) + return nil + } } - convertedOrderDiscount, err := strconv.ParseFloat(evaluatedOrderDiscount, 64) - if err != nil { - logEvent(events.NewErrorf("error converting order discount %s to int: %v", evaluatedOrderDiscount, err)) - return nil + if evaluatedOrderDiscount != "" { + discountInRealFloatRepresentation := strings.Replace(evaluatedOrderDiscount, ",", ".", -1) + convertedOrderDiscount, err = strconv.ParseFloat(discountInRealFloatRepresentation, 64) + if err != nil { + logEvent(events.NewErrorf("error converting order discount %s to int: %v", evaluatedOrderDiscount, err)) + return nil + } } subTotalValue := 0 for _, item := range orderItems { - if item.SaleAmount != 0 { - subTotalValue += item.SaleAmount * item.Quantity + if item.SaleAmount != nil && item.SaleAmount.Value > 0 { + subTotalValue += item.SaleAmount.Value * item.Quantity } else { - subTotalValue += item.Amount * item.Quantity + subTotalValue += item.Amount.Value * item.Quantity } } diff --git a/flows/actions/testdata/send_whatsapp_msg.json b/flows/actions/testdata/send_whatsapp_msg.json index efa031a39..2d9fe258b 100644 --- a/flows/actions/testdata/send_whatsapp_msg.json +++ b/flows/actions/testdata/send_whatsapp_msg.json @@ -186,5 +186,121 @@ "waiting_exits": [], "parent_refs": [] } + }, + { + "description": "WhatsApp Order Details message", + "action": { + "type": "send_whatsapp_msg", + "uuid": "c67969d6-f400-4b5e-bb7c-e45115cd3aa4", + "header_type": "media", + "attachment": "image/jpeg:https://foo.bar/image.jpg", + "footer": "footer", + "interaction_type": "order_details", + "text": "Hi there!", + "button_text": "Button text", + "order_details": { + "reference_id": "123456", + "item_list": "[{\"retailer_id\":\"123\",\"name\":\"item 1\",\"quantity\":1,\"amount\":{\"value\":2000,\"offset\":100},\"sale_amount\":{\"value\":1000,\"offset\":100}}]", + "tax": { + "value": "2,99", + "description": "tax" + }, + "shipping": { + "value": "9.99", + "description": "shipping" + }, + "discount": { + "value": "5.99", + "description": "discount" + }, + "payment_settings": { + "type": "digital-goods", + "payment_link": "https://www.foo.bar", + "pix_config": { + "key": "key", + "key_type": "key type", + "merchant_name": "merchant name", + "code": "code" + } + } + } + }, + "localization": {}, + "events": [ + { + "type": "msg_wpp_created", + "created_on": "2018-10-18T14:20:30.000123456Z", + "step_uuid": "59d74b86-3e2f-4a93-aece-b05d2fdcde0c", + "msg": { + "uuid": "9688d21d-95aa-4bed-afc7-f31b35731a3d", + "urn": "tel:+12065551212?channel=57f1078f-88aa-46f4-a59a-948a5739c03d&id=123", + "channel": { + "uuid": "57f1078f-88aa-46f4-a59a-948a5739c03d", + "name": "My Android Phone" + }, + "text": "Hi there!", + "footer": "footer", + "header_type": "media", + "attachments": [ + "image/jpeg:https://foo.bar/image.jpg" + ], + "interaction_type": "order_details", + "cta_message": {}, + "list_message": {}, + "flow_message": {}, + "order_details_message": { + "order": { + "discount": { + "description": "discount", + "value": 599 + }, + "items": [ + { + "amount": { + "offset": 100, + "value": 2000 + }, + "name": "item 1", + "quantity": 1, + "retailer_id": "123", + "sale_amount": { + "offset": 100, + "value": 1000 + } + } + ], + "shipping": { + "description": "shipping", + "value": 999 + }, + "subtotal": 1000, + "tax": { + "description": "tax", + "value": 299 + } + }, + "payment_settings": { + "payment_link": "https://www.foo.bar", + "pix_config": { + "code": "code", + "key": "key", + "key_type": "key type", + "merchant_name": "merchant name" + }, + "type": "digital-goods" + }, + "reference_id": "123456", + "total_amount": 1699 + } + } + } + ], + "inspection": { + "dependencies": [], + "issues": [], + "results": [], + "waiting_exits": [], + "parent_refs": [] + } } ] \ No newline at end of file diff --git a/flows/msg_wpp.go b/flows/msg_wpp.go index 5e4d49bf8..5dcdca6c0 100644 --- a/flows/msg_wpp.go +++ b/flows/msg_wpp.go @@ -102,12 +102,13 @@ type MessageOrder struct { } type MessageOrderItem struct { - RetailerID string `json:"retailer_id,omitempty"` - Name string `json:"name,omitempty"` - Amount int `json:"amount,omitempty"` - Quantity int `json:"quantity,omitempty"` - SaleAmount int `json:"sale_amount,omitempty"` + RetailerID string `json:"retailer_id,omitempty"` + Name string `json:"name,omitempty"` + Quantity int `json:"quantity,omitempty"` + Amount MessageOrderAmountWithOffset `json:"amount,omitempty"` + SaleAmount *MessageOrderAmountWithOffset `json:"sale_amount,omitempty"` } + type MessageOrderAmountWithDescription struct { Value int `json:"value,omitempty"` Description string `json:"description,omitempty"` @@ -119,6 +120,11 @@ type MessageOrderDiscount struct { ProgramName string `json:"program_name,omitempty"` } +type MessageOrderAmountWithOffset struct { + Value int `json:"value"` + Offset int `json:"offset"` +} + func NewMsgWppOut(urn urns.URN, channel *assets.ChannelReference, interactionType, headerType, headerText, text, footer string, ctaMessage CTAMessage, listMessage ListMessage, flowMessage FlowMessage, orderDetailsMessage OrderDetailsMessage, attachments []utils.Attachment, replyButtons []string, topic MsgTopic) *MsgWppOut { return &MsgWppOut{ BaseMsg: BaseMsg{