feat: enforce SKU validity, use SKU as handle

This commit enforces proper SKU validation, uses the SKU as the Shopify handle, and implements ID-based product updates to allow renaming. It also extracts the IShop interface for TDD.
This commit is contained in:
Ben Miller
2025-12-25 04:54:55 -07:00
parent 2672d47203
commit 7cb469ccf9
8 changed files with 251 additions and 9 deletions

106
src/ProductLogic.test.ts Normal file
View File

@ -0,0 +1,106 @@
import { Product } from "./Product";
import { MockSpreadsheetService } from "./services/MockSpreadsheetService";
import { MockShop } from "./test/MockShop";
describe("Product Logic (TDD)", () => {
let mockService: MockSpreadsheetService;
let mockShop: MockShop;
beforeEach(() => {
mockService = new MockSpreadsheetService({
product_inventory: [
["sku", "title", "price", "shopify_id", "product_type"], // Headers
["TEST-SKU-1", "Test Product", 10.99, "", "Type A"] // Data
],
values: [
["product_type", "shopify_category", "ebay_category_id"],
["Type A", "Category A", "123"]
]
});
mockShop = new MockShop();
});
test("UpdateShopifyProduct should abort if SKU is invalid", () => {
// Setup invalid SKU
mockService.setSheetData("product_inventory", [
["sku", "title"],
["?", "Invalid Product"]
]);
// Allow empty sku to be passed to constructor logic check, but Product constructor throws if sku not found.
// So we pass a valid sku that exists in sheet, but looks invalid?
// The requirement is "based on the product's title... If I have a placeholder value... ensure products are not created until they have a valid SKU".
// In `Product.ts`, constructor takes `sku`.
// If I pass `?` and it's in the sheet, it constructs.
const product = new Product("?", mockService);
// Attempt update
product.UpdateShopifyProduct(mockShop);
// Verify no calls to creating product
// We expect MatchToShopifyProduct might be called (read only), but NOT productSet (writ)
// Actually our plan said "abort operation" at start of UpdateShopifyProduct.
expect(mockShop.productSetCalledWith).toBeNull();
});
test("ToShopifyProductSet should set handle to SKU", () => {
const product = new Product("TEST-SKU-1", mockService);
const sps = product.ToShopifyProductSet();
// We expect sps to have a 'handle' property equal to the sku
// This will fail compilation initially as ShopifyProductSetInput doesn't have handle
expect((sps as any).handle).toBe("TEST-SKU-1");
});
test("MatchToShopifyProduct should verify ID if present", () => {
// Setup data with shopify_id
mockService.setSheetData("product_inventory", [
["sku", "shopify_id"],
["TEST-SKU-OLD", "123456789"]
]);
const product = new Product("TEST-SKU-OLD", mockService);
// Mock the response for GetProductById
mockShop.mockProductById = {
id: "123456789",
title: "Old Title",
variants: { nodes: [{ id: "gid://shopify/ProductVariant/123456789", sku: "TEST-SKU-OLD" }] },
options: [{ id: "opt1", optionValues: [{ id: "optval1" }] }]
};
// We need to call Match, but it's called inside Update usually.
// We can call it directly for testing.
product.MatchToShopifyProduct(mockShop);
// Expect GetProductById to have been called
expect(mockShop.getProductByIdCalledWith).toBe("123456789");
expect(product.shopify_id).toBe("123456789");
});
test("MatchToShopifyProduct should fall back to SKU if ID lookup fails", () => {
// Setup data with shopify_id that is invalid
mockService.setSheetData("product_inventory", [
["sku", "shopify_id"],
["TEST-SKU-FAIL", "999999999"]
]);
const product = new Product("TEST-SKU-FAIL", mockService);
// Mock ID lookup failure (returns null/undefined)
mockShop.mockProductById = null;
// Mock SKU lookup success
mockShop.mockProductBySku = {
id: "555555555",
title: "Found By SKU",
variants: { nodes: [{ id: "gid://shopify/ProductVariant/555555555", sku: "TEST-SKU-FAIL" }] },
options: [{ id: "opt2", optionValues: [{ id: "optval2" }] }]
};
product.MatchToShopifyProduct(mockShop);
expect(mockShop.getProductByIdCalledWith).toBe("999999999");
// Should fall back to SKU
expect(mockShop.getProductBySkuCalledWith).toBe("TEST-SKU-FAIL");
expect(product.shopify_id).toBe("555555555");
});
});