Чем мы занимаемся?
Наша команда разрабатывает платформу программной отправки уведомлений посредством 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 Ok\n") return } } c.String(http.StatusOK, "Ok\n") 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.
Благодарю за внимание!
