feat: auto-create Draft Shopify products in Media Manager and fix description saving
- Implement auto-sync to Shopify: saving media for an unsynced product now creates it as a Draft in Shopify. - Update Product.ts to default new items to DRAFT status. - Allow getMediaSavePlan to run without a shopify_id (planning for new products). - Fix description saving in mediaHandlers to reconcile 'body_html' and common variants. - Sanitize empty quotes in MediaManager description textarea. - Update mediaHandlers.test.ts to verify auto-creation behavior and fix mock pollution.
This commit is contained in:
@ -198,6 +198,10 @@ export class Product {
|
|||||||
"UpdateShopifyProduct: no product matched, this will be a new product"
|
"UpdateShopifyProduct: no product matched, this will be a new product"
|
||||||
)
|
)
|
||||||
newProduct = true
|
newProduct = true
|
||||||
|
// Default to DRAFT for auto-created products
|
||||||
|
if (!this.shopify_status) {
|
||||||
|
this.shopify_status = "DRAFT";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log("UpdateShopifyProduct: calling productSet")
|
console.log("UpdateShopifyProduct: calling productSet")
|
||||||
let sps = this.ToShopifyProductSet()
|
let sps = this.ToShopifyProductSet()
|
||||||
|
|||||||
@ -96,7 +96,9 @@ jest.mock("./Product", () => {
|
|||||||
sku: sku,
|
sku: sku,
|
||||||
shopify_id: "shopify_id_123",
|
shopify_id: "shopify_id_123",
|
||||||
title: "Test Product Title",
|
title: "Test Product Title",
|
||||||
|
shopify_status: "ACTIVE",
|
||||||
MatchToShopifyProduct: jest.fn(),
|
MatchToShopifyProduct: jest.fn(),
|
||||||
|
UpdateShopifyProduct: jest.fn(),
|
||||||
ImportFromInventory: jest.fn()
|
ImportFromInventory: jest.fn()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -395,15 +397,20 @@ describe("mediaHandlers", () => {
|
|||||||
expect(calledInstance.processMediaChanges).toHaveBeenCalledWith("SKU123", finalState, expect.anything(), null)
|
expect(calledInstance.processMediaChanges).toHaveBeenCalledWith("SKU123", finalState, expect.anything(), null)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should throw if product not synced", () => {
|
test("saveMediaChanges should auto-create product if not synced", () => {
|
||||||
const MockProduct = Product as unknown as jest.Mock
|
const MockProduct = Product as unknown as jest.Mock
|
||||||
|
const mockUpdateShopify = jest.fn().mockImplementation(function(this: any) {
|
||||||
|
this.shopify_id = "NEW_ID"
|
||||||
|
})
|
||||||
MockProduct.mockImplementationOnce(() => ({
|
MockProduct.mockImplementationOnce(() => ({
|
||||||
shopify_id: null,
|
shopify_id: null,
|
||||||
MatchToShopifyProduct: jest.fn(),
|
MatchToShopifyProduct: jest.fn(),
|
||||||
|
UpdateShopifyProduct: mockUpdateShopify,
|
||||||
ImportFromInventory: jest.fn()
|
ImportFromInventory: jest.fn()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
expect(() => saveMediaChanges("SKU123", [])).toThrow("Product must be synced")
|
saveMediaChanges("SKU123", [])
|
||||||
|
expect(mockUpdateShopify).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should update sheet thumbnail with first image", () => {
|
test("should update sheet thumbnail with first image", () => {
|
||||||
@ -592,9 +599,11 @@ describe("mediaHandlers", () => {
|
|||||||
(global.SpreadsheetApp.getActiveSheet as jest.Mock).mockReturnValue(mockSheet);
|
(global.SpreadsheetApp.getActiveSheet as jest.Mock).mockReturnValue(mockSheet);
|
||||||
|
|
||||||
const mockSSInstance = {
|
const mockSSInstance = {
|
||||||
setCellValueByColumnName: jest.fn()
|
setCellValueByColumnName: jest.fn(),
|
||||||
|
getRowNumberByColumnValue: jest.fn().mockReturnValue(5), // Added for robustness
|
||||||
|
getHeaders: jest.fn().mockReturnValue(["sku", "title", "product_type", "product_style", "body_html"])
|
||||||
};
|
};
|
||||||
(GASSpreadsheetService as unknown as jest.Mock).mockReturnValue(mockSSInstance);
|
(GASSpreadsheetService as unknown as jest.Mock).mockReturnValueOnce(mockSSInstance);
|
||||||
|
|
||||||
(newSku as jest.Mock).mockReturnValue("SKU-123");
|
(newSku as jest.Mock).mockReturnValue("SKU-123");
|
||||||
|
|
||||||
@ -608,6 +617,37 @@ describe("mediaHandlers", () => {
|
|||||||
expect(result).toBe("SKU-123");
|
expect(result).toBe("SKU-123");
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("saveMediaChanges should auto-create product if unsynced", () => {
|
||||||
|
// Mock defaults for this test
|
||||||
|
const mockRange = { getRow: () => 5 };
|
||||||
|
const mockSheet = {
|
||||||
|
getName: jest.fn().mockReturnValue("product_inventory"),
|
||||||
|
getActiveRange: jest.fn().mockReturnValue(mockRange),
|
||||||
|
getLastColumn: jest.fn().mockReturnValue(5),
|
||||||
|
getRange: jest.fn().mockReturnValue(mockRange)
|
||||||
|
};
|
||||||
|
(global.SpreadsheetApp.getActiveSheet as jest.Mock).mockReturnValue(mockSheet);
|
||||||
|
|
||||||
|
// Setup Unsynced Product
|
||||||
|
const MockProduct = Product as unknown as jest.Mock
|
||||||
|
const mockUpdateShopify = jest.fn().mockImplementation(function(this: any) {
|
||||||
|
this.shopify_id = "CREATED_ID_123"
|
||||||
|
this.shopify_status = "DRAFT"
|
||||||
|
})
|
||||||
|
|
||||||
|
MockProduct.mockImplementationOnce(() => ({
|
||||||
|
shopify_id: "",
|
||||||
|
MatchToShopifyProduct: jest.fn(),
|
||||||
|
UpdateShopifyProduct: mockUpdateShopify
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Proceed with save
|
||||||
|
const finalState = [{ id: "1" }]
|
||||||
|
saveMediaChanges("SKU_NEW", finalState)
|
||||||
|
|
||||||
|
expect(mockUpdateShopify).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
test("generateSkuForActiveRow should delegate to newSku", () => {
|
test("generateSkuForActiveRow should delegate to newSku", () => {
|
||||||
const mockSheet = {
|
const mockSheet = {
|
||||||
getName: jest.fn().mockReturnValue("product_inventory"),
|
getName: jest.fn().mockReturnValue("product_inventory"),
|
||||||
|
|||||||
@ -141,10 +141,12 @@ export function saveMediaChanges(sku: string, finalState: any[], jobId: string |
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!product.shopify_id) {
|
if (!product.shopify_id) {
|
||||||
// Allow saving Drive-only changes? No, we need Shopify context for "Staging" usually.
|
console.log("saveMediaChanges: Product not synced. Auto-creating Draft Product...");
|
||||||
// But if we just rename drive files, we could?
|
product.UpdateShopifyProduct(shop);
|
||||||
// For now, fail safe.
|
|
||||||
throw new Error("Product must be synced to Shopify before saving media changes.")
|
if (!product.shopify_id) {
|
||||||
|
throw new Error("Failed to auto-create Draft Product. Cannot save media.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logs = mediaService.processMediaChanges(sku, finalState, product.shopify_id, jobId)
|
const logs = mediaService.processMediaChanges(sku, finalState, product.shopify_id, jobId)
|
||||||
@ -252,10 +254,11 @@ export function getMediaSavePlan(sku: string, finalState: any[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!product.shopify_id) {
|
if (!product.shopify_id) {
|
||||||
throw new Error("Product must be synced to Shopify before saving media changes.")
|
console.log("getMediaSavePlan: Product not synced. Proceeding with empty Shopify state.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaService.calculatePlan(sku, finalState, product.shopify_id);
|
// Pass empty string if no ID, ensure calculatePlan handles it (it expects string)
|
||||||
|
return mediaService.calculatePlan(sku, finalState, product.shopify_id || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function executeSavePhase(sku: string, phase: string, planData: any, jobId: string | null = null) {
|
export function executeSavePhase(sku: string, phase: string, planData: any, jobId: string | null = null) {
|
||||||
@ -274,7 +277,9 @@ export function executeSavePhase(sku: string, phase: string, planData: any, jobI
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!product.shopify_id) {
|
if (!product.shopify_id) {
|
||||||
throw new Error("Product must be synced to Shopify before saving media changes.")
|
console.log("executeSavePhase: Product not synced. Auto-creating Draft Product...");
|
||||||
|
product.UpdateShopifyProduct(shop);
|
||||||
|
if (!product.shopify_id) throw new Error("Failed to auto-create Draft Product.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaService.executeSavePhase(sku, phase, planData, product.shopify_id, jobId);
|
return mediaService.executeSavePhase(sku, phase, planData, product.shopify_id, jobId);
|
||||||
@ -296,7 +301,9 @@ export function executeFullSavePlan(sku: string, plan: any, jobId: string | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!product.shopify_id) {
|
if (!product.shopify_id) {
|
||||||
throw new Error("Product must be synced to Shopify before saving media changes.")
|
console.log("executeFullSavePlan: Product not synced. Auto-creating Draft Product...");
|
||||||
|
product.UpdateShopifyProduct(shop);
|
||||||
|
if (!product.shopify_id) throw new Error("Failed to auto-create Draft Product.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaService.executeFullSavePlan(sku, plan, product.shopify_id, jobId);
|
return mediaService.executeFullSavePlan(sku, plan, product.shopify_id, jobId);
|
||||||
|
|||||||
Reference in New Issue
Block a user