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

@ -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. 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. 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. 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.

View File

@ -13,6 +13,7 @@ The system allows you to:
- Monitor and troubleshoot background processes via a custom side panel. - 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. - **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. - **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 ## Prerequisites

View File

@ -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. 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
Triggers are managed programmatically via `src/triggers.ts`. Running `reinstallTriggers` will wipe existing project triggers and set up the standard set: Triggers are managed programmatically via `src/triggers.ts`. Running `reinstallTriggers` will wipe existing project triggers and set up the standard set:
- `onEdit` -> `newSkuHandler` - `onEdit` -> `onEditHandler` (Main Router)
- `onEdit` -> `matchProductToShopifyOnEditHandler`
- `onEdit` -> `onEditQueue`
- `TimeBased (1 min)` -> `processBatchedEdits` - `TimeBased (1 min)` -> `processBatchedEdits`
- `TimeBased (10 min)` -> `checkRecentSales`
### 5. Troubleshooting Panel (`src/sidebar.ts`, `src/Sidebar.html`) ### 5. Troubleshooting Panel (`src/sidebar.ts`, `src/Sidebar.html`)

View File

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

View File

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

View File

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

View File

@ -671,6 +671,49 @@ export class Shop {
return newItem 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) { SetInventoryItemDefaults(item: shopify.InventoryItem, config: Config) {
let gql = /* GraphQL */ ` let gql = /* GraphQL */ `
mutation inventoryItemUpdate($id: ID!, $input: InventoryItemInput!) { 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() let ss = SpreadsheetApp.getActive()
ScriptApp.newTrigger("newSkuHandler").forSpreadsheet(ss).onEdit().create() ScriptApp.newTrigger("onEditHandler").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("processBatchedEdits") ScriptApp.newTrigger("processBatchedEdits")
.timeBased() .timeBased()
.everyMinutes(1) .everyMinutes(1)