Чем мы занимаемся?
Наша команда разрабатывает платформу программной отправки уведомлений посредством REST API на мобильные устройства. В настоящий момент это push уведомления для iOS устройств, а также SMS благодаря интеграции с Twilio). С целью организации процесса поддержки, а также сообщества пользователей, был выбран ряд информационных систем и сервисов (список см. ниже), которые, с нашей точки зрения, позволяли решить поставленную задачу в кратчайшие сроки и с минимальными усилиями.
Задачи
Автоматизировать выполнение следующих действий:
- Pегистрация пользователей в системе учета обращений
- Регистрации пользователей на портале сообщества
- Управление услугами для пользователей
Используемые системы (и их назначение)
Все нижеперечисленные информационные системы и сервисы имеют REST API интерфейс, позволяющий решить поставленные задачи.
- Atlassian Jira Service Desk (система учета обращений позьзователей)
- Atlassian Confluence (информационный портал)
- Slack (сообщество пользователей)
- Paddle (распространение/ лицензирование программного обеспечения)
Описание процесса
В нашем случае нижеперечисленные события являются исчерпывающими:
- При загрузке программного продукта необходимо создать пользователя в системе учета обращений пользователей, а также отправить приглашение на портал сообщества
- При активации коммерческой поддержки необходимо добавить пользователя в соответствующую группу системы обращеня пользователей
- При истечении срока коммерческой подписки необходимо удалить пользователя из соответствующей группы
Реализация
В первую очередь, источником всех вышеперечисленных событий процесса является платформа Paddle, а именно их подсистема Events/ Alerts. При наступлении определенного события, Paddle инициирует POST запрос на заранее определенный URL (нашего сервера).
События, которые мы будем обрабатывать:
- Payment Success (Non-Subscription)
- Subscription Created
- Subscription Cancelled
Запрос со стороны Paddle содержит следующую информацию (содержимое запроса со стороны Paddle может отличаться в зависимости от событий описанных выше, однако интересующие нас элементы остаются неизменными в любом случае):
{
"method": "POST",
"data": {
"alert_id": "XXXXXX",
"alert_name": "subscription_created",
"cancel_url": "https://checkout.paddle.com/subscription/cancel?XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"currency": "USD",
"email": "name@example.com",
"event_time": "2016-08-05 13:26:06",
"next_bill_date": "2017-08-05",
"passthrough": "",
"status": "active",
"subscription_id": "XXXXXX",
"subscription_plan_id": "XXXXXX",
"update_url": "https://checkout.paddle.com/subscription/update?XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"p_signature": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
}
и ожидает в ответ HTTP 200 OK:
{
"http_code": 200,
"redirect_url": "",
"content_type": "text/plain; charset=utf-8",
"total_time": 1.018837
}
В случае, если ответ отличается от HTTP 200 OK, Paddle будет направлять повторый запрос в сторону нашего сервера на протяжение 72-х часов с интервалом раз в час.
В вышеупомянутом запросе со стороны Paddle нас интересуют параметры alert_name и email которые мы будем использовать для регистрации пользователей в наших информационных системах.
Разбираем запросы со стороны Paddle
type Event struct {
AlertName string `form:"alert_name" binding:"required"`
Email string `form:"email" binding:"required"`
Status string `form:"status"`
SubscriptionId int `form:"subscription_id"`
OrderId string `form:"order_id"`
SubscriptionPlanId int `form:"subscription_plan_id"`
Country string `form:"country"`
Fee float32 `form:"fee"`
Currency string `form:"currency"`
Psignature string `form:"p_signature"`
Passthrough string `form:"passthrough"`
PaymentMethod string `form:"payment_method"`
PaymentTax float32 `form:"payment_tax"`
SaleGross float32 `form:"sale_gross"`
Earnings float32 `form:"earnings"`
EventTime string `form:"event_time"`
NextBillDate string `form:"next_bill_date"`
}
func (e *Event) ToFields() (fields log.Fields) {
fields = make(log.Fields)
fields["alert_name"] = e.AlertName
fields["email"] = e.Email
fields["status"] = e.Status
fields["subscription_id"] = e.SubscriptionId
fields["order_id"] = e.OrderId
fields["subscription_plan_id"] = e.SubscriptionPlanId
fields["country"] = e.Country
fields["fee"] = e.Fee
fields["currency"] = e.Currency
fields["p_signature"] = e.Psignature
fields["passthrough"] = e.Passthrough
fields["payment_method"] = e.PaymentMethod
fields["payment_tax"] = e.PaymentTax
fields["sale_gross"] = e.SaleGross
fields["earnings"] = e.Earnings
fields["event_time"] = e.EventTime
fields["next_bill_date"] = e.NextBillDate
return
}
Выполняем маршрутизацию запросов в зависимости от события
func eventPOST(c *gin.Context) {
status := []bool{}
c.Bind(&event)
log.WithFields(event.ToFields()).Info("Processing event")
switch event.AlertName {
case "subscription_created":
status = append(status, InviteToJira(event.Email))
status = append(status, AddToJiraGroup(event.Email))
status = append(status, InviteToSlack(event.Email))
case "payment_succeeded":
status = append(status, InviteToJira(event.Email))
status = append(status, InviteToSlack(event.Email))
case "subscription_cancelled":
status = append(status, RemoveFromJiraGroup(event.Email))
default:
status = append(status, false)
log.WithFields(log.Fields{
"event": event.AlertName,
}).Error("Unknown event")
}
for _, s := range status {
if s == false {
c.String(http.StatusInternalServerError, "not Okn")
return
}
}
c.String(http.StatusOK, "Okn")
return
}
Далее, в зависимости от полученного события, мы будем их соответствующим образом обрабатывать.
Приглашение пользователя в Slack (портал сообщества)
func InviteToSlack(username string) bool {
api := slack.New(Conf.Slack.Token)
err := api.InviteToTeam(Conf.Slack.Team, "", "", username)
if err != nil {
if strings.Contains(err.Error(), "already_in_team") {
log.WithFields(log.Fields{
"user": username,
"slack_team": Conf.Slack.Team,
}).Warn("User already in Slack team")
return true
} else if strings.Contains(err.Error(), "already_invited") {
log.WithFields(log.Fields{
"user": username,
"slack_team": Conf.Slack.Team,
}).Warn("User already invited to Slack")
return true
}
log.Error(err)
return false
}
log.WithFields(log.Fields{
"user": username,
}).Warn("User successfuly invited to Slack")
return true
}
Приглашение/ регистрация пользователя в Jira (система учета обращений позьзователей)
func InviteToJira(username string) bool {
jira := jira.New(Conf.Jira.Host, Conf.Jira.User, Conf.Jira.Password, false)
_, err := jira.GetUser(username)
if err != nil {
if err.Error() == "Not Found" {
ok, err := jira.InviteSdUser(username, Conf.Jira.Invite_uri)
if ok {
log.WithFields(log.Fields{
"user": username,
}).Info("Invited user to JIRA")
return true
} else {
log.WithFields(log.Fields{
"user": username,
"error": err,
}).Error("Invite to JIRA Failed")
return false
}
} else {
log.Error(err)
return false
}
}
log.WithFields(log.Fields{
"user": username,
}).Warn("User already Invited to JIRA")
return true
}
Добавления пользователя в группу Jira (коммерческая поддержка)
func AddToJiraGroup(username string) bool {
jira := jira.New(Conf.Jira.Host, Conf.Jira.User, Conf.Jira.Password, false)
ok, err := jira.AddToGroup(username, Conf.Jira.Invite_group)
if ok {
log.WithFields(log.Fields{
"user": username,
"group": Conf.Jira.Invite_group,
}).Info("Added user to Jira Group")
return true
} else {
log.WithFields(log.Fields{
"user": username,
"group": Conf.Jira.Invite_group,
"error": err,
}).Error("Failed to add user to Jira Group")
return false
}
}
Удаление пользователя из группы Jira (коммерческая поддержка)
func RemoveFromJiraGroup(username string) bool {
jira := jira.New(Conf.Jira.Host, Conf.Jira.User, Conf.Jira.Password, false)
ok, err := jira.RemoveFromGroup(username, Conf.Jira.Invite_group)
if ok {
log.WithFields(log.Fields{
"user": username,
"group": Conf.Jira.Invite_group,
}).Info("Removed user from Jira Group")
return true
} else {
log.WithFields(log.Fields{
"user": username,
"group": Conf.Jira.Invite_group,
"error": err,
}).Error("Failed to remove User from Jira Group")
return false
}
}
Используемые модули (основные)
Заключение
В ближайшие несколько дней исходный код описанного выше решения будет доступен на GitHub: paddle-endpoint.
Благодарю за внимание!
Автор: pavelklymenko