feat: implement status automation and router pattern

- Implemented modular status automation system (statusHandlers.ts).
- Added handlers for 'Published' (Active/Qty 1), 'Sold' (Active/Qty 0), and 'Drafted'.
- Refactored onEdit triggers into a central Router pattern in OnEditHandler.ts.
- Updated Product.ts to support explicit quantity setting (fixed 0 value bug).
- Updated shopifyApi.ts to implement SetInventoryItemQuantity (using ignoreCompareQuantity).
- Consolidated triggers into single onEditHandler.
- Updated project documentation.
This commit is contained in:
2025-12-24 23:55:28 -07:00
parent 2d43c07546
commit 85cdfe1443
9 changed files with 158 additions and 15 deletions

View File

@ -4,10 +4,15 @@ import { getCellRangeByColumnName } from "./sheetUtils"
import { matchProductToShopify, updateProductToShopify } from "./match"
import { getColumnName, toastAndLog } from "./sheetUtils"
import { onEditQueue } from "./onEditQueue"
import { statusOnEditHandler } from "./statusHandlers"
export function onEditHandler(e: GoogleAppsScript.Events.SheetsOnEdit) {
//TODO: process each edited row
// Router pattern: execute all handlers
newSkuHandler(e)
matchProductToShopifyOnEditHandler(e)
onEditQueue(e)
statusOnEditHandler(e)
}
export function matchProductToShopifyOnEditHandler(

View File

@ -39,6 +39,7 @@ export class Product {
product_depth_cm: number = 0
product_height_cm: number = 0
photos: string = ""
quantity: number | null = null
shopify_product: shopify.Product
shopify_default_variant_id: string = ""
shopify_default_option_id: string = ""
@ -76,7 +77,7 @@ export class Product {
console.log("skipping '" + headers[i] + "'")
continue
}
if (productValues[i] == "") {
if (productValues[i] === "") {
console.log(
"keeping '" + headers[i] + "' default: '" + this[headers[i]] + "'"
)
@ -211,7 +212,10 @@ export class Product {
shopify.WeightUnit.GRAMS
)
}
if (newProduct) {
if (this.quantity !== null) {
console.log("UpdateShopifyProduct: setting inventory item quantity to " + this.quantity)
shop.SetInventoryItemQuantity(item, this.quantity, config)
} else if (newProduct) {
console.log("UpdateShopifyProduct: setting defaults on new product")
console.log("UpdateShopifyProduct: adjusting inventory item quantity")
shop.UpdateInventoryItemQuantity(item, 1, config)

View File

@ -14,7 +14,7 @@ import {
import { createMissingPhotoFolders } from "./createMissingPhotoFolders"
import { reinstallTriggers } from "./triggers"
import { newSkuHandler } from "./newSku"
import { columnOnEditHandler } from "./OnEditHandler"
import { columnOnEditHandler, onEditHandler } from "./OnEditHandler"
import {
onEditQueue,
processBatchedEdits
@ -33,6 +33,7 @@ import { installSalesSyncTrigger } from "./triggers"
;(global as any).matchProductToShopifyOnEditHandler = matchProductToShopifyOnEditHandler
;(global as any).updateShopifyProductHandler = updateShopifyProductHandler
;(global as any).columnOnEditHandler = columnOnEditHandler
;(global as any).onEditHandler = onEditHandler
;(global as any).onEditQueue = onEditQueue
;(global as any).processBatchedEdits = processBatchedEdits
;(global as any).reauthorizeScript = reauthorizeScript

View File

@ -671,6 +671,49 @@ export class Shop {
return newItem
}
SetInventoryItemQuantity(
item: shopify.InventoryItem,
quantity: number,
config: Config
) {
console.log("SetInventoryItemQuantity(" + JSON.stringify(item) + ", " + quantity + ")")
let gql = /* GraphQL */ `
mutation inventorySetQuantities($input: InventorySetQuantitiesInput!) {
inventorySetQuantities(input: $input) {
inventoryAdjustmentGroup {
changes {
name
delta
}
}
userErrors {
field
message
}
}
}
`
let variables = {
input: {
name: "available",
reason: "correction",
ignoreCompareQuantity: true,
quantities: [
{
inventoryItemId: item.id,
locationId: config.shopifyLocationId,
quantity: quantity,
},
],
},
}
let query = buildGqlQuery(gql, variables)
let response = this.shopifyGraphQLAPI(query)
// Response structure is different for setQuantities
console.log("SetInventoryItemQuantity response:\n" + JSON.stringify(response, null, 2))
return response.content
}
SetInventoryItemDefaults(item: shopify.InventoryItem, config: Config) {
let gql = /* GraphQL */ `
mutation inventoryItemUpdate($id: ID!, $input: InventoryItemInput!) {

89
src/statusHandlers.ts Normal file
View File

@ -0,0 +1,89 @@
import { getCellRangeByColumnName, getColumnName, toastAndLog } from "./sheetUtils"
export function statusOnEditHandler(e: GoogleAppsScript.Events.SheetsOnEdit) {
const sheet = e.range.getSheet()
if (sheet.getName() !== "product_inventory") return
const range = e.range
const col = range.getColumn()
// Optimization: Check if we are in the 'status' column (usually relatively early or fixed position,
// but looking up by name is safer against loose columns).
// Note: getColumnName technically opens the sheet again, but it's cached in Apps Script context usually.
const header = getColumnName("product_inventory", col)
if (header !== "status") return
// Handle multiple rows edit?
// Current requirement implies single row interactions, but let's just handle the top-left cell
// of the range for now or iterate if needed.
// Given e.value is used for single cell, let's stick to simple single-line logic or iterate.
// Safest to iterate if multiple rows selected.
const numRows = range.getNumRows()
const startRow = range.getRow()
// If e.value is present, it's a single cell edit.
// If not, it might be a paste.
const values = range.getValues() // 2D array
for (let i = 0; i < numRows; i++) {
const row = startRow + i
const val = String(values[i][0]).toLowerCase()
const handler = handlers[val]
if (handler) {
console.log(`Executing handler for status: ${val} on row ${row}`)
handler.handle(sheet, row)
} else {
console.log(`No specific action for status: ${val} on row ${row}`)
}
}
}
interface StatusHandler {
handle(sheet: GoogleAppsScript.Spreadsheet.Sheet, row: number): void
}
class PublishedHandler implements StatusHandler {
handle(sheet: GoogleAppsScript.Spreadsheet.Sheet, row: number) {
const statusCell = getCellRangeByColumnName(sheet, "shopify_status", row)
if (statusCell) statusCell.setValue("ACTIVE")
const qtyCell = getCellRangeByColumnName(sheet, "quantity", row)
if (qtyCell) qtyCell.setValue(1)
toastAndLog("Status 'Published': Set to Active, Qty 1")
}
}
class DraftedHandler implements StatusHandler {
handle(sheet: GoogleAppsScript.Spreadsheet.Sheet, row: number) {
const statusCell = getCellRangeByColumnName(sheet, "shopify_status", row)
if (statusCell) statusCell.setValue("DRAFT")
toastAndLog("Status 'Drafted': Set to Draft")
}
}
class SoldHandler implements StatusHandler {
handle(sheet: GoogleAppsScript.Spreadsheet.Sheet, row: number) {
const statusCell = getCellRangeByColumnName(sheet, "shopify_status", row)
if (statusCell) statusCell.setValue("ACTIVE")
const qtyCell = getCellRangeByColumnName(sheet, "quantity", row)
if (qtyCell) qtyCell.setValue(0)
toastAndLog("Status 'Sold': Set to Active, Qty 0")
}
}
const handlers: { [key: string]: StatusHandler } = {
"published": new PublishedHandler(),
"drafted": new DraftedHandler(),
"sold": new SoldHandler(),
"artist swap": new SoldHandler(),
"freebee": new SoldHandler(),
"sold: destroyed": new SoldHandler(),
"sold: gift": new SoldHandler(),
"sold: home use": new SoldHandler(),
}

View File

@ -5,14 +5,7 @@ export function reinstallTriggers() {
}
let ss = SpreadsheetApp.getActive()
ScriptApp.newTrigger("newSkuHandler").forSpreadsheet(ss).onEdit().create()
ScriptApp.newTrigger("matchProductToShopifyOnEditHandler")
.forSpreadsheet(ss)
.onEdit()
.create()
// ScriptApp.newTrigger("columnOnEditHandler").forSpreadsheet(ss).onEdit().create()
// ScriptApp.newTrigger("onEditQueue").forSpreadsheet(ss).onEdit().create()
ScriptApp.newTrigger("onEditQueue").forSpreadsheet(ss).onEdit().create()
ScriptApp.newTrigger("onEditHandler").forSpreadsheet(ss).onEdit().create()
ScriptApp.newTrigger("processBatchedEdits")
.timeBased()
.everyMinutes(1)