diff --git a/package-lock.json b/package-lock.json index 511230f..c7a18e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@types/google-apps-script": "^1.0.85", "gas-webpack-plugin": "^2.6.0", "graphql-tag": "^2.12.6", - "shopify-admin-api-typings": "^1.2.2", + "shopify-admin-api-typings": "github:beepmill/shopify-admin-api-typings", "ts-loader": "^9.5.1", "webpack": "^5.96.1", "webpack-cli": "^5.1.4" @@ -1474,9 +1474,8 @@ } }, "node_modules/shopify-admin-api-typings": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/shopify-admin-api-typings/-/shopify-admin-api-typings-1.2.2.tgz", - "integrity": "sha512-n9NSBPBKX+TSfAr8ibpXKoGdHCUCY6WBKV2FhD9xvjGG3DIqqUylqhC3OXKiWcrTQuyb3WnaRCvl16i3uhJMiQ==", + "version": "2.0.0", + "resolved": "git+ssh://git@github.com/beepmill/shopify-admin-api-typings.git#e8ba51acff0a4e66c31ee61d62f5244ad4f4233a", "dev": true, "license": "MIT", "peerDependencies": { diff --git a/package.json b/package.json index 5d67aba..aeaae4d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@types/google-apps-script": "^1.0.85", "gas-webpack-plugin": "^2.6.0", "graphql-tag": "^2.12.6", - "shopify-admin-api-typings": "^1.2.2", + "shopify-admin-api-typings": "github:beepmill/shopify-admin-api-typings", "ts-loader": "^9.5.1", "webpack": "^5.96.1", "webpack-cli": "^5.1.4" diff --git a/product_inventory.code-workspace b/product_inventory.code-workspace index bf7695b..1c2f936 100644 --- a/product_inventory.code-workspace +++ b/product_inventory.code-workspace @@ -1,8 +1,11 @@ { "folders": [ - { - "path": "." - } + { + "path": "." + }, + { + "path": "../shopify-admin-api-typings" + } ], "settings": { "prettier.semi": false, diff --git a/src/Product.ts b/src/Product.ts index b8f78ff..4e8fb09 100644 --- a/src/Product.ts +++ b/src/Product.ts @@ -30,7 +30,7 @@ export class Product { type: string = "" weight_grams: number = 0 photos: string = "" - shopify_product: ShopifyProduct + shopify_product: shopify.Product shopify_default_variant_id: string = "" shopify_default_option_id: string = "" shopify_default_option_value_id: string = "" @@ -73,52 +73,17 @@ export class Product { } } - MatchToShopifyProduct(shop: Shop) { - /* if (this.shopify_id.startsWith("gid://shopify/Product/")) { - return - } */ - - let query = new ShopifyProductsQuery("sku:" + this.sku, ["id", "title"]) - console.log(query.JSON) - let response = shop.shopifyGraphQLAPI(query.JSON) - console.log(response) - let productsResponse = new ShopifyProductsResponse(response.content) - if (productsResponse.products.edges.length <= 0) { - console.log("no products matched") + MatchToShopifyProduct(shop: Shop): shopify.Product { + let product = shop.GetProductBySku(this.sku) + if (product == undefined || product.id == undefined || product.id == "") { + console.log("MatchToShopifyProduct: no product matched") return } - if (productsResponse.products.edges.length > 1) { - console.log("more than one product matched") - return - } - this.shopify_product = productsResponse.products.edges[0].node + this.shopify_product = product this.shopify_id = this.shopify_product.id.toString() - this.shopify_default_variant_id = - productsResponse.products.edges[0].node.variants.nodes[0].id - console.log(JSON.stringify(productsResponse, null, 2)) - console.log(JSON.stringify(productsResponse.products, null, 2)) - console.log(JSON.stringify(productsResponse.products.edges[0], null, 2)) - console.log( - JSON.stringify(productsResponse.products.edges[0].node, null, 2) - ) - console.log( - JSON.stringify( - productsResponse.products.edges[0].node.options[0], - null, - 2 - ) - ) - console.log( - JSON.stringify( - productsResponse.products.edges[0].node.options[0].id, - null, - 2 - ) - ) - this.shopify_default_option_id = - productsResponse.products.edges[0].node.options[0].id - this.shopify_default_option_value_id = - productsResponse.products.edges[0].node.options[0].optionValues[0].id + this.shopify_default_variant_id = product.variants.nodes[0].id + this.shopify_default_option_id = product.options[0].id + this.shopify_default_option_value_id = product.options[0].optionValues[0].id let productInventorySheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory") let row = getRowByColumnValue("product_inventory", "sku", this.sku) @@ -134,7 +99,7 @@ export class Product { if (this.shopify_id != "") { sps.id = this.shopify_id } - if (this.shopify_status.match('DRAFT|ACTIVE|ARCHIVED')) { + if (this.shopify_status.match("DRAFT|ACTIVE|ARCHIVED")) { sps.status = this.shopify_status } sps.productType = this.product_type @@ -151,26 +116,50 @@ export class Product { variant.price = this.price sps.variants.push(variant) console.log("ToShopifyProductSet:\n" + JSON.stringify(sps, null, 2)) - //TODO: add sales channels //TODO: add initial inventory return sps } UpdateShopifyProduct(shop: Shop) { + console.log("UpdateShopifyProduct()") + var newProduct = false + let config = new Config() this.MatchToShopifyProduct(shop) + if (this.shopify_id == "") { + console.log( + "UpdateShopifyProduct: no product matched, this will be a new product" + ) + newProduct = true + } + console.log("UpdateShopifyProduct: calling productSet") let sps = this.ToShopifyProductSet() console.log("sps: " + JSON.stringify(sps)) let query = new ShopifyProductSetQuery(sps) - console.log(JSON.stringify(query)) let response = shop.shopifyGraphQLAPI(query.JSON) - console.log(JSON.stringify(response, null, 2)) + let product = response.content.data.productSet.product + this.shopify_id = product.id + if (newProduct) { + console.log("UpdateShopifyProduct: setting defaults on new product") + let item: shopify.InventoryItem + do { + console.log("UpdateShopifyProduct: attempting to get inventory item") + item = shop.GetInventoryItemBySku(this.sku) + } while (item.id == "") + console.log("UpdateShopifyProduct: setting defaults on inventory item") + shop.SetInventoryItemDefaults(item, config) + console.log("UpdateShopifyProduct: publishing to online store") + response = this.PublishToShopifyOnlineStore(shop) + console.log("UpdateShopifyProduct: adjusting inventory item quantity") + shop.UpdateInventoryItemQuantity(item, 1, config) + console.log(JSON.stringify(response, null, 2)) + } + // Update to include new changes + console.log("UpdateShopifyProduct: updating inventory match") this.MatchToShopifyProduct(shop) - response = this.PublishToShopifyOnlineStore(shop) - this.MatchToShopifyProduct(shop) - console.log(JSON.stringify(response, null, 2)) } PublishToShopifyOnlineStore(shop: Shop) { + console.log("PublishToShopifyOnlineStore") let config = new Config() let query = /* GraphQL */ ` mutation publishablePublish($id: ID!, $input: [PublicationInput!]!) { @@ -191,12 +180,13 @@ export class Product { message } } - }` - let variables = { - "id": this.shopify_id, - "input": { - "publicationId": config.shopifyStorePublicationId } + ` + let variables = { + id: this.shopify_id, + input: { + publicationId: config.shopifyStorePublicationId, + }, } let j = `{ "query": ${formatGqlForJSON(String(query))}, diff --git a/src/config.ts b/src/config.ts index 6642ed4..710a88f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -7,7 +7,9 @@ export class Config { shopifyAdminApiAccessToken: string shopifyApiURI: string shopifyStorePublicationId: string - + shopifyLocationId: string + shopifyCountryCodeOfOrigin: string + shopifyProvinceCodeOfOrigin: string constructor() { let ss = SpreadsheetApp.getActive() @@ -49,5 +51,23 @@ export class Config { "shopifyStorePublicationId", "value" ) + this.shopifyLocationId = vlookupByColumns( + "vars", + "key", + "shopifyLocationId", + "value" + ) + this.shopifyCountryCodeOfOrigin = vlookupByColumns( + "vars", + "key", + "shopifyCountryCodeOfOrigin", + "value" + ) + this.shopifyProvinceCodeOfOrigin = vlookupByColumns( + "vars", + "key", + "shopifyProvinceCodeOfOrigin", + "value" + ) } } diff --git a/src/shopifyApi.ts b/src/shopifyApi.ts index bd8d2d7..976975d 100644 --- a/src/shopifyApi.ts +++ b/src/shopifyApi.ts @@ -517,6 +517,160 @@ export class Shop { } while (!done) } + GetProductBySku(sku: string) { + console.log("GetProductBySku('" + sku + "')") + let gql = /* GraphQL */ ` + query productBySku { + products(first: 1, query: "sku:${sku}") { + edges { + node { + id + title + handle + variants(first: 1) { + nodes { + id + sku + } + } + options { + id + name + optionValues { + id + name + } + } + } + } + } + } + ` + let query = buildGqlQuery(gql, {}) + let response = this.shopifyGraphQLAPI(query) + if (response.content.data.products.edges.length <= 0) { + console.log("GetProductBySku: no product matched") + return + } + let product = response.content.data.products.edges[0].node + console.log("Product found:\n" + JSON.stringify(product, null, 2)) + return product + } + + GetInventoryItemBySku(sku: string) { + console.log('GetInventoryItemBySku("' + sku + '")') + let gql = /* GraphQL */ ` + query inventoryItems { + inventoryItems(first:1, query:"sku:${sku}") { + edges { + node { + id + tracked + sku + } + } + } + } + ` + let query = buildGqlQuery(gql, {}) + let response = this.shopifyGraphQLAPI(query) + let item: shopify.InventoryItem = + response.content.data.inventoryItems.edges[0].node + console.log( + "GetInventoryItemBySku: found item:\n" + JSON.stringify(item, null, 2) + ) + return item + } + + UpdateInventoryItemQuantity( + item: shopify.InventoryItem, + delta: number = 1, + config: Config + ) { + console.log("UpdateInventoryItemQuantity(" + JSON.stringify(item) + ")") + let gql = /* GraphQL */ ` + mutation inventoryAdjustQuantities( + $input: InventoryAdjustQuantitiesInput! + ) { + inventoryAdjustQuantities(input: $input) { + userErrors { + field + message + } + inventoryAdjustmentGroup { + createdAt + reason + referenceDocumentUri + changes { + name + delta + } + } + } + } + ` + let variables = { + input: { + reason: "correction", + name: "available", + changes: [ + { + delta: delta, + inventoryItemId: item.id, + locationId: config.shopifyLocationId, + }, + ], + }, + } + let query = buildGqlQuery(gql, variables) + let response = this.shopifyGraphQLAPI(query) + let newItem: shopify.InventoryItem = response.content + console.log("new item:\n" + JSON.stringify(newItem, null, 2)) + return newItem + } + + SetInventoryItemDefaults(item: shopify.InventoryItem, config: Config) { + let gql = /* GraphQL */ ` + mutation inventoryItemUpdate($id: ID!, $input: InventoryItemInput!) { + inventoryItemUpdate(id: $id, input: $input) { + inventoryItem { + id + unitCost { + amount + } + tracked + countryCodeOfOrigin + provinceCodeOfOrigin + harmonizedSystemCode + countryHarmonizedSystemCodes(first: 1) { + edges { + node { + harmonizedSystemCode + countryCode + } + } + } + } + userErrors { + message + } + } + } + ` + let variables = { + id: item.id, + input: { + tracked: true, + countryCodeOfOrigin: config.shopifyCountryCodeOfOrigin, + provinceCodeOfOrigin: config.shopifyProvinceCodeOfOrigin, + }, + } + let query = buildGqlQuery(gql, variables) + let response = this.shopifyGraphQLAPI(query) + let newItem: shopify.InventoryItem = response.content + return newItem + } + shopifyAPI(endpoint: string, query: {}, next = "") { var options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = { method: "get", @@ -541,6 +695,7 @@ export class Shop { } shopifyGraphQLAPI(query: {}, next = "") { + console.log("shopifyGraphQLAPI:query: " + JSON.stringify(query)) let endpoint = Shop.endpoints.graphql let options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = { method: "post", @@ -552,10 +707,13 @@ export class Shop { muteHttpExceptions: true, } var url = this.buildURL(endpoint) - console.log(UrlFetchApp.getRequest(url, options)) + console.log( + "shopifyGraphQLAPI sending request:\n" + + JSON.stringify(UrlFetchApp.getRequest(url, options), null, 2) + ) var resp = UrlFetchApp.fetch(url, options) let content = resp.getContentText() - console.log(content) + console.log("shopifyGraphQLAPI got response:\n" + content) let content_json = JSON.parse(content) console.log(JSON.stringify(content_json, null, 2)) @@ -737,8 +895,8 @@ export class ShopifyVariant { optionValues: [{}] = [ { optionName: "Title", - name: "Default Title" - } + name: "Default Title", + }, ] } @@ -756,7 +914,7 @@ export class ShopifyProductsQuery { query: string = "", fields: string[] = ["id", "title", "handle"], cursor: string = "", - pageSize: number = 10, + pageSize: number = 10 ) { let cursorText: string if (cursor == "") { @@ -838,7 +996,8 @@ export class ShopifyProductSetQuery { let j = `{ "query": ${formatGqlForJSON(String(this.GQL))}, "variables": { - "productSet": ${JSON.stringify(product)} + "productSet": ${JSON.stringify(product)}, + "synchronous": ${synchronous} } }` console.log(j) @@ -862,9 +1021,9 @@ export class ShopifyProductSetInput { { name: "Title", values: { - name: "Default Title" - } - } + name: "Default Title", + }, + }, ] } @@ -904,4 +1063,11 @@ export function getShopifyProducts() { shop.GetProducts() } -(global as any) \ No newline at end of file +export function buildGqlQuery(gql: string, variables: {}) { + let query = `{ + "query": ${formatGqlForJSON(String(gql))}, + "variables": ${JSON.stringify(variables)} + }` + console.log("buildGqlQuery:\n" + query) + return JSON.parse(query) +} \ No newline at end of file