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:
@ -6,3 +6,4 @@ This file serves as a guide for future sessions working on this codebase.
|
||||
2. **Update Memory**: If we make significant architectural decisions or change our working patterns, update `MEMORY.md` to reflect this.
|
||||
3. **Check Documentation**: `README.md`, `docs/ARCHITECTURE.md`, and `docs/SETUP.md` are the sources of truth for the system. Keep them updated as code changes.
|
||||
4. **Task Tracking**: Use the `task.md` artifact to track progress on multi-step tasks.
|
||||
5. **Shopify API Reference**: When developing features involving Shopify, **ALWAYS** check the [Shopify API Reference](https://shopify.dev/docs/api/admin-graphql) for the specific version in use (check `shopifyApi.ts` for version). Do not guess field names or structure.
|
||||
|
||||
@ -13,6 +13,7 @@ The system allows you to:
|
||||
- Monitor and troubleshoot background processes via a custom side panel.
|
||||
- **Automated Sales Sync**: Periodically check Shopify for recent sales and mark items as "sold" in the sheet.
|
||||
- **Manual Reconciliation**: Backfill sales data for a specific time range via menu command.
|
||||
- **Status Workflow Automation**: Automatically update Shopify status and inventory based on the sheet's "status" column (e.g., "Sold" -> Active, 0 Qty).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
@ -59,13 +59,19 @@ Configuration, including API keys, is stored in a dedicated Google Sheet named "
|
||||
|
||||
Since Apps Script functions must be top-level to be triggered or attached to buttons, `src/global.ts` explicitly exposes necessary functions from the modules to the global scope.
|
||||
|
||||
### 5. Status Automation (`src/statusHandlers.ts`)
|
||||
|
||||
A modular system handles changes to the `status` column. It uses a registry of `StatusHandler` implementations:
|
||||
- **Published**: Sets Shopify Status `ACTIVE`, Quantity `1`.
|
||||
- **Sold/Artist Swap**: Sets Shopify Status `ACTIVE`, Quantity `0`.
|
||||
- **Drafted**: Sets Shopify Status `DRAFT`.
|
||||
|
||||
## Triggers
|
||||
|
||||
Triggers are managed programmatically via `src/triggers.ts`. Running `reinstallTriggers` will wipe existing project triggers and set up the standard set:
|
||||
- `onEdit` -> `newSkuHandler`
|
||||
- `onEdit` -> `matchProductToShopifyOnEditHandler`
|
||||
- `onEdit` -> `onEditQueue`
|
||||
- `onEdit` -> `onEditHandler` (Main Router)
|
||||
- `TimeBased (1 min)` -> `processBatchedEdits`
|
||||
- `TimeBased (10 min)` -> `checkRecentSales`
|
||||
|
||||
### 5. Troubleshooting Panel (`src/sidebar.ts`, `src/Sidebar.html`)
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
89
src/statusHandlers.ts
Normal 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(),
|
||||
}
|
||||
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user