Комментарии

Timoshika
Оставлен: 17.05.2020
Cool
Gridmaniac
Оставлен: Автоматически
Смело оставляйте комментарии, критикуйте, задавайте вопросы

Интеграция с Shopify

Опубликовано: 14.05.2020

Shopify является SaaS eCommerce платформой. Базовые возможности реализуются в облаке, а настройка и управление предоставлены в виде веб-интерфейса.

Для расширения базового функционала используется система приложений.

Приложения

Про виды, их отличия и ограничения можно прочитать здесь.

С точки зрения реализации они отличаются способом аутентификации.

Приложением является любой код, выполняемый во внешней среде, который взаимодействует с платформой. Простыми словами: приложение - это любая программа или веб-приложение, развернутые на нашей стороне, которые подключается к Shopify API и реализуют дополнительный функционал.

API

Существует несколько ключевых API:

  • Admin API - наиболее полный доступ ко всем компонентам магазина;
  • Storefront API - для third-party интеграций (при активации любой разработчик сможет взаимодействовать с публичными данными такого магазина, в плоть до создания своей версии магазина);
  • Marketing activities API - используется для автоматизации и мониторинга жизненного цикла рекламных кампаний.

В данной статье я буду использовать Admin API, так как для моей задачи требовались особые права доступа.

Создание приложения

Для разработки необходимо зарегистрировать партнерский аккаунт, создать магазин и переключить его в режим разработки. Подробнее здесь.

Далее необходимо зарегистрировать новое приложение. На время разработки достаточно создать Private App и получить API Key. В будущем для добавления приложения в публичный стор для распространения или удобства установки достаточно будет зарегистрировать его как публичное, а также заменить аутентификацию на OAuth 2.0.

Выполнение запросов

Для начала покажу как можно выполнить запрос. На протяжение всей статьи я буду использовать именно GraphQL в качестве протокола взаимодействия с Admin API и Go в качестве языка разработки.

func sendQuery(url string, headers map[string]string, query []byte) string {
    req, _ := http.NewRequest("POST", url, bytes.NewBuffer(query))
    
    req.Header.Set("Content-Type", "application/json")
 
    for key, value := range headers {
        req.Header.Set(key, value)
    }
 
    client := &http.Client{}
 
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
 
    defer resp.Body.Close()
 
    body, _ := ioutil.ReadAll(resp.Body)
    return string(body)
}

Это универсальный метод. Я намеренно использовал Content-Type: application/json, а не application/graphql, так как в данном случае проще работать с Variables + сервер поддерживает такой вариант работы.

Теперь мы можем получить к примеру Order по идентификатору со вложенным объектом CustomerJourney и, таким образом, получить реферальный код пользователя, совершившего покупку после перехода по реферальной ссылке. Подробнее про реферальные ссылки можно прочитать здесь.

type GraphQL struct {
    Query string `json:"query"`
}
 
url := "https://" + c.StoreUrl + "/admin/api/2020-04/graphql.json"
 
query := GraphQL {
	Query: `{
		order(id: "` + id + `") {
			name
			customerJourney {
				firstVisit {
					referralCode
				}
				lastVisit {
					referralCode
				}
			}
		}
	}`,
}

serialized, _ := json.Marshal(query)

headers := map[string]string{
	"X-Shopify-Access-Token": c.Token,
}

res := sendQuery(url, headers, serialized)

jsonData := []byte(res)
var data OrderRoot

json.Unmarshal(jsonData, &data)

В качестве значения заголовка X-Shopify-Access-Token необходимо указывать API Key password.

Webhooks

Рассмотрим работу с веб-хуками на основе, реализованной выше.

type WebhookSubscriptionCreate struct {
    Query string `json:"query"`
    Variables WebhookSubscriptionCreateVariables `json:"variables"`
}
 
type WebhookSubscriptionCreateVariables struct {
    Topic string `json:"topic"`
    Subscription WebhookSubscription `json:"webhookSubscription"`
}
 
query := WebhookSubscriptionCreate {
	Query: `
		mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
			webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
				userErrors {
					field
					message
				}
				webhookSubscription {
					id
				}
			}
		}
	`,
	Variables: WebhookSubscriptionCreateVariables{
		Topic: "ORDERS_PAID",
		Subscription: WebhookSubscription{
			CallbackUrl: callbackUrl,
			Format: "JSON",
		},
	},
}

