1
0
mirror of synced 2024-11-22 04:26:01 +03:00

Merge pull request #45 from DmitryZagorulko/master

add bot name in channel settings request, add markdown formatting, minor fixes
This commit is contained in:
Alex Lushpai 2018-09-28 17:20:32 +03:00 committed by GitHub
commit 41d57a8b18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 95 additions and 33 deletions

View File

@ -23,6 +23,7 @@ jenkins_test: migrate_test
@echo "==> Running tests (result in test-report.xml)" @echo "==> Running tests (result in test-report.xml)"
@go get -v -u github.com/jstemmer/go-junit-report @go get -v -u github.com/jstemmer/go-junit-report
@go test ./... -v -cpu 2 -race | /go/bin/go-junit-report -set-exit-code > ./test-report.xml @go test ./... -v -cpu 2 -race | /go/bin/go-junit-report -set-exit-code > ./test-report.xml
@go mod tidy
fmt: fmt:
@echo "==> Running gofmt" @echo "==> Running gofmt"

6
go.mod
View File

@ -30,8 +30,8 @@ require (
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae // indirect github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae // indirect
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
github.com/joho/godotenv v1.3.0 // indirect
github.com/json-iterator/go v1.1.5 // indirect github.com/json-iterator/go v1.1.5 // indirect
github.com/jstemmer/go-junit-report v0.0.0-20180614143834-385fac0ced9a // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/kr/pretty v0.1.0 // indirect github.com/kr/pretty v0.1.0 // indirect
github.com/lib/pq v1.0.0 // indirect github.com/lib/pq v1.0.0 // indirect
@ -44,8 +44,8 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pkg/errors v0.8.0 github.com/pkg/errors v0.8.0
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/retailcrm/api-client-go v1.0.6 github.com/retailcrm/api-client-go v1.0.7
github.com/retailcrm/mg-transport-api-client-go v1.1.10 github.com/retailcrm/mg-transport-api-client-go v1.1.11
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf // indirect github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf // indirect
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50 // indirect github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50 // indirect

14
go.sum
View File

@ -62,10 +62,10 @@ github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Ao
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20180614143834-385fac0ced9a h1:2qq552JOlVHGYvqPc9ynBnGPDHeA7p0/QRn2NkrO8vk=
github.com/jstemmer/go-junit-report v0.0.0-20180614143834-385fac0ced9a/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -93,12 +93,10 @@ github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/retailcrm/api-client-go v1.0.6 h1:4Q3e4ve8GOOHIQdq3/wTGqgWuWa1cKMKqmgrTv4FoDU= github.com/retailcrm/api-client-go v1.0.7 h1:j4C2PvPUDP9nAuYWDvJPnYNpkj+LDBgn71kHvxJmSPg=
github.com/retailcrm/api-client-go v1.0.6/go.mod h1:QRoPE2SM6ST7i2g0yEdqm7Iw98y7cYuq3q14Ot+6N8c= github.com/retailcrm/api-client-go v1.0.7/go.mod h1:QRoPE2SM6ST7i2g0yEdqm7Iw98y7cYuq3q14Ot+6N8c=
github.com/retailcrm/mg-transport-api-client-go v1.1.9 h1:ogh5ThoqZJM5v4ZY6CqctUj01pVVHfBLXkrmX+BFjHE= github.com/retailcrm/mg-transport-api-client-go v1.1.11 h1:jAIOKqkjA2r0v/V6lTHYQsD8q0lFpfpqzAffHAJlhCQ=
github.com/retailcrm/mg-transport-api-client-go v1.1.9/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI= github.com/retailcrm/mg-transport-api-client-go v1.1.11/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI=
github.com/retailcrm/mg-transport-api-client-go v1.1.10 h1:RR8S5NA6FPVrF6UVXaLwu/gJyKUg5aUObQ97S98M3Yc=
github.com/retailcrm/mg-transport-api-client-go v1.1.10/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI=
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf h1:6V1qxN6Usn4jy8unvggSJz/NC790tefw8Zdy6OZS5co= github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf h1:6V1qxN6Usn4jy8unvggSJz/NC790tefw8Zdy6OZS5co=
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo=

View File

@ -57,11 +57,17 @@ func addBotHandler(c *gin.Context) {
return return
} }
b.Name = bot.Self.FirstName b.Name = bot.Self.UserName
conn := getConnectionById(b.ConnectionID) conn := getConnectionById(b.ConnectionID)
client := v1.New(conn.MGURL, conn.MGToken) client := v1.New(conn.MGURL, conn.MGToken)
client.Debug = config.Debug
data, status, err := client.ActivateTransportChannel(getChannelSettings()) channelSettings := getChannelSettings()
if b.Name != "" {
channelSettings.Name = "@" + b.Name
}
data, status, err := client.ActivateTransportChannel(channelSettings)
if status != http.StatusCreated { if status != http.StatusCreated {
c.AbortWithStatusJSON(BadRequest("error_activating_channel")) c.AbortWithStatusJSON(BadRequest("error_activating_channel"))
logger.Error(conn.APIURL, status, err.Error(), data) logger.Error(conn.APIURL, status, err.Error(), data)
@ -88,6 +94,7 @@ func deleteBotHandler(c *gin.Context) {
} }
var client = v1.New(conn.MGURL, conn.MGToken) var client = v1.New(conn.MGURL, conn.MGToken)
client.Debug = config.Debug
data, status, err := client.DeactivateTransportChannel(getBotChannelByToken(b.Token)) data, status, err := client.DeactivateTransportChannel(getBotChannelByToken(b.Token))
if status > http.StatusOK { if status > http.StatusOK {
@ -140,7 +147,7 @@ func saveHandler(c *gin.Context) {
if code == http.StatusInternalServerError { if code == http.StatusInternalServerError {
c.Error(err) c.Error(err)
} else { } else {
c.AbortWithStatusJSON(BadRequest(err.Error())) c.AbortWithStatusJSON(code, gin.H{"error": err.Error()})
} }
return return
} }
@ -362,12 +369,18 @@ func updateBots(conn *Connection, hashSettings string) {
bots := conn.getBotsByClientID() bots := conn.getBotsByClientID()
if len(bots) > 0 { if len(bots) > 0 {
client := v1.New(conn.MGURL, conn.MGToken) client := v1.New(conn.MGURL, conn.MGToken)
client.Debug = config.Debug
for _, bot := range bots { for _, bot := range bots {
if bot.ChannelSettingsHash == hashSettings { if bot.ChannelSettingsHash == hashSettings {
continue continue
} }
data, status, err := client.UpdateTransportChannel(getChannelSettings(bot.Channel)) channelSettings := getChannelSettings(bot.Channel)
if bot.Name != "" {
channelSettings.Name = "@" + bot.Name
}
data, status, err := client.UpdateTransportChannel(channelSettings)
if config.Debug { if config.Debug {
logger.Infof( logger.Infof(
"updateChannelsSettings apiURL: %s, ChannelID: %d, Data: %v, Status: %d, err: %v", "updateChannelsSettings apiURL: %s, ChannelID: %d, Data: %v, Status: %d, err: %v",
@ -422,6 +435,7 @@ func telegramWebhookHandler(c *gin.Context) {
} }
var client = v1.New(conn.MGURL, conn.MGToken) var client = v1.New(conn.MGURL, conn.MGToken)
client.Debug = config.Debug
if update.Message != nil { if update.Message != nil {
if update.Message.Text == "" { if update.Message.Text == "" {
@ -580,6 +594,7 @@ func mgWebhookHandler(c *gin.Context) {
return return
} }
bot.Debug = config.Debug
setLocale(b.Lang) setLocale(b.Lang)
switch msg.Type { switch msg.Type {
@ -587,7 +602,7 @@ func mgWebhookHandler(c *gin.Context) {
var mb string var mb string
switch msg.Data.Type { switch msg.Data.Type {
case v1.MsgTypeProduct: case v1.MsgTypeProduct:
mb = fmt.Sprintf("%s\n", msg.Data.Product.Name) mb = fmt.Sprintf("*%s*\n", msg.Data.Product.Name)
if msg.Data.Product.Cost != nil && msg.Data.Product.Cost.Value != 0 { if msg.Data.Product.Cost != nil && msg.Data.Product.Cost.Value != 0 {
mb += fmt.Sprintf( mb += fmt.Sprintf(
@ -624,6 +639,8 @@ func mgWebhookHandler(c *gin.Context) {
m.ReplyToMessageID = qid m.ReplyToMessageID = qid
} }
m.ParseMode = "Markdown"
msgSend, err := bot.Send(m) msgSend, err := bot.Send(m)
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
@ -669,7 +686,7 @@ func mgWebhookHandler(c *gin.Context) {
} }
func getOrderMessage(dataOrder *v1.MessageDataOrder) string { func getOrderMessage(dataOrder *v1.MessageDataOrder) string {
mb := getLocalizedMessage("order") mb := "*" + getLocalizedMessage("order")
if dataOrder.Number != "" { if dataOrder.Number != "" {
mb += " " + dataOrder.Number mb += " " + dataOrder.Number
@ -678,7 +695,7 @@ func getOrderMessage(dataOrder *v1.MessageDataOrder) string {
if dataOrder.Date != "" { if dataOrder.Date != "" {
mb += fmt.Sprintf(" (%s)", dataOrder.Date) mb += fmt.Sprintf(" (%s)", dataOrder.Date)
} }
mb += "\n" mb += "*\n"
if len(dataOrder.Items) > 0 { if len(dataOrder.Items) > 0 {
mb += "\n" mb += "\n"
for k, v := range dataOrder.Items { for k, v := range dataOrder.Items {
@ -691,7 +708,7 @@ func getOrderMessage(dataOrder *v1.MessageDataOrder) string {
if v.Quantity != nil { if v.Quantity != nil {
if v.Quantity.Value != 0 { if v.Quantity.Value != 0 {
mb += fmt.Sprintf( mb += fmt.Sprintf(
" %v", " _%v_",
v.Quantity.Value, v.Quantity.Value,
) )
} }
@ -700,7 +717,7 @@ func getOrderMessage(dataOrder *v1.MessageDataOrder) string {
if v.Price != nil { if v.Price != nil {
if val, ok := currency[strings.ToLower(v.Price.Currency)]; ok { if val, ok := currency[strings.ToLower(v.Price.Currency)]; ok {
mb += fmt.Sprintf( mb += fmt.Sprintf(
" x %s\n", " _x %s_\n",
getLocalizedTemplateMessage( getLocalizedTemplateMessage(
"cost_currency", "cost_currency",
map[string]interface{}{ map[string]interface{}{
@ -719,20 +736,20 @@ func getOrderMessage(dataOrder *v1.MessageDataOrder) string {
if dataOrder.Delivery != nil { if dataOrder.Delivery != nil {
if dataOrder.Delivery.Name != "" { if dataOrder.Delivery.Name != "" {
mb += fmt.Sprintf( mb += fmt.Sprintf(
"\n%s:\n%s", "\n*%s:*\n%s",
getLocalizedMessage("delivery"), getLocalizedMessage("delivery"),
dataOrder.Delivery.Name, dataOrder.Delivery.Name,
) )
} }
if dataOrder.Delivery.Amount != nil { if dataOrder.Delivery.Price != nil {
if val, ok := currency[strings.ToLower(dataOrder.Delivery.Amount.Currency)]; ok && dataOrder.Delivery.Amount.Value != 0 { if val, ok := currency[strings.ToLower(dataOrder.Delivery.Price.Currency)]; ok && dataOrder.Delivery.Price.Value != 0 {
mb += fmt.Sprintf( mb += fmt.Sprintf(
"; %s", "; %s",
getLocalizedTemplateMessage( getLocalizedTemplateMessage(
"cost_currency", "cost_currency",
map[string]interface{}{ map[string]interface{}{
"Amount": dataOrder.Delivery.Amount.Value, "Amount": dataOrder.Delivery.Price.Value,
"Currency": val, "Currency": val,
}, },
), ),
@ -749,7 +766,7 @@ func getOrderMessage(dataOrder *v1.MessageDataOrder) string {
if len(dataOrder.Payments) > 0 { if len(dataOrder.Payments) > 0 {
mb += fmt.Sprintf( mb += fmt.Sprintf(
"\n%s:\n", "\n*%s:*\n",
getLocalizedMessage("payment"), getLocalizedMessage("payment"),
) )
for _, v := range dataOrder.Payments { for _, v := range dataOrder.Payments {

View File

@ -58,6 +58,7 @@ func TestRouting_addBotHandler(t *testing.T) {
ch := v1.Channel{ ch := v1.Channel{
Type: "telegram", Type: "telegram",
Name: "@TestBot",
Settings: v1.ChannelSettings{ Settings: v1.ChannelSettings{
SpamAllowed: false, SpamAllowed: false,
Status: v1.Status{ Status: v1.Status{

View File

@ -59,8 +59,8 @@ func setup() *gin.Engine {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
} }
r := gin.Default() r := gin.New()
r.Use(gin.Recovery())
if config.Debug { if config.Debug {
r.Use(gin.Logger()) r.Use(gin.Logger())
} }

View File

@ -35,6 +35,7 @@ func GenerateToken() string {
func getAPIClient(url, key string) (*v5.Client, error, int) { func getAPIClient(url, key string) (*v5.Client, error, int) {
client := v5.New(url, key) client := v5.New(url, key)
client.Debug = config.Debug
cr, status, e := client.APICredentials() cr, status, e := client.APICredentials()
if e.RuntimeErr != nil { if e.RuntimeErr != nil {

View File

@ -13,9 +13,11 @@ $(document).on("change", "select", function(e) {
$('#save-crm').on("submit", function(e) { $('#save-crm').on("submit", function(e) {
e.preventDefault(); e.preventDefault();
let formData = formDataToObj($(this).serializeArray());
disableForm($(this));
send( send(
$(this).attr('action'), $(this).attr('action'),
formDataToObj($(this).serializeArray()), formData,
function (data) { function (data) {
sessionStorage.setItem("createdMsg", data.message); sessionStorage.setItem("createdMsg", data.message);
@ -28,17 +30,26 @@ $('#save-crm').on("submit", function(e) {
$("#save").on("submit", function(e) { $("#save").on("submit", function(e) {
e.preventDefault(); e.preventDefault();
let formData = formDataToObj($(this).serializeArray());
disableForm($(this));
send( send(
$(this).attr('action'), $(this).attr('action'),
formDataToObj($(this).serializeArray()), formData,
function (data) { function (data) {
M.toast({html: data.message}); M.toast({
html: data.message,
displayLength: 1000,
completeCallback: function(){
enableForm();
}
});
} }
) )
}); });
$("#add-bot").on("submit", function(e) { $("#add-bot").on("submit", function(e) {
e.preventDefault(); e.preventDefault();
disableForm($(this));
send( send(
$(this).attr('action'), $(this).attr('action'),
{ {
@ -53,6 +64,7 @@ $("#add-bot").on("submit", function(e) {
$("#bots tbody").append(getBotTemplate(data)); $("#bots tbody").append(getBotTemplate(data));
$("#token").val(""); $("#token").val("");
$('select').formSelect(); $('select').formSelect();
enableForm();
} }
) )
}); });
@ -61,6 +73,7 @@ $(document).on("click", ".delete-bot", function(e) {
e.preventDefault(); e.preventDefault();
var but = $(this); var but = $(this);
var confirmText = JSON.parse(sessionStorage.getItem("confirmText")); var confirmText = JSON.parse(sessionStorage.getItem("confirmText"));
but.addClass('disabled');
$.confirm({ $.confirm({
title: false, title: false,
@ -90,6 +103,9 @@ $(document).on("click", ".delete-bot", function(e) {
}, },
cancel: { cancel: {
text: confirmText["cancel"], text: confirmText["cancel"],
action: function () {
but.removeClass('disabled');
},
}, },
} }
}); });
@ -103,14 +119,19 @@ function send(url, data, callback) {
success: callback, success: callback,
error: function (res){ error: function (res){
if (res.status >= 400) { if (res.status >= 400) {
M.toast({html: res.responseJSON.error}) M.toast({
html: res.responseJSON.error,
displayLength: 1000,
completeCallback: function(){
enableForm()
}
})
} }
} }
}); });
} }
function getBotTemplate(data) { function getBotTemplate(data) {
// let bot = JSON.parse(data);
tmpl = tmpl =
`<tr> `<tr>
<td>${data.name}</td> <td>${data.name}</td>
@ -180,3 +201,15 @@ $( document ).ready(function() {
}, 1000); }, 1000);
} }
}); });
function disableForm(elem) {
$(document).find('button.btn').addClass('disabled');
elem.find(".material-icons").addClass('animate');
$("form :input").prop("disabled", true);
}
function enableForm() {
$(document).find('button.btn').removeClass('disabled');
$(document).find(".material-icons").removeClass('animate');
$("form :input").prop("disabled", false);
}

View File

@ -137,3 +137,14 @@ main {
.footer-copyright p { .footer-copyright p {
color: #9e9e9e; color: #9e9e9e;
} }
.animate {
transition: all 0.5s ease;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(360deg);
}
}

View File

@ -48,4 +48,4 @@ order: "Order"
delivery: "Delivery" delivery: "Delivery"
payment: "Payment" payment: "Payment"
order_total: "Order total" order_total: "Order total"
cost_currency: "{{.Amount}} {{.Currency}}" cost_currency: "{{.Currency}}{{.Amount}}"