feat: add supplier scoring and UPC file analysis functionality
- Implemented supplier scoring logic in `supplier-scoring.ts` with functions to compute demand score, competition penalty, and overall supplier product score. - Created unit tests for supplier scoring in `supplier-scoring.test.ts` to validate scoring logic against various scenarios. - Developed UPC file analysis tool in `upc-file-analysis.ts` to process UPCs in batches, fetch product data from Keepa and SP-API, and generate supplier results. - Added UPC input reading functionality in `upc-file-reader.ts` to handle XLSX and XLS files, including validation for UPC formats. - Introduced a command-line tool in `upc-lookup.ts` for looking up UPCs and displaying detailed results or mappings to ASINs. - Enhanced error handling and logging throughout the new modules for better traceability and user feedback.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { fetchKeepaDataBatch } from "./keepa.ts";
|
||||
import { fetchSellabilityBatch, fetchSpApiPricingAndFees } from "./sp-api.ts";
|
||||
import { getCache, setCache } from "./cache.ts";
|
||||
import { analyzeProducts } from "./llm.ts";
|
||||
import { fetchKeepaDataBatch } from "./integrations/keepa.ts";
|
||||
import { fetchSellabilityBatch, fetchSpApiPricingAndFees } from "./integrations/sp-api.ts";
|
||||
import { getCache, setCache } from "./integrations/cache.ts";
|
||||
import { analyzeProducts } from "./integrations/llm.ts";
|
||||
import type {
|
||||
AnalysisResult,
|
||||
EnrichedProduct,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { searchProductOffers, type SearxngOfferSearchResult } from "./searxng.ts";
|
||||
import { searchProductOffers, type SearxngOfferSearchResult } from "./integrations/searxng.ts";
|
||||
|
||||
type CliArgs = {
|
||||
query: string;
|
||||
|
||||
@@ -35,7 +35,7 @@ const makeMockDb = (): any => ({
|
||||
transaction: async (fn: (tx: any) => Promise<any>) => fn(makeMockDb()),
|
||||
});
|
||||
|
||||
mock.module("./db/index.ts", () => ({ db: makeMockDb(), client: {} }));
|
||||
mock.module("../db/index.ts", () => ({ db: makeMockDb(), client: {} }));
|
||||
|
||||
const fetchSellabilityBatchMock = mock(async (asins: string[]) => {
|
||||
return new Map(
|
||||
@@ -69,12 +69,12 @@ const analyzeProductsMock = mock(async (products: any[]) => {
|
||||
}));
|
||||
});
|
||||
|
||||
mock.module("./sp-api.ts", () => ({
|
||||
mock.module("../integrations/sp-api.ts", () => ({
|
||||
fetchSellabilityBatch: fetchSellabilityBatchMock,
|
||||
fetchSpApiPricingAndFees: fetchSpApiPricingAndFeesMock,
|
||||
}));
|
||||
|
||||
mock.module("./llm.ts", () => ({
|
||||
mock.module("../integrations/llm.ts", () => ({
|
||||
analyzeProducts: analyzeProductsMock,
|
||||
}));
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { db } from "./db/index.ts";
|
||||
import { runs, categoryProductResults } from "./db/schema.ts";
|
||||
import { db } from "../db/index.ts";
|
||||
import { runs, categoryProductResults } from "../db/schema.ts";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { config } from "./config.ts";
|
||||
import { analyzeProducts } from "./llm.ts";
|
||||
import { fetchSellabilityBatch, fetchSpApiPricingAndFees } from "./sp-api.ts";
|
||||
import { config } from "../config.ts";
|
||||
import { analyzeProducts } from "../integrations/llm.ts";
|
||||
import { fetchSellabilityBatch, fetchSpApiPricingAndFees } from "../integrations/sp-api.ts";
|
||||
import type {
|
||||
AnalysisResult,
|
||||
EnrichedProduct,
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
ProductRecord,
|
||||
SellabilityInfo,
|
||||
SpApiData,
|
||||
} from "./types.ts";
|
||||
} from "../types.ts";
|
||||
|
||||
type CategoryInfo = {
|
||||
id: number;
|
||||
@@ -35,7 +35,7 @@ const makeMockDb = (): any => ({
|
||||
transaction: async (fn: (tx: any) => Promise<any>) => fn(makeMockDb()),
|
||||
});
|
||||
|
||||
mock.module("./db/index.ts", () => ({ db: makeMockDb(), client: {} }));
|
||||
mock.module("../db/index.ts", () => ({ db: makeMockDb(), client: {} }));
|
||||
|
||||
const fetchSellabilityBatchMock = mock(async (asins: string[]) => {
|
||||
return new Map<string, any>(
|
||||
@@ -84,12 +84,12 @@ const analyzeProductsMock = mock(async (products: any[]) => {
|
||||
}));
|
||||
});
|
||||
|
||||
mock.module("./sp-api.ts", () => ({
|
||||
mock.module("../integrations/sp-api.ts", () => ({
|
||||
fetchSellabilityBatch: fetchSellabilityBatchMock,
|
||||
fetchSpApiPricingAndFees: fetchSpApiPricingAndFeesMock,
|
||||
}));
|
||||
|
||||
mock.module("./llm.ts", () => ({
|
||||
mock.module("../integrations/llm.ts", () => ({
|
||||
analyzeProducts: analyzeProductsMock,
|
||||
}));
|
||||
|
||||
@@ -2,18 +2,18 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
import { db } from "./db/index.ts";
|
||||
import { runs, categoryProductResults } from "./db/schema.ts";
|
||||
import { db } from "../db/index.ts";
|
||||
import { runs, categoryProductResults } from "../db/schema.ts";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { config } from "./config.ts";
|
||||
import { config } from "../config.ts";
|
||||
import {
|
||||
connectCache,
|
||||
disconnectCache,
|
||||
getApiCache,
|
||||
setApiCache,
|
||||
} from "./cache.ts";
|
||||
import { analyzeProducts } from "./llm.ts";
|
||||
import { fetchSellabilityBatch, fetchSpApiPricingAndFees } from "./sp-api.ts";
|
||||
} from "../integrations/cache.ts";
|
||||
import { analyzeProducts } from "../integrations/llm.ts";
|
||||
import { fetchSellabilityBatch, fetchSpApiPricingAndFees } from "../integrations/sp-api.ts";
|
||||
import type {
|
||||
AnalysisResult,
|
||||
EnrichedProduct,
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
ProductRecord,
|
||||
SellabilityInfo,
|
||||
SpApiData,
|
||||
} from "./types.ts";
|
||||
} from "../types.ts";
|
||||
|
||||
type CategoryInfo = {
|
||||
id: number;
|
||||
@@ -35,7 +35,7 @@ const makeMockDb = (): any => ({
|
||||
transaction: async (fn: (tx: any) => Promise<any>) => fn(makeMockDb()),
|
||||
});
|
||||
|
||||
mock.module("./db/index.ts", () => ({ db: makeMockDb(), client: {} }));
|
||||
mock.module("../db/index.ts", () => ({ db: makeMockDb(), client: {} }));
|
||||
|
||||
const fetchSellabilityBatchMock = mock(async (asins: string[]) => {
|
||||
return new Map<string, any>(
|
||||
@@ -82,12 +82,12 @@ const analyzeProductsMock = mock(async (products: any[]) => {
|
||||
}));
|
||||
});
|
||||
|
||||
mock.module("./sp-api.ts", () => ({
|
||||
mock.module("../integrations/sp-api.ts", () => ({
|
||||
fetchSellabilityBatch: fetchSellabilityBatchMock,
|
||||
fetchSpApiPricingAndFees: fetchSpApiPricingAndFeesMock,
|
||||
}));
|
||||
|
||||
mock.module("./llm.ts", () => ({
|
||||
mock.module("../integrations/llm.ts", () => ({
|
||||
analyzeProducts: analyzeProductsMock,
|
||||
}));
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { db } from "./db/index.ts";
|
||||
import { runs, categoryProductResults } from "./db/schema.ts";
|
||||
import { db } from "../db/index.ts";
|
||||
import { runs, categoryProductResults } from "../db/schema.ts";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { config } from "./config.ts";
|
||||
import { analyzeProducts } from "./llm.ts";
|
||||
import { fetchSellabilityBatch, fetchSpApiPricingAndFees } from "./sp-api.ts";
|
||||
import { config } from "../config.ts";
|
||||
import { analyzeProducts } from "../integrations/llm.ts";
|
||||
import { fetchSellabilityBatch, fetchSpApiPricingAndFees } from "../integrations/sp-api.ts";
|
||||
import type {
|
||||
AnalysisResult,
|
||||
EnrichedProduct,
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
ProductRecord,
|
||||
SellabilityInfo,
|
||||
SpApiData,
|
||||
} from "./types.ts";
|
||||
} from "../types.ts";
|
||||
|
||||
type CategoryInfo = {
|
||||
id: number;
|
||||
@@ -1,3 +0,0 @@
|
||||
// Central re-export so existing `import { db } from "./database.ts"` keeps working.
|
||||
export { db, type Db } from "./db/index.ts";
|
||||
export * as schema from "./db/schema.ts";
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readProducts } from "./reader.ts";
|
||||
import { connectCache, disconnectCache } from "./cache.ts";
|
||||
import { connectCache, disconnectCache } from "./integrations/cache.ts";
|
||||
import {
|
||||
printResults,
|
||||
writeResultsToDb,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Redis from "ioredis";
|
||||
import { config } from "./config.ts";
|
||||
import type { EnrichedProduct, KeepaData, SpApiData } from "./types.ts";
|
||||
import { config } from "../config.ts";
|
||||
import type { EnrichedProduct, KeepaData, SpApiData } from "../types.ts";
|
||||
|
||||
let redis: Redis | null = null;
|
||||
let disabled = false;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { config } from "./config.ts";
|
||||
import type { KeepaData, KeepaUpcLookupDetail } from "./types.ts";
|
||||
import { config } from "../config.ts";
|
||||
import type { KeepaData, KeepaUpcLookupDetail } from "../types.ts";
|
||||
|
||||
const KEEPA_BASE = "https://api.keepa.com";
|
||||
const MAX_ASINS_PER_REQUEST = 100;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { config } from "./config.ts";
|
||||
import type { EnrichedProduct, LlmVerdict } from "./types.ts";
|
||||
import { config } from "../config.ts";
|
||||
import type { EnrichedProduct, LlmVerdict } from "../types.ts";
|
||||
|
||||
const SYSTEM_PROMPT_STRICT = `You are an expert Amazon product analyst specializing in FBA and FBM fulfillment strategy.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { SellingPartner } from "amazon-sp-api";
|
||||
import { config } from "./config.ts";
|
||||
import { config } from "../config.ts";
|
||||
import type {
|
||||
KeepaUpcLookupStatus,
|
||||
SpApiData,
|
||||
SellabilityInfo,
|
||||
UpcLookupDetail,
|
||||
} from "./types.ts";
|
||||
} from "../types.ts";
|
||||
|
||||
type RegionCode = "na" | "eu" | "fe";
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
fetchKeepaDataBatch,
|
||||
lookupKeepaUpcs,
|
||||
mapUpcsToAsins,
|
||||
} from "./keepa.ts";
|
||||
import { runUpcFileAnalysis } from "./upc-file-analysis.ts";
|
||||
import { fetchSellabilityBatch, fetchSpApiPricingAndFees } from "./sp-api.ts";
|
||||
import { analyzeProducts } from "./llm.ts";
|
||||
} from "./integrations/keepa.ts";
|
||||
import { runUpcFileAnalysis } from "./supplier/upc-file-analysis.ts";
|
||||
import { fetchSellabilityBatch, fetchSpApiPricingAndFees } from "./integrations/sp-api.ts";
|
||||
import { analyzeProducts } from "./integrations/llm.ts";
|
||||
import type {
|
||||
EnrichedProduct,
|
||||
KeepaUpcLookupDetail,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { testSpApiConnectivity, testSpApiSellability } from "./sp-api.ts";
|
||||
import { testSpApiConnectivity, testSpApiSellability } from "./integrations/sp-api.ts";
|
||||
|
||||
function parseArgs(): { asin?: string; sellabilityMode: boolean } {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { db } from "./db/index.ts";
|
||||
import { categoryProductResults, runs } from "./db/schema.ts";
|
||||
import { db } from "../db/index.ts";
|
||||
import { categoryProductResults, runs } from "../db/schema.ts";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { analyzeProducts } from "./llm.ts";
|
||||
import { fetchSpApiPricingAndFees } from "./sp-api.ts";
|
||||
import { analyzeProducts } from "../integrations/llm.ts";
|
||||
import { fetchSpApiPricingAndFees } from "../integrations/sp-api.ts";
|
||||
import type {
|
||||
AnalysisResult,
|
||||
EnrichedProduct,
|
||||
KeepaData,
|
||||
ProductRecord,
|
||||
SellabilityInfo,
|
||||
} from "./types.ts";
|
||||
} from "../types.ts";
|
||||
|
||||
const LLM_BATCH_SIZE = 5;
|
||||
const LLM_BATCH_DELAY_MS = 5_000;
|
||||
@@ -62,7 +62,7 @@ const makeMockDb = (): any => ({
|
||||
transaction: async (fn: (tx: any) => Promise<any>) => fn(makeMockTx()),
|
||||
});
|
||||
|
||||
mock.module("./db/index.ts", () => ({ db: makeMockDb(), client: {} }));
|
||||
mock.module("../db/index.ts", () => ({ db: makeMockDb(), client: {} }));
|
||||
|
||||
const TEST_DIR = path.join(process.cwd(), "test_output", "stalker-sellability");
|
||||
const originalFetch = globalThis.fetch;
|
||||
@@ -87,10 +87,6 @@ const fetchSellabilityBatchMock = mock(async (asins: string[]) => {
|
||||
);
|
||||
});
|
||||
|
||||
mock.module("./sp-api.ts", () => ({
|
||||
fetchSellabilityBatch: fetchSellabilityBatchMock,
|
||||
}));
|
||||
|
||||
const modulePromise = import("./stalker.ts");
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -194,22 +190,25 @@ test("sellability checks matched seller inventory, not the source ASIN", async (
|
||||
return new Response("not found", { status: 404 });
|
||||
}) as unknown as typeof globalThis.fetch;
|
||||
|
||||
const stats = await runStalker({
|
||||
input: inputPath,
|
||||
maxAsins: null,
|
||||
storefrontUpdateHours: 168,
|
||||
offerLimit: 20,
|
||||
sellerLimit: 30,
|
||||
inventoryLimit: 200,
|
||||
sellerCacheHours: 168,
|
||||
includeStock: false,
|
||||
dryRun: false,
|
||||
resume: true,
|
||||
maxSellerRequests: null,
|
||||
sellability: true,
|
||||
analyzeSellable: false,
|
||||
useClaude: false,
|
||||
});
|
||||
const stats = await runStalker(
|
||||
{
|
||||
input: inputPath,
|
||||
maxAsins: null,
|
||||
storefrontUpdateHours: 168,
|
||||
offerLimit: 20,
|
||||
sellerLimit: 30,
|
||||
inventoryLimit: 200,
|
||||
sellerCacheHours: 168,
|
||||
includeStock: false,
|
||||
dryRun: false,
|
||||
resume: true,
|
||||
maxSellerRequests: null,
|
||||
sellability: true,
|
||||
analyzeSellable: false,
|
||||
useClaude: false,
|
||||
},
|
||||
{ fetchSellabilityBatch: fetchSellabilityBatchMock },
|
||||
);
|
||||
|
||||
expect(fetchSellabilityBatchMock.mock.calls.length).toBe(1);
|
||||
expect(fetchSellabilityBatchMock.mock.calls[0]?.[0]).toEqual([
|
||||
@@ -69,7 +69,7 @@ const makeMockDb = (): any => ({
|
||||
transaction: async (fn: (tx: any) => Promise<any>) => fn(makeMockTx()),
|
||||
});
|
||||
|
||||
mock.module("./db/index.ts", () => ({ db: makeMockDb(), client: {} }));
|
||||
mock.module("../db/index.ts", () => ({ db: makeMockDb(), client: {} }));
|
||||
|
||||
const TEST_DIR = path.join(process.cwd(), "test_output", "stalker");
|
||||
const originalFetch = globalThis.fetch;
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import path from "node:path";
|
||||
import { db } from "./db/index.ts";
|
||||
import { db } from "../db/index.ts";
|
||||
import {
|
||||
runs,
|
||||
stalkerRuns,
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
sellers,
|
||||
stalkerAsinSellers,
|
||||
stalkerSellerInventory,
|
||||
} from "./db/schema.ts";
|
||||
} from "../db/schema.ts";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { fetchSellabilityBatch } from "./sp-api.ts";
|
||||
import type { SellabilityInfo } from "./types.ts";
|
||||
import { fetchSellabilityBatch } from "../integrations/sp-api.ts";
|
||||
import type { SellabilityInfo } from "../types.ts";
|
||||
|
||||
const KEEPA_BASE = "https://api.keepa.com";
|
||||
const DOMAIN_US = "1";
|
||||
@@ -310,7 +310,11 @@ export function extractLiveOfferSellerCandidates(
|
||||
return Array.from(bySeller.values());
|
||||
}
|
||||
|
||||
export async function runStalker(args: StalkerArgs): Promise<StalkerRunStats> {
|
||||
export type StalkerDeps = {
|
||||
fetchSellabilityBatch?: (asins: string[]) => Promise<Map<string, SellabilityInfo>>;
|
||||
};
|
||||
|
||||
export async function runStalker(args: StalkerArgs, deps: StalkerDeps = {}): Promise<StalkerRunStats> {
|
||||
const apiKey = Bun.env.KEEPA_API_KEY;
|
||||
if (!apiKey) throw new Error("Missing required env var: KEEPA_API_KEY");
|
||||
|
||||
@@ -383,7 +387,7 @@ export async function runStalker(args: StalkerArgs): Promise<StalkerRunStats> {
|
||||
);
|
||||
|
||||
if (args.sellability && !args.dryRun) {
|
||||
await enrichInventorySellability(result, stats);
|
||||
await enrichInventorySellability(result, stats, deps.fetchSellabilityBatch ?? fetchSellabilityBatch);
|
||||
}
|
||||
applyInventoryPersistencePolicy(result, args.sellability && !args.dryRun);
|
||||
if (args.sellability && !args.dryRun) {
|
||||
@@ -545,6 +549,7 @@ function applyInventoryPersistencePolicy(
|
||||
async function enrichInventorySellability(
|
||||
result: StalkerAsinResult,
|
||||
stats: StalkerRunStats,
|
||||
sellabilityFn: (asins: string[]) => Promise<Map<string, SellabilityInfo>>,
|
||||
): Promise<void> {
|
||||
const sellers = result.matchedSellers.map(({ seller }) => seller);
|
||||
const items = sellers.flatMap((seller) => seller.storefrontItems);
|
||||
@@ -554,7 +559,7 @@ async function enrichInventorySellability(
|
||||
console.log(
|
||||
`Stalker inventory sellability: checking ${uniqueAsins.length} ASIN(s) from matched seller storefronts...`,
|
||||
);
|
||||
const sellabilityMap = await fetchSellabilityBatch(uniqueAsins);
|
||||
const sellabilityMap = await sellabilityFn(uniqueAsins);
|
||||
stats.inventorySellabilityCheckedAsins += uniqueAsins.length;
|
||||
|
||||
for (const asin of uniqueAsins) {
|
||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
||||
import { rmSync } from "node:fs";
|
||||
import ExcelJS from "exceljs";
|
||||
import { writeSupplierWorkbook } from "./supplier-export.ts";
|
||||
import type { SupplierAnalysisResult } from "./types.ts";
|
||||
import type { SupplierAnalysisResult } from "../types.ts";
|
||||
|
||||
const OUTPUT_FILE = path.join("/private/tmp", "asin-check-supplier-export-test.xlsx");
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
KeepaUpcLookupStatus,
|
||||
SupplierAnalysisResult,
|
||||
SupplierVerdict,
|
||||
} from "./types.ts";
|
||||
} from "../types.ts";
|
||||
|
||||
export type SupplierExportSummary = {
|
||||
processedRows: number;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { scoreSupplierProduct } from "./supplier-scoring.ts";
|
||||
import type { KeepaData, ProductRecord, SpApiData } from "./types.ts";
|
||||
import type { KeepaData, ProductRecord, SpApiData } from "../types.ts";
|
||||
|
||||
function record(overrides: Partial<ProductRecord> = {}): ProductRecord {
|
||||
return {
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
ProductRecord,
|
||||
SpApiData,
|
||||
SupplierScore,
|
||||
} from "./types.ts";
|
||||
} from "../types.ts";
|
||||
|
||||
function round2(value: number): number {
|
||||
return Math.round(value * 100) / 100;
|
||||
@@ -1,10 +1,10 @@
|
||||
import path from "node:path";
|
||||
import { fetchKeepaDataBatch, lookupKeepaUpcs } from "./keepa.ts";
|
||||
import { fetchKeepaDataBatch, lookupKeepaUpcs } from "../integrations/keepa.ts";
|
||||
import {
|
||||
fetchSellabilityBatch,
|
||||
fetchSpApiPricingAndFees,
|
||||
lookupSpApiUpcs,
|
||||
} from "./sp-api.ts";
|
||||
} from "../integrations/sp-api.ts";
|
||||
import {
|
||||
processUpcFileInBatches,
|
||||
type UpcInputRow,
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
refreshRunCountsInDb,
|
||||
startRunInDb,
|
||||
type RunCounts,
|
||||
} from "./writer.ts";
|
||||
import { connectCache, disconnectCache } from "./cache.ts";
|
||||
} from "../writer.ts";
|
||||
import { connectCache, disconnectCache } from "../integrations/cache.ts";
|
||||
import { scoreSupplierProduct, resolveSupplierSalePrice } from "./supplier-scoring.ts";
|
||||
import {
|
||||
writeSupplierWorkbook,
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
SupplierAnalysisResult,
|
||||
SupplierScore,
|
||||
UpcLookupDetail,
|
||||
} from "./types.ts";
|
||||
} from "../types.ts";
|
||||
|
||||
const DEFAULT_INPUT_BATCH_SIZE = 200;
|
||||
const DEFAULT_UPC_LOOKUP_BATCH_SIZE = 100;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { lookupKeepaUpcs, mapUpcsToAsins } from "./keepa.ts";
|
||||
import { lookupKeepaUpcs, mapUpcsToAsins } from "../integrations/keepa.ts";
|
||||
|
||||
function printUsage(): void {
|
||||
console.log("Usage:");
|
||||
Reference in New Issue
Block a user