serialized, _ := json.Marshal(query)

headers := map[string]string{
	"X-Shopify-Access-Token": c.Token,
}

sendQuery(url, headers, serialized)

В этот раз мы задействовали мутацию с Variables для передачи более сложных параметров запроса. Выполнение такого запроса активирует веб-хук на событие оплаты заказа. Shopify будет отправлять POST запрос на наш callbackUrl. В ответ на этот запрос наше приложение должно отдавать код 200, иначе хук будет приходить повторно через увеличивающийся интервал вплоть до нескольких дней. Подробнее об интервалах и правилах работы с веб-хуками можно узнать здесь.

Важным моментом является то, что наш endpoint должен работать через HTTPS.

Когда мне нужно протестировать код на доверенном хосте, я настраиваю какой-нибудь домен или поддомен через обратный прокси (например nginx) на свой выделенный IP, таким образом я получаю полноценную поддержку HTTPS.

Для автоматизации настройки веб-хуков в момент запуска приложения я сделал такую проверку через cron.

data := getWebhookSubscriptions(x)
 
var isMaintained bool
// TODO: error handling
webhooksUrl, _ := os.LookupEnv("MODULE_SHOPIFY_WEBHOOKS_URL")

for _, edge := range data.Data.Subscriptions.Edges {
	if edge.Node.CallbackUrl != webhooksUrl ||
		edge.Node.Topic != "ORDERS_PAID" {
		deleteWebhookSubscription(x, edge.Node.Id)
	} else {
		isMaintained = true
	}
}

if !isMaintained {
	createWebhookSubscription(x, webhooksUrl)
}

Обработка входящих запросов может быть такой:

func PropagateWebhook(c echo.Context) error {
    var json map[string]interface{} = map[string]interface{}{}
 
    if err := c.Bind(&json); err != nil {
        return c.JSON(http.StatusInternalServerError, err)
    }
 
    apiId := json["admin_graphql_api_id"]
    totalPrice:= json["subtotal_price"]
    …
    return c.JSON(http.StatusOK, nil)
}

Вариант с NodeJS

Изначально, я разрабатывал приложение на NodeJS, но позднее мне потребовалось его переделать на Go. Для примера код того же запроса на ноде:

import axios from 'axios'
 
export default class {
    /**
     * Represents a GraphQL Client.
     * @constructor
     * @param {string} url - GraphQL server URL.
     * @param {string} headers - Authentication headers etc.
     */
    constructor(url, headers) {
        this.instance = axios.create({
            baseURL: url,
            timeout: 1000,
            headers: {
                //'Content-Type': 'application/graphql',
                'Content-Type': 'application/json',
                ...headers
            }
        })
    }
 
    /** Run GraphQL query against the server. */
    async query(query, variables) {
        const { data } = await this.instance.post('/', JSON.stringify({
            query,
            variables,
        }))
 
        return data
    }
}

Инициализация:

const client = new GraphQL(`${process.env.SHOP_URL}/admin/api/2020-04/graphql.json`, {
    'X-Shopify-Access-Token': process.env.SHOP_ACCESS_TOKEN
})

Код получения заказа:

async function getOrder(id) {
    const { data, errors } = await client.query(`{
        order(id: "${id}") {
            name
            customerJourney {
                firstVisit {
                    referralCode
                }
                lastVisit {
                    referralCode
                }
            }
        }
    }`)
 
    if (errors)
        throw errors[0]
 
    return data
}

Регистрация веб-хука:

async function webhook()
{
    const query = `
        mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
            webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
                userErrors {
                    field
                    message
                }
                webhookSubscription {
                    id
                }
            }
        }
    `
 
    const variables = {
        topic: 'ORDERS_PAID',
        webhookSubscription: {
            callbackUrl: 'https://shopify.gridmaniac.com/webhooks',
            format: 'JSON'
        }
    }
 
    const { data, errors } = await client.query(query, variables)
}

На этом все :)

В рамках данной статьи я рассказал о видах интеграции с Shopify с различными примерами кода из реального приложения.


P.S. Пример верификации веб-хуков оставлю здесь.