Хотел бы рассказать о своем опыте настройки «YouTrack как HelpDesk».

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

1. Регистрируем обращение, формируем автоответы


Я работаю в компании, которая предоставляет свои услуги банкам. До определенного времени мы использовали самописный HelpDesk и он был убогим. В определенное время встал вопрос о его замене. При этом для сотрудников банков этот переезд должен был быть незаметен/заметен минимально.

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

С регистрацией все просто, добавляем SMTP, в параметрах интеграции с электронной почтой добавляем ящик и правило фильтрации. С автоответом поинтереснее. Изначально у меня было правило «Send notifications to unregistered users» в процессе notifyUnregisteredUsers. Суть его сводилась к правилу «Когда добавлен комментарий — отправить ». Однако это правило подходит нам только для отправки ответа в банк, когда мы его написали сами в комментарии.

В качестве решения было написано такое правило:

rule notification about created issue 

when becomesReported() { 
  .....
if (Customer email != null) {
sendMail(Customer email, "Autoreply:" + " [" + getId() + "] " + issue.summary, "Ваше письмо получено службой технической поддержки компании \"ИМЯ РЕК\", соответствующая задача зарегистрирована под номером: [" + getId() + "]<br>Время регистрации: " + now.format(mediumDateTime) + "<br>В ближайшее время Ваше обращение будет обработано. <br><br>С уважением, .... и т.д.")
}
}

Условие when becomesReported() срабатывает всегда ДО момента ��убликации нового issue. Так решена задача автоответа.
Следующим стал вопрос, как отправить автоответ при добавлении комментария банка (т.е. ответа на ответ суппортера). В качестве решения подправил Send notifications to unregistered users:

when comments.added.isNotEmpty {
......
// Если Last comment author не null, это ответ банка
if (Last comment author != null) {
sendMail(Last comment author, "Autoreply:" + " [" + getId() + "] " + issue.summary, "Ваше письмо получено cлужбой технической поддержки компании \"ИМЯ РЕК\" и добавлено к ранее зарегистрированной задаче под номером: [" + getId() + "]<br>Время регистрации: " + now.format(mediumDateTime) + "<br>В ближайшее время Ваше обращение будет обработано. <br><br>С уважением, cлужба .....");
}
....... 
Last comment author = null;
}

Last comment author поле техническое, оно необходимо для того, чтобы понять кто писал банк или суппортер. В почтовом правиле в пост-обработке мы указали «Last comment author ${from}» что означает «Установить значение поля Last comment author как e-mail отправителя письма».
А в конце правила мы его обязательно ставим как null. Поэтому написал банк — отправить автоответ, иначе не отправлять.

2. Заполнение кастомных полей


Кроме общения письмами, нам нужно было собирать информацию о состоянии задачи, кому принадлежит, сколько писем по задаче ушло. Для этих целей были созданы собственные поля задачи, такие как «Тип», «Критичность», «Количество писем банка», «Полигон», «Релиз», «Дедлайн», «Дата закрытия банком» и прочее.

Добрую половину из них заполняли при помощи Workflow. Ниже пример заполнения дедлайна и определения банка по домену почты:

