ebay-mcp-remote-edition 3.1.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ import { buildResolvedValidationQueryPlan } from './query-utils.js';
2
+ export async function getTerapeakValidationSignals(_api, request) {
3
+ await Promise.resolve();
4
+ const { queryPlan, queryResolution } = buildResolvedValidationQueryPlan(request);
5
+ const queryCandidates = queryPlan.map((candidate) => candidate.query);
6
+ const currentQuery = queryCandidates[0] ?? null;
7
+ const previousPobQuery = queryCandidates[1] ?? currentQuery;
8
+ return {
9
+ avgWatchersPerListing: null,
10
+ preOrderListingsCount: null,
11
+ marketPriceUsd: null,
12
+ avgShippingCostUsd: null,
13
+ competitionLevel: null,
14
+ previousPobAvgPriceUsd: null,
15
+ previousPobSellThroughPct: null,
16
+ currentListingsCount: null,
17
+ soldListingsCount: null,
18
+ provider: 'none',
19
+ confidence: 'Low',
20
+ queryDebug: {
21
+ currentQuery,
22
+ previousPobQuery,
23
+ selectedMode: 'combined',
24
+ currentResultCount: null,
25
+ previousPobResultCount: null,
26
+ queryResolution,
27
+ notes: 'Terapeak/eBay research provider contract is in place, but live authenticated research retrieval is not implemented yet.',
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,100 @@
1
+ function addHours(timestamp, hours) {
2
+ return new Date(new Date(timestamp).getTime() + hours * 60 * 60 * 1000).toISOString();
3
+ }
4
+ export function buildValidationRecommendation(request, signals) {
5
+ const dDay = request.validation.dDay;
6
+ const baseCadence = typeof dDay === 'number' && dDay >= -3 && dDay <= 3 ? 'Hourly' : 'Daily';
7
+ const shouldAutoTrack = request.validation.autoCheckEnabled &&
8
+ request.validation.automationStatus === 'Watching' &&
9
+ request.validation.buyDecision === 'Watching';
10
+ const trackingCadence = shouldAutoTrack ? baseCadence : 'Off';
11
+ const nextCheckAt = !shouldAutoTrack
12
+ ? null
13
+ : trackingCadence === 'Hourly'
14
+ ? addHours(request.timestamp, 1)
15
+ : addHours(request.timestamp, 24);
16
+ const marketPrice = signals.terapeak.marketPriceUsd ??
17
+ signals.sold.soldMedianPriceUsd ??
18
+ signals.ebay.marketPriceUsd;
19
+ const preorderListingsCount = signals.terapeak.preOrderListingsCount ?? signals.ebay.preOrderListingsCount;
20
+ const wholesale = request.item.wholesalePrice;
21
+ const marginRatio = marketPrice !== null && wholesale !== null && wholesale > 0
22
+ ? (marketPrice - wholesale) / wholesale
23
+ : null;
24
+ const recentSoldCount = [
25
+ signals.sold.soldVelocity.day1Sold,
26
+ signals.sold.soldVelocity.day2Sold,
27
+ signals.sold.soldVelocity.day3Sold,
28
+ ].reduce((sum, value) => sum + (value ?? 0), 0);
29
+ let latestAiRecommendation = 'Continue watching until stronger market signal appears.';
30
+ let latestAiConfidence = 'Medium';
31
+ let monitoringNotes = 'Baseline recommendation generated from current validation state.';
32
+ if (!shouldAutoTrack) {
33
+ latestAiRecommendation =
34
+ 'Automatic tracking paused because the validation is no longer in a watchable state.';
35
+ latestAiConfidence = 'High';
36
+ monitoringNotes =
37
+ 'Stop conditions were met, so automation will not schedule another validation run.';
38
+ }
39
+ else if (marginRatio !== null &&
40
+ marginRatio >= 1 &&
41
+ preorderListingsCount !== null &&
42
+ preorderListingsCount >= 25) {
43
+ latestAiRecommendation =
44
+ 'Demand and pricing look constructive. Continue tracking closely and be ready to upgrade from watch status if sell-through strengthens.';
45
+ latestAiConfidence = 'High';
46
+ monitoringNotes =
47
+ 'Healthy active-listing volume and strong projected margin support continued monitoring.';
48
+ if (signals.sold.soldMedianPriceUsd !== null) {
49
+ monitoringNotes =
50
+ 'Healthy active-listing volume and sold comparables support continued monitoring without yet forcing a buy-state change.';
51
+ }
52
+ }
53
+ else if (signals.sold.soldMedianPriceUsd !== null &&
54
+ recentSoldCount > 0 &&
55
+ signals.sold.confidence !== 'Low') {
56
+ latestAiRecommendation =
57
+ 'Recent sold comparables support real resale demand. Continue watching closely while waiting for a stronger conviction signal.';
58
+ latestAiConfidence = signals.sold.confidence === 'High' ? 'High' : 'Medium';
59
+ monitoringNotes =
60
+ 'Sold-item data confirms recent transaction activity, improving confidence while remaining conservative on buy-state changes.';
61
+ }
62
+ else if (signals.sold.soldMedianPriceUsd !== null) {
63
+ latestAiRecommendation =
64
+ 'Sold comps are available, but sample depth is still limited. Keep monitoring until resale momentum becomes clearer.';
65
+ latestAiConfidence = 'Medium';
66
+ monitoringNotes =
67
+ 'Temporary sold-provider data is present, but sample depth is not yet strong enough to justify an automatic buy-decision change.';
68
+ }
69
+ else if (signals.social.twitterTrending === true ||
70
+ (signals.social.youtubeViews24hMillions !== null &&
71
+ signals.social.youtubeViews24hMillions >= 0.1) ||
72
+ (signals.social.redditPostsCount7d !== null && signals.social.redditPostsCount7d >= 5)) {
73
+ latestAiRecommendation =
74
+ 'Demand signals are mixed. Keep monitoring until eBay pricing and social momentum become more decisive.';
75
+ latestAiConfidence = 'Medium';
76
+ monitoringNotes =
77
+ 'Cross-channel social activity exists, but it is only being used as a supporting confidence signal and is not strong enough to justify an automatic buy change.';
78
+ }
79
+ if (signals.terapeak.previousPobSellThroughPct !== null &&
80
+ signals.terapeak.previousPobSellThroughPct >= 50) {
81
+ monitoringNotes +=
82
+ ' Previous POB sell-through research suggests the comparable release sold through efficiently.';
83
+ if (latestAiConfidence !== 'High') {
84
+ latestAiConfidence = 'Medium';
85
+ }
86
+ }
87
+ if (signals.research.previousComebackFirstWeekSales !== null) {
88
+ monitoringNotes += ` Previous comeback first-week sales reference: ${signals.research.previousComebackFirstWeekSales}.`;
89
+ }
90
+ return {
91
+ buyDecision: request.validation.buyDecision,
92
+ automationStatus: shouldAutoTrack ? 'Watching' : 'Paused',
93
+ trackingCadence,
94
+ shouldAutoTrack,
95
+ nextCheckAt,
96
+ latestAiRecommendation,
97
+ latestAiConfidence,
98
+ monitoringNotes,
99
+ };
100
+ }
@@ -0,0 +1,374 @@
1
+ import { validationRunRequestSchema } from './schemas.js';
2
+ import { getEbayValidationSignals } from './providers/ebay.js';
3
+ import { getEbaySoldValidationSignals } from './providers/ebay-sold.js';
4
+ import { getTerapeakValidationSignals } from './providers/terapeak.js';
5
+ import { getSocialValidationSignals } from './providers/social.js';
6
+ import { getChartValidationSignals } from './providers/chart.js';
7
+ import { getPreviousComebackResearchSignals } from './providers/research.js';
8
+ import { buildProviderQueryResolutionDebug } from './providers/query-utils.js';
9
+ import { buildValidationRecommendation } from './recommendation.js';
10
+ function addMinutes(timestamp, minutes) {
11
+ return new Date(new Date(timestamp).getTime() + minutes * 60 * 1000).toISOString();
12
+ }
13
+ function mapErrorCode(error) {
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ if (/refresh|authorization expired|access token|token/i.test(message)) {
16
+ return 'EBAY_AUTH_FAILED';
17
+ }
18
+ return 'VALIDATION_RUN_FAILED';
19
+ }
20
+ function getValidationId(input) {
21
+ if (typeof input === 'object' &&
22
+ input !== null &&
23
+ 'validationId' in input &&
24
+ typeof input.validationId === 'string') {
25
+ return input.validationId;
26
+ }
27
+ return '';
28
+ }
29
+ function isMeaningfulWriteValue(value) {
30
+ if (value === null || value === undefined) {
31
+ return false;
32
+ }
33
+ return typeof value !== 'string' || value.length > 0;
34
+ }
35
+ function getFieldPresence(fields) {
36
+ const contributed = [];
37
+ const omitted = [];
38
+ for (const [field, value] of Object.entries(fields)) {
39
+ if (isMeaningfulWriteValue(value)) {
40
+ contributed.push(field);
41
+ }
42
+ else {
43
+ omitted.push(field);
44
+ }
45
+ }
46
+ return { contributed, omitted };
47
+ }
48
+ function getWriteSource(value, source) {
49
+ return isMeaningfulWriteValue(value) ? source : 'none';
50
+ }
51
+ function buildProviderDebug(request, ebay, sold, terapeak, social, chart, research) {
52
+ const requestQueryResolution = buildProviderQueryResolutionDebug(request, Boolean(ebay.queryResolution?.queryContextUsed));
53
+ const ebayFields = getFieldPresence({
54
+ avgWatchersPerListing: ebay.avgWatchersPerListing,
55
+ preOrderListingsCount: ebay.preOrderListingsCount,
56
+ marketPriceUsd: ebay.marketPriceUsd,
57
+ avgShippingCostUsd: ebay.avgShippingCostUsd,
58
+ competitionLevel: ebay.competitionLevel,
59
+ });
60
+ const soldFields = getFieldPresence({
61
+ soldAveragePriceUsd: sold.soldAveragePriceUsd,
62
+ soldMedianPriceUsd: sold.soldMedianPriceUsd,
63
+ soldMinPriceUsd: sold.soldMinPriceUsd,
64
+ soldMaxPriceUsd: sold.soldMaxPriceUsd,
65
+ day1Sold: sold.soldVelocity.day1Sold,
66
+ day2Sold: sold.soldVelocity.day2Sold,
67
+ day3Sold: sold.soldVelocity.day3Sold,
68
+ day4Sold: sold.soldVelocity.day4Sold,
69
+ day5Sold: sold.soldVelocity.day5Sold,
70
+ daysTracked: sold.soldVelocity.daysTracked,
71
+ });
72
+ const terapeakFields = getFieldPresence({
73
+ avgWatchersPerListing: terapeak.avgWatchersPerListing,
74
+ preOrderListingsCount: terapeak.preOrderListingsCount,
75
+ marketPriceUsd: terapeak.marketPriceUsd,
76
+ avgShippingCostUsd: terapeak.avgShippingCostUsd,
77
+ competitionLevel: terapeak.competitionLevel,
78
+ previousPobAvgPriceUsd: terapeak.previousPobAvgPriceUsd,
79
+ previousPobSellThroughPct: terapeak.previousPobSellThroughPct,
80
+ });
81
+ const socialFields = getFieldPresence({
82
+ twitterTrending: social.twitterTrending,
83
+ youtubeViews24hMillions: social.youtubeViews24hMillions,
84
+ redditPostsCount7d: social.redditPostsCount7d,
85
+ });
86
+ const researchFields = getFieldPresence({
87
+ previousAlbumTitle: research.previousAlbumTitle,
88
+ previousComebackFirstWeekSales: research.previousComebackFirstWeekSales,
89
+ });
90
+ const ebayStatus = (ebay.queryCandidates?.length ?? 0) === 0
91
+ ? 'unavailable'
92
+ : ebay.sampleSize > 0
93
+ ? 'ok'
94
+ : 'partial';
95
+ const socialStatus = socialFields.contributed.length > 0 ? 'ok' : social.debug ? 'partial' : 'unavailable';
96
+ const terapeakStatus = terapeak.provider === 'none'
97
+ ? 'stub'
98
+ : terapeakFields.contributed.length > 0
99
+ ? 'ok'
100
+ : 'partial';
101
+ const researchStatus = research.previousComebackFirstWeekSales !== null || research.previousAlbumTitle !== null
102
+ ? 'ok'
103
+ : 'stub';
104
+ return {
105
+ ebay: {
106
+ status: ebayStatus,
107
+ confidence: ebay.sampleSize >= 10 ? 'medium' : 'low',
108
+ browseSampleSize: ebay.sampleSize,
109
+ queryCandidates: ebay.queryCandidates ?? [],
110
+ selectedQuery: ebay.selectedQuery,
111
+ selectedQueryTier: ebay.selectedQueryTier,
112
+ queryDiagnostics: ebay.queryDiagnostics ?? [],
113
+ queryContextUsed: ebay.queryResolution?.queryContextUsed ?? requestQueryResolution.queryContextUsed,
114
+ querySource: ebay.queryResolution?.querySource ?? requestQueryResolution.querySource,
115
+ resolvedSearchQuery: ebay.queryResolution?.resolvedSearchQuery ?? requestQueryResolution.resolvedSearchQuery,
116
+ validationScope: ebay.queryResolution?.validationScope ?? requestQueryResolution.validationScope,
117
+ queryScope: ebay.queryResolution?.queryScope ?? requestQueryResolution.queryScope,
118
+ selectionReason: ebay.selectionReason,
119
+ errorMessage: ebay.errorMessage,
120
+ responseStatus: ebay.responseStatus,
121
+ responseBodyExcerpt: ebay.responseBodyExcerpt,
122
+ contributedFields: ebayFields.contributed,
123
+ omittedFields: ebayFields.omitted,
124
+ },
125
+ sold: {
126
+ status: sold.status,
127
+ provider: sold.provider,
128
+ confidence: sold.confidence.toLowerCase(),
129
+ soldResultsCount: sold.soldResultsCount,
130
+ queryCandidates: sold.queryCandidates ?? [],
131
+ selectedQuery: sold.selectedQuery,
132
+ selectedQueryTier: sold.selectedQueryTier,
133
+ queryContextUsed: sold.queryResolution?.queryContextUsed ?? requestQueryResolution.queryContextUsed,
134
+ querySource: sold.queryResolution?.querySource ?? requestQueryResolution.querySource,
135
+ resolvedSearchQuery: sold.queryResolution?.resolvedSearchQuery ?? requestQueryResolution.resolvedSearchQuery,
136
+ validationScope: sold.queryResolution?.validationScope ?? requestQueryResolution.validationScope,
137
+ queryScope: sold.queryResolution?.queryScope ?? requestQueryResolution.queryScope,
138
+ contributedFields: soldFields.contributed,
139
+ omittedFields: soldFields.omitted,
140
+ errorMessage: sold.errorMessage,
141
+ },
142
+ terapeak: {
143
+ status: terapeakStatus,
144
+ provider: terapeak.provider,
145
+ confidence: terapeak.confidence.toLowerCase(),
146
+ queryCandidates: [
147
+ terapeak.queryDebug.currentQuery,
148
+ terapeak.queryDebug.previousPobQuery,
149
+ ].filter((value) => typeof value === 'string' && value.length > 0),
150
+ currentQuery: terapeak.queryDebug.currentQuery,
151
+ previousPobQuery: terapeak.queryDebug.previousPobQuery,
152
+ queryContextUsed: terapeak.queryDebug.queryResolution?.queryContextUsed ??
153
+ requestQueryResolution.queryContextUsed,
154
+ querySource: terapeak.queryDebug.queryResolution?.querySource ?? requestQueryResolution.querySource,
155
+ resolvedSearchQuery: terapeak.queryDebug.queryResolution?.resolvedSearchQuery ??
156
+ requestQueryResolution.resolvedSearchQuery,
157
+ validationScope: terapeak.queryDebug.queryResolution?.validationScope ??
158
+ requestQueryResolution.validationScope,
159
+ queryScope: terapeak.queryDebug.queryResolution?.queryScope ?? requestQueryResolution.queryScope,
160
+ selectedMode: terapeak.queryDebug.selectedMode,
161
+ currentResultCount: terapeak.queryDebug.currentResultCount,
162
+ previousPobResultCount: terapeak.queryDebug.previousPobResultCount,
163
+ contributedFields: terapeakFields.contributed,
164
+ omittedFields: terapeakFields.omitted,
165
+ notes: terapeak.queryDebug.notes,
166
+ },
167
+ social: {
168
+ status: socialStatus,
169
+ confidence: 'low',
170
+ queryContextUsed: requestQueryResolution.queryContextUsed,
171
+ querySource: requestQueryResolution.querySource,
172
+ resolvedSearchQuery: requestQueryResolution.resolvedSearchQuery,
173
+ validationScope: requestQueryResolution.validationScope,
174
+ queryScope: requestQueryResolution.queryScope,
175
+ contributedFields: socialFields.contributed,
176
+ omittedFields: socialFields.omitted,
177
+ details: social.debug,
178
+ },
179
+ chart: {
180
+ status: 'stub',
181
+ confidence: 'low',
182
+ contributedFields: [],
183
+ omittedFields: ['chartMomentum'],
184
+ },
185
+ research: {
186
+ status: researchStatus,
187
+ confidence: research.confidence.toLowerCase(),
188
+ previousAlbumTitle: research.previousAlbumTitle,
189
+ previousComebackFirstWeekSales: research.previousComebackFirstWeekSales,
190
+ contributedFields: researchFields.contributed,
191
+ omittedFields: researchFields.omitted,
192
+ notes: research.notes,
193
+ sources: research.sources ?? [],
194
+ },
195
+ };
196
+ }
197
+ export async function runValidation(api, input) {
198
+ let request;
199
+ try {
200
+ request = validationRunRequestSchema.parse(input);
201
+ }
202
+ catch (error) {
203
+ return {
204
+ status: 'error',
205
+ validationId: getValidationId(input),
206
+ errorCode: 'VALIDATION_REQUEST_INVALID',
207
+ message: error instanceof Error ? error.message : String(error),
208
+ retryable: false,
209
+ nextCheckAt: null,
210
+ };
211
+ }
212
+ try {
213
+ const ebay = await getEbayValidationSignals(api, request);
214
+ const sold = await getEbaySoldValidationSignals(request);
215
+ const terapeak = await getTerapeakValidationSignals(api, request);
216
+ const social = await getSocialValidationSignals(request);
217
+ const chart = getChartValidationSignals(request);
218
+ const research = await getPreviousComebackResearchSignals(request);
219
+ const mergedAvgWatchers = terapeak.avgWatchersPerListing ?? ebay.avgWatchersPerListing;
220
+ const mergedPreorderListings = terapeak.preOrderListingsCount ?? ebay.preOrderListingsCount;
221
+ const marketPriceUsd = terapeak.marketPriceUsd ?? sold.soldMedianPriceUsd ?? ebay.marketPriceUsd;
222
+ const mergedAvgShippingCostUsd = terapeak.avgShippingCostUsd ?? ebay.avgShippingCostUsd;
223
+ const mergedCompetitionLevel = terapeak.competitionLevel ?? ebay.competitionLevel;
224
+ const soldVelocity = {
225
+ day1Sold: sold.soldVelocity.day1Sold ?? ebay.soldVelocity.day1Sold,
226
+ day2Sold: sold.soldVelocity.day2Sold ?? ebay.soldVelocity.day2Sold,
227
+ day3Sold: sold.soldVelocity.day3Sold ?? ebay.soldVelocity.day3Sold,
228
+ day4Sold: sold.soldVelocity.day4Sold ?? ebay.soldVelocity.day4Sold,
229
+ day5Sold: sold.soldVelocity.day5Sold ?? ebay.soldVelocity.day5Sold,
230
+ daysTracked: sold.soldVelocity.daysTracked ?? ebay.soldVelocity.daysTracked,
231
+ };
232
+ const recommendation = buildValidationRecommendation(request, {
233
+ ebay,
234
+ sold,
235
+ terapeak,
236
+ social,
237
+ chart,
238
+ research,
239
+ });
240
+ const requestQueryResolution = buildProviderQueryResolutionDebug(request, Boolean(ebay.queryResolution?.queryContextUsed));
241
+ const mergedSignals = { ebay, sold, terapeak, social, chart, research };
242
+ const socialWrites = {
243
+ ...(social.twitterTrending !== null ? { twitterTrending: social.twitterTrending } : {}),
244
+ ...(social.youtubeViews24hMillions !== null
245
+ ? { youtubeViews24hMillions: social.youtubeViews24hMillions }
246
+ : {}),
247
+ ...(social.redditPostsCount7d !== null
248
+ ? { redditPostsCount7d: social.redditPostsCount7d }
249
+ : {}),
250
+ };
251
+ const terapeakWrites = {
252
+ ...(terapeak.previousPobAvgPriceUsd !== null
253
+ ? { previousPobAvgPriceUsd: terapeak.previousPobAvgPriceUsd }
254
+ : {}),
255
+ ...(terapeak.previousPobSellThroughPct !== null
256
+ ? { previousPobSellThroughPct: terapeak.previousPobSellThroughPct }
257
+ : {}),
258
+ };
259
+ const researchWrites = {
260
+ ...(research.previousComebackFirstWeekSales !== null
261
+ ? { previousComebackFirstWeekSales: research.previousComebackFirstWeekSales }
262
+ : {}),
263
+ };
264
+ const writeResolution = {
265
+ avgWatchersPerListing: terapeak.avgWatchersPerListing !== null
266
+ ? 'terapeak'
267
+ : getWriteSource(ebay.avgWatchersPerListing, 'ebay'),
268
+ preOrderListingsCount: terapeak.preOrderListingsCount !== null
269
+ ? 'terapeak'
270
+ : getWriteSource(ebay.preOrderListingsCount, 'ebay'),
271
+ marketPriceUsd: terapeak.marketPriceUsd !== null
272
+ ? 'terapeak'
273
+ : sold.soldMedianPriceUsd !== null
274
+ ? 'sold'
275
+ : getWriteSource(ebay.marketPriceUsd, 'ebay'),
276
+ avgShippingCostUsd: terapeak.avgShippingCostUsd !== null
277
+ ? 'terapeak'
278
+ : getWriteSource(ebay.avgShippingCostUsd, 'ebay'),
279
+ competitionLevel: terapeak.competitionLevel !== null
280
+ ? 'terapeak'
281
+ : getWriteSource(ebay.competitionLevel, 'ebay'),
282
+ twitterTrending: getWriteSource(social.twitterTrending, 'social'),
283
+ youtubeViews24hMillions: getWriteSource(social.youtubeViews24hMillions, 'social'),
284
+ redditPostsCount7d: getWriteSource(social.redditPostsCount7d, 'social'),
285
+ day1Sold: sold.soldVelocity.day1Sold !== null
286
+ ? 'sold'
287
+ : getWriteSource(ebay.soldVelocity.day1Sold, 'ebay'),
288
+ day2Sold: sold.soldVelocity.day2Sold !== null
289
+ ? 'sold'
290
+ : getWriteSource(ebay.soldVelocity.day2Sold, 'ebay'),
291
+ day3Sold: sold.soldVelocity.day3Sold !== null
292
+ ? 'sold'
293
+ : getWriteSource(ebay.soldVelocity.day3Sold, 'ebay'),
294
+ day4Sold: sold.soldVelocity.day4Sold !== null
295
+ ? 'sold'
296
+ : getWriteSource(ebay.soldVelocity.day4Sold, 'ebay'),
297
+ day5Sold: sold.soldVelocity.day5Sold !== null
298
+ ? 'sold'
299
+ : getWriteSource(ebay.soldVelocity.day5Sold, 'ebay'),
300
+ daysTracked: sold.soldVelocity.daysTracked !== null
301
+ ? 'sold'
302
+ : getWriteSource(ebay.soldVelocity.daysTracked, 'ebay'),
303
+ previousPobAvgPriceUsd: getWriteSource(terapeak.previousPobAvgPriceUsd, 'terapeak'),
304
+ previousPobSellThroughPct: getWriteSource(terapeak.previousPobSellThroughPct, 'terapeak'),
305
+ previousComebackFirstWeekSales: getWriteSource(research.previousComebackFirstWeekSales, 'research'),
306
+ };
307
+ const omittedOptionalWrites = Object.entries(writeResolution)
308
+ .filter(([, source]) => source === 'none')
309
+ .map(([field]) => field);
310
+ return {
311
+ status: 'ok',
312
+ validationId: request.validationId,
313
+ writes: {
314
+ avgWatchersPerListing: mergedAvgWatchers,
315
+ preOrderListingsCount: mergedPreorderListings,
316
+ ...socialWrites,
317
+ marketPriceUsd,
318
+ avgShippingCostUsd: mergedAvgShippingCostUsd,
319
+ competitionLevel: mergedCompetitionLevel,
320
+ marketPriceTrend: ebay.marketPriceTrend,
321
+ day1Sold: soldVelocity.day1Sold,
322
+ day2Sold: soldVelocity.day2Sold,
323
+ day3Sold: soldVelocity.day3Sold,
324
+ day4Sold: soldVelocity.day4Sold,
325
+ day5Sold: soldVelocity.day5Sold,
326
+ daysTracked: soldVelocity.daysTracked,
327
+ ...terapeakWrites,
328
+ ...researchWrites,
329
+ monitoringNotes: recommendation.monitoringNotes,
330
+ lastDataSnapshot: JSON.stringify(mergedSignals),
331
+ latestAiRecommendation: recommendation.latestAiRecommendation,
332
+ latestAiConfidence: recommendation.latestAiConfidence,
333
+ validationError: '',
334
+ },
335
+ decision: {
336
+ buyDecision: recommendation.buyDecision,
337
+ automationStatus: recommendation.automationStatus,
338
+ trackingCadence: recommendation.trackingCadence,
339
+ shouldAutoTrack: recommendation.shouldAutoTrack,
340
+ nextCheckAt: recommendation.nextCheckAt,
341
+ },
342
+ debug: {
343
+ ebayQuery: ebay.ebayQuery,
344
+ soldQuery: sold.query,
345
+ queryCandidates: {
346
+ ebay: ebay.queryCandidates ?? [],
347
+ sold: sold.queryCandidates ?? [],
348
+ terapeak: [terapeak.queryDebug.currentQuery, terapeak.queryDebug.previousPobQuery].filter((value) => typeof value === 'string' && value.length > 0),
349
+ },
350
+ browseSampleSize: ebay.sampleSize,
351
+ soldResultsCount: sold.soldResultsCount,
352
+ omittedOptionalWrites,
353
+ writeResolution,
354
+ sourceSet: ['ebay', 'sold', 'terapeak', 'social', 'chart', 'research'],
355
+ providers: buildProviderDebug(request, ebay, sold, terapeak, social, chart, research),
356
+ queryContextUsed: requestQueryResolution.queryContextUsed,
357
+ querySource: requestQueryResolution.querySource,
358
+ resolvedSearchQuery: requestQueryResolution.resolvedSearchQuery,
359
+ validationScope: requestQueryResolution.validationScope,
360
+ queryScope: requestQueryResolution.queryScope,
361
+ },
362
+ };
363
+ }
364
+ catch (error) {
365
+ return {
366
+ status: 'error',
367
+ validationId: request.validationId,
368
+ errorCode: mapErrorCode(error),
369
+ message: error instanceof Error ? error.message : String(error),
370
+ retryable: true,
371
+ nextCheckAt: addMinutes(request.timestamp, 30),
372
+ };
373
+ }
374
+ }
@@ -0,0 +1,105 @@
1
+ import { z } from 'zod';
2
+ export const trackingCadenceSchema = z.enum(['Daily', 'Hourly', 'Off']);
3
+ export const validationCurrentMetricsSchema = z.object({
4
+ avgWatchersPerListing: z.number().nullable(),
5
+ preOrderListingsCount: z.number().nullable(),
6
+ twitterTrending: z.boolean(),
7
+ youtubeViews24hMillions: z.number().nullable(),
8
+ redditPostsCount7d: z.number().nullable(),
9
+ marketPriceUsd: z.number().nullable(),
10
+ avgShippingCostUsd: z.number().nullable(),
11
+ competitionLevel: z.number().nullable(),
12
+ marketPriceTrend: z.string(),
13
+ day1Sold: z.number().nullable(),
14
+ day2Sold: z.number().nullable(),
15
+ day3Sold: z.number().nullable(),
16
+ day4Sold: z.number().nullable(),
17
+ day5Sold: z.number().nullable(),
18
+ daysTracked: z.number().nullable(),
19
+ });
20
+ export const validationQueryContextSchema = z.object({
21
+ resolvedSearchQuery: z.string().nullable().optional(),
22
+ validationScope: z.string().nullable().optional(),
23
+ queryScope: z.string().nullable().optional(),
24
+ });
25
+ export const validationRunRequestSchema = z.object({
26
+ validationId: z.string().min(1),
27
+ runType: z.enum(['scheduled', 'manual']),
28
+ cadence: trackingCadenceSchema,
29
+ timestamp: z.string().datetime({ offset: true }),
30
+ item: z.object({
31
+ recordId: z.string().min(1),
32
+ name: z.string().min(1),
33
+ variation: z.array(z.string()),
34
+ itemType: z.array(z.string()),
35
+ releaseType: z.array(z.string()),
36
+ releaseDate: z.string().datetime({ offset: true }).nullable(),
37
+ releasePeriod: z.array(z.string()),
38
+ availability: z.array(z.string()),
39
+ wholesalePrice: z.number().nullable(),
40
+ supplierNames: z.array(z.string()),
41
+ canonicalArtists: z.array(z.string()),
42
+ relatedAlbums: z.array(z.string()),
43
+ }),
44
+ validation: z.object({
45
+ validationType: z.string(),
46
+ buyDecision: z.string(),
47
+ automationStatus: z.string(),
48
+ autoCheckEnabled: z.boolean(),
49
+ dDay: z.number().nullable(),
50
+ artistTier: z.string(),
51
+ initialBudget: z.number().nullable(),
52
+ reserveBudget: z.number().nullable(),
53
+ queryContext: validationQueryContextSchema.optional(),
54
+ currentMetrics: validationCurrentMetricsSchema,
55
+ }),
56
+ });
57
+ export const validationWritesSchema = z.object({
58
+ avgWatchersPerListing: z.number().nullable().optional(),
59
+ preOrderListingsCount: z.number().nullable().optional(),
60
+ twitterTrending: z.boolean().optional(),
61
+ youtubeViews24hMillions: z.number().nullable().optional(),
62
+ redditPostsCount7d: z.number().nullable().optional(),
63
+ marketPriceUsd: z.number().nullable().optional(),
64
+ avgShippingCostUsd: z.number().nullable().optional(),
65
+ competitionLevel: z.number().nullable().optional(),
66
+ marketPriceTrend: z.string().optional(),
67
+ day1Sold: z.number().nullable().optional(),
68
+ day2Sold: z.number().nullable().optional(),
69
+ day3Sold: z.number().nullable().optional(),
70
+ day4Sold: z.number().nullable().optional(),
71
+ day5Sold: z.number().nullable().optional(),
72
+ daysTracked: z.number().nullable().optional(),
73
+ previousPobAvgPriceUsd: z.number().nullable().optional(),
74
+ previousPobSellThroughPct: z.number().nullable().optional(),
75
+ previousComebackFirstWeekSales: z.number().nullable().optional(),
76
+ monitoringNotes: z.string().optional(),
77
+ lastDataSnapshot: z.string().optional(),
78
+ latestAiRecommendation: z.string().optional(),
79
+ latestAiConfidence: z.enum(['High', 'Medium', 'Low']).optional(),
80
+ validationError: z.string().optional(),
81
+ });
82
+ export const validationDecisionSchema = z.object({
83
+ buyDecision: z.string().optional(),
84
+ automationStatus: z.string().optional(),
85
+ trackingCadence: trackingCadenceSchema.optional(),
86
+ shouldAutoTrack: z.boolean().optional(),
87
+ nextCheckAt: z.string().datetime({ offset: true }).nullable().optional(),
88
+ });
89
+ export const validationRunResponseSchema = z.discriminatedUnion('status', [
90
+ z.object({
91
+ status: z.literal('ok'),
92
+ validationId: z.string(),
93
+ writes: validationWritesSchema.optional(),
94
+ decision: validationDecisionSchema.optional(),
95
+ debug: z.record(z.string(), z.unknown()).optional(),
96
+ }),
97
+ z.object({
98
+ status: z.literal('error'),
99
+ validationId: z.string(),
100
+ errorCode: z.string(),
101
+ message: z.string(),
102
+ retryable: z.boolean().optional(),
103
+ nextCheckAt: z.string().datetime({ offset: true }).nullable().optional(),
104
+ }),
105
+ ]);
@@ -0,0 +1 @@
1
+ export {};