Экосистема поддержки. Автоматизация регистрации пользователей средствами Golang

в 10:52, , рубрики: api, confluence, Go, golang, jira, paddle, rest api, slack

Чем мы занимаемся?

Наша команда разрабатывает платформу программной отправки уведомлений посредством REST API на мобильные устройства. В настоящий момент это push уведомления для iOS устройств, а также SMS благодаря интеграции с Twilio). С целью организации процесса поддержки, а также сообщества пользователей, был выбран ряд информационных систем и сервисов (список см. ниже), которые, с нашей точки зрения, позволяли решить поставленную задачу в кратчайшие сроки и с минимальными усилиями.

Задачи

Автоматизировать выполнение следующих действий:

  • Pегистрация пользователей в системе учета обращений
  • Регистрации пользователей на портале сообщества
  • Управление услугами для пользователей

Используемые системы (и их назначение)

Все нижеперечисленные информационные системы и сервисы имеют REST API интерфейс, позволяющий решить поставленные задачи.

  • Atlassian Jira Service Desk (система учета обращений позьзователей)
  • Atlassian Confluence (информационный портал)
  • Slack (сообщество пользователей)
  • Paddle (распространение/ лицензирование программного обеспечения)

Описание процесса

В нашем случае нижеперечисленные события являются исчерпывающими:

  1. При загрузке программного продукта необходимо создать пользователя в системе учета обращений пользователей, а также отправить приглашение на портал сообщества
  2. При активации коммерческой поддержки необходимо добавить пользователя в соответствующую группу системы обращеня пользователей
  3. При истечении срока коммерческой подписки необходимо удалить пользователя из соответствующей группы

Реализация

В первую очередь, источником всех вышеперечисленных событий процесса является платформа 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js