when becomesReported() { 
  .....
// на ответ банку сутки без учета выходных
issue.deadline = now + 24 hours; 
if (issue.deadline.format(#e) == 6) { 
  issue.deadline = issue.deadline + 48 hours; 
} 
if (issue.deadline.format(#e) == 7) { 
  issue.deadline = issue.deadline + 24 hours; 
}
.....

// Определим и сохраним Банк 
var bank = ""; 
var ce = ""; 
ce = issue.Customer email + "~"; 
bank = ce.substringBetween("@", "~");
if (issue.Банк == null) {
if (bank == "somebank.domain.com") { 
  issue.Банк = {Какой-то банк}; 
}
.......
}
}

Немного поясню код. issue.{что-то} — это кастомное поле задачи. Оно отображается в проекте. А еще можно опускать «issue.» таким образом, issue.Customer email и Customer email это одно и то же. А вот var bank = ""; просто локальная переменная.

Вот правило, которое закрывает задачу самостоятельно:

rule CloseFromBankRequest 
 
when issue.hasTag("Закрывается банком") { 
  if (issue.hasTag("Закрывается банком")) { 
    debug("Нашли тег у задачи" + issue.getId()); 
     
    if (issue.Type == {Bug}) { 
      // Если это инцидент, проверим набор полей на заполнение и пустые заполним дефолтом  
      if (issue.desc == null) { 
        issue.desc = issue.summary; 
      } 
      if (issue.solution == null) { 
        issue.solution = "Закрыта банком самостоятельно"; 
      } 
      issue.ballis = {Банк}; 
      issue.polygon = {Продуктивный}; 
      issue.causer = {Банк}; 
      if (issue.Банк == null) { 
        issue.Банк = {Клиент банка}; 
      } 
      issue.Timer = {Закрыта}; 
       
    } else { 
      if (issue.desc == null) { 
        issue.desc = issue.description; 
      } 
      if (issue.Банк == null) { 
        issue.Банк = {Клиент банка}; 
      } 
      issue.ballis = {Банк}; 
      issue.Timer = {Закрыта}; 
    } 
  } 
}

when issue.hasTag — когда добавили тег.

У меня это правило используется так. Реализован свой web-интерфейс, в котором через api передается список задач банка. Нажав на кнопку «Закрыть» о��ять же через api YouTrack ставится тег. По этому тегу Workflow сам закрывает задачу, сам ставит нужные поля.

3. Еще пару примеров


Уведомление о закрытии задачи:

rule VoteCloseIssues 
 
when issue.Состояние.changed { 
  if (issue.Состояние == {Fixed} && (issue.Type == {Bug} || issue.Type == {Task})) { 
    var preview = "Добрый день. <br><br> Уведомляем Вас, что ранее зарегистрированная задача [" + getId() + "] закрыта. <br>Вы можете повторно открыть задачу, перейдя по ссылке <a href=\"mailto:support@bifit.ua?subject=[" + getId() + "] " + summary + "&body=Укажите комментарий\">переоткрыть</a> или ответив на это письмо. <br><br>Как бы Вы оценили уровень полученной поддержки? <br><a href=\"http://{link_to_web_interface}/report/Issues.php?api=vote&id=" + getId() + "&mark=good\">Хорошо, я доволен</a><br><a href=\"http://{link_to_web_interface}/report/Issues.php?api=vote&id=" + getId() + "&mark=bad\">Плохо, я недоволен</a><br><br>Ниже история переписки:<br><br>"; 
    var i = 0; 
    var author_comment = ""; 
    var subj_comment = "Уведомление о закрытии задачи [" + getId() + "]"; 
    var text_comment = ""; 
    var date_comment = now.format(mediumDateTime); 
    var full_text = ""; 
    while (issue.comments[i].text != null || i < 10) { 
      author_comment = issue.comments[i].author.fullName; 
      date_comment = issue.comments[i].created.format(mediumDateTime); 
      text_comment = wikify(issue.comments[i].text); 
      // Уберем внутренние комменты 
      if (issue.comments[i].permittedGroup == null && issue.comments[i].text != null) { 
        full_text = "<b>" + author_comment + ":</b><br>Дата сообщения: " + date_comment + "<br><blockquote>" + text_comment + "</blockquote><br><hr><br><br>" + full_text; 
      } 
      i = i + 1; 
    } 
    full_text = full_text + "<b>Банк" + ":</b><br>Дата сообщения: " + issue.created.format(mediumDateTime) + "<br><blockquote>" + wikify(issue.description) + "</blockquote><br><hr><br><br>"; 
    full_text = preview + full_text; 
    sendMail(Customer email, subj_comment, full_text); 
  } 
}

Тут немного объясню.

when issue.Состояние.changed
Когда изменили значение поля «Состояние».
if (issue.Состояние == {Fixed} && (issue.Type == {Bug} || issue.Type == {Task}))
Если состояние «Закрыт» и тип задачи «Баг» или «Консультация».
preview
Начало текста письма. Тут уведомление о закрытии задачи, ссылки на оценку (все тот же web-интерфейс и api).
while (issue.comments[i].text != null || i < 10)
Пройдемся по 10-ти последним комментам и добавим автора, текст коммента в full_text (это история переписки)
full_text = full_text + "<b>Банк" + ":</b><br>Дата сообщения: " + issue.created.format(mediumDateTime) + "<br><blockquote>" + wikify(issue.description) + "</blockquote><br><hr><br><br>"; 
В истории самым последним комментарием будет первое обращение банка wikify(issue.description)
sendMail(Customer email, subj_comment, full_text); 
Отправим все это в банк.

Заполним подсистему автоматически, если выбран конкретный тип модуля интеграции:

rule set_subsystem_if_change_gateway 
 
when issue.Модуль интеграции.changed { 
  if (issue.Subsystem != {Модуль интеграции} && issue.Модуль интеграции != null) { 
    issue.Subsystem = {Модуль интеграции}; 
  } 
}

Проверяем, не вышли ли мы за дедлайн (задачи, запускаемые по времени):

schedule rule check deadline 
 
every minute [issue.Timer != {Закрыта} || issue.Состояние == {Fixed}] { 
  if (issue.deadline == null || issue.Состояние == {Fixed} || issue.ballis == {Банк}) { 
    // Если нет дедлайна или задача закрыта или задача на стороне банка и есть тег просрочена, убираем 
    if (issue.hasTag("Просрочена")) { 
      issue.applyCommand("убрать тег Просрочена"); 
    } 
    // иначе, если вышел дедлайн 
  } else if (issue.deadline <= now) { 
    // если есть тег отложена и просрочена, убираем "просрочена" и признак просрочена тоже 
    if (issue.hasTag("Отложена")) { 
      if (issue.hasTag("Просрочена")) { 
        issue.applyCommand("убрать тег Просрочена"); 
        if (issue.isoverdue == {Да}) { 
          issue.isoverdue = null; 
        } 
      } 
      // иначе (не отложена задача) 
    } else { 
      if (!issue.hasTag("Просрочена")) { 
        issue.applyCommand("добавить тег Просрочена"); 
        if (issue.isoverdue == null) { 
          issue.isoverdue = {Да}; 
        } 
      } 
    } 
  } 
}


Если мы забыли остановить задачу и ушли домой, YouTrack это сделает за вас:

schedule rule pause all issues in overtime 
 
daily at 18:15:00 [issue.Timer == {В работе}] { 
  issue.Timer = {Пауза}; 
}


Ну, вот и все. Надеюсь моя публикация когда-нибудь поможет.

Вопросы/замечания пишите в комменты или личные сообщения.