feat(platform): marketplace module — types, repository, 52 tests passing
This commit is contained in:
parent
383a8dad32
commit
761a0c17f4
@ -0,0 +1,415 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
LISTING_STATUSES,
|
||||
LISTING_CATEGORIES,
|
||||
CERTIFICATION_STATUSES,
|
||||
REPORT_STATUSES,
|
||||
REPORT_REASONS,
|
||||
CreateListingSchema,
|
||||
UpdateListingSchema,
|
||||
CreateReviewSchema,
|
||||
CreateReportSchema,
|
||||
ResolveReportSchema,
|
||||
CertificationDecisionSchema,
|
||||
BrowseCatalogQuerySchema,
|
||||
ListQuerySchema,
|
||||
} from './types.js';
|
||||
import {
|
||||
buildListingDoc,
|
||||
applyListingUpdate,
|
||||
buildReviewDoc,
|
||||
buildInstallDoc,
|
||||
buildCertificationDoc,
|
||||
buildReportDoc,
|
||||
buildCatalogSortClause,
|
||||
buildCatalogWhereClause,
|
||||
buildListWhereClause,
|
||||
} from './repository.js';
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────
|
||||
|
||||
describe('marketplace constants', () => {
|
||||
it('has expected listing statuses', () => {
|
||||
expect(LISTING_STATUSES).toEqual(['draft', 'pending', 'published', 'suspended']);
|
||||
});
|
||||
|
||||
it('has expected listing categories', () => {
|
||||
expect(LISTING_CATEGORIES.length).toBe(9);
|
||||
expect(LISTING_CATEGORIES).toContain('coaching');
|
||||
expect(LISTING_CATEGORIES).toContain('other');
|
||||
});
|
||||
|
||||
it('has expected certification statuses', () => {
|
||||
expect(CERTIFICATION_STATUSES).toEqual(['pending', 'approved', 'rejected']);
|
||||
});
|
||||
|
||||
it('has expected report statuses', () => {
|
||||
expect(REPORT_STATUSES).toEqual(['open', 'investigating', 'resolved', 'dismissed']);
|
||||
});
|
||||
|
||||
it('has expected report reasons', () => {
|
||||
expect(REPORT_REASONS.length).toBe(6);
|
||||
expect(REPORT_REASONS).toContain('inappropriate_content');
|
||||
expect(REPORT_REASONS).toContain('safety_concern');
|
||||
});
|
||||
});
|
||||
|
||||
// ── CreateListingSchema ─────────────────────────────────────
|
||||
|
||||
describe('CreateListingSchema', () => {
|
||||
const validInput = {
|
||||
title: 'Test Agent',
|
||||
description: 'A test coaching agent.',
|
||||
category: 'coaching' as const,
|
||||
agentConfig: { name: 'Test', role: 'Tester' },
|
||||
};
|
||||
|
||||
it('accepts minimal valid input with defaults', () => {
|
||||
const result = CreateListingSchema.parse(validInput);
|
||||
expect(result.title).toBe('Test Agent');
|
||||
expect(result.tags).toEqual([]);
|
||||
expect(result.version).toBe('1.0.0');
|
||||
});
|
||||
|
||||
it('accepts full input with all fields', () => {
|
||||
const result = CreateListingSchema.parse({
|
||||
...validInput,
|
||||
tags: ['coaching', 'leadership'],
|
||||
version: '2.0.0',
|
||||
});
|
||||
expect(result.tags).toEqual(['coaching', 'leadership']);
|
||||
expect(result.version).toBe('2.0.0');
|
||||
});
|
||||
|
||||
it('rejects missing title', () => {
|
||||
expect(() => CreateListingSchema.parse({ ...validInput, title: undefined })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects missing description', () => {
|
||||
expect(() => CreateListingSchema.parse({ ...validInput, description: undefined })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects missing category', () => {
|
||||
expect(() => CreateListingSchema.parse({ ...validInput, category: undefined })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects invalid category', () => {
|
||||
expect(() => CreateListingSchema.parse({ ...validInput, category: 'invalid' })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects missing agentConfig', () => {
|
||||
expect(() => CreateListingSchema.parse({ ...validInput, agentConfig: undefined })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects title exceeding max length', () => {
|
||||
expect(() => CreateListingSchema.parse({ ...validInput, title: 'x'.repeat(201) })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects too many tags', () => {
|
||||
const tags = Array.from({ length: 11 }, (_, i) => `tag${i}`);
|
||||
expect(() => CreateListingSchema.parse({ ...validInput, tags })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ── UpdateListingSchema ─────────────────────────────────────
|
||||
|
||||
describe('UpdateListingSchema', () => {
|
||||
it('accepts partial update with single field', () => {
|
||||
const result = UpdateListingSchema.parse({ title: 'Updated' });
|
||||
expect(result.title).toBe('Updated');
|
||||
expect(result.description).toBeUndefined();
|
||||
});
|
||||
|
||||
it('accepts empty update (no fields)', () => {
|
||||
const result = UpdateListingSchema.parse({});
|
||||
expect(Object.keys(result)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('rejects invalid category in update', () => {
|
||||
expect(() => UpdateListingSchema.parse({ category: 'invalid' })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ── CreateReviewSchema ──────────────────────────────────────
|
||||
|
||||
describe('CreateReviewSchema', () => {
|
||||
it('accepts valid review', () => {
|
||||
const result = CreateReviewSchema.parse({ rating: 5, comment: 'Great agent!' });
|
||||
expect(result.rating).toBe(5);
|
||||
expect(result.comment).toBe('Great agent!');
|
||||
});
|
||||
|
||||
it('rejects rating below 1', () => {
|
||||
expect(() => CreateReviewSchema.parse({ rating: 0, comment: 'Bad' })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects rating above 5', () => {
|
||||
expect(() => CreateReviewSchema.parse({ rating: 6, comment: 'Too high' })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects empty comment', () => {
|
||||
expect(() => CreateReviewSchema.parse({ rating: 3, comment: '' })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects comment exceeding max length', () => {
|
||||
expect(() => CreateReviewSchema.parse({ rating: 3, comment: 'x'.repeat(2001) })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ── CreateReportSchema ──────────────────────────────────────
|
||||
|
||||
describe('CreateReportSchema', () => {
|
||||
it('accepts valid report', () => {
|
||||
const result = CreateReportSchema.parse({
|
||||
reason: 'spam',
|
||||
details: 'This listing is spam.',
|
||||
});
|
||||
expect(result.reason).toBe('spam');
|
||||
});
|
||||
|
||||
it('rejects invalid reason', () => {
|
||||
expect(() => CreateReportSchema.parse({ reason: 'invalid', details: 'test' })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects empty details', () => {
|
||||
expect(() => CreateReportSchema.parse({ reason: 'spam', details: '' })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ── ResolveReportSchema ─────────────────────────────────────
|
||||
|
||||
describe('ResolveReportSchema', () => {
|
||||
it('accepts valid resolution', () => {
|
||||
const result = ResolveReportSchema.parse({ resolution: 'Listing suspended.' });
|
||||
expect(result.resolution).toBe('Listing suspended.');
|
||||
});
|
||||
|
||||
it('rejects empty resolution', () => {
|
||||
expect(() => ResolveReportSchema.parse({ resolution: '' })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ── CertificationDecisionSchema ─────────────────────────────
|
||||
|
||||
describe('CertificationDecisionSchema', () => {
|
||||
it('accepts with notes', () => {
|
||||
const result = CertificationDecisionSchema.parse({ notes: 'Looks good.' });
|
||||
expect(result.notes).toBe('Looks good.');
|
||||
});
|
||||
|
||||
it('defaults notes to empty string', () => {
|
||||
const result = CertificationDecisionSchema.parse({});
|
||||
expect(result.notes).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
// ── BrowseCatalogQuerySchema ────────────────────────────────
|
||||
|
||||
describe('BrowseCatalogQuerySchema', () => {
|
||||
it('applies defaults for empty query', () => {
|
||||
const result = BrowseCatalogQuerySchema.parse({});
|
||||
expect(result.sort).toBe('popular');
|
||||
expect(result.limit).toBe(20);
|
||||
expect(result.offset).toBe(0);
|
||||
});
|
||||
|
||||
it('accepts category filter', () => {
|
||||
const result = BrowseCatalogQuerySchema.parse({ category: 'coaching' });
|
||||
expect(result.category).toBe('coaching');
|
||||
});
|
||||
|
||||
it('accepts search term', () => {
|
||||
const result = BrowseCatalogQuerySchema.parse({ search: 'interview' });
|
||||
expect(result.search).toBe('interview');
|
||||
});
|
||||
|
||||
it('rejects invalid sort', () => {
|
||||
expect(() => BrowseCatalogQuerySchema.parse({ sort: 'invalid' })).toThrow();
|
||||
});
|
||||
|
||||
it('rejects limit exceeding max', () => {
|
||||
expect(() => BrowseCatalogQuerySchema.parse({ limit: 101 })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ── ListQuerySchema ─────────────────────────────────────────
|
||||
|
||||
describe('ListQuerySchema', () => {
|
||||
it('applies defaults for empty query', () => {
|
||||
const result = ListQuerySchema.parse({});
|
||||
expect(result.limit).toBe(50);
|
||||
expect(result.offset).toBe(0);
|
||||
});
|
||||
|
||||
it('accepts status filter', () => {
|
||||
const result = ListQuerySchema.parse({ status: 'pending' });
|
||||
expect(result.status).toBe('pending');
|
||||
});
|
||||
|
||||
it('rejects invalid status', () => {
|
||||
expect(() => ListQuerySchema.parse({ status: 'invalid' })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Repository Builders ─────────────────────────────────────
|
||||
|
||||
describe('buildListingDoc', () => {
|
||||
it('creates a listing with correct fields', () => {
|
||||
const doc = buildListingDoc(
|
||||
{
|
||||
title: 'Test',
|
||||
description: 'Desc',
|
||||
category: 'coaching',
|
||||
agentConfig: { name: 'Test' },
|
||||
tags: ['tag1'],
|
||||
version: '1.0.0',
|
||||
},
|
||||
'user_1',
|
||||
'jarvisjr'
|
||||
);
|
||||
expect(doc.id).toMatch(/^listing_/);
|
||||
expect(doc.authorId).toBe('user_1');
|
||||
expect(doc.productId).toBe('jarvisjr');
|
||||
expect(doc.status).toBe('draft');
|
||||
expect(doc.downloads).toBe(0);
|
||||
expect(doc.rating).toBe(0);
|
||||
expect(doc.isFeatured).toBe(false);
|
||||
expect(doc.isVerified).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyListingUpdate', () => {
|
||||
it('updates specified fields only', () => {
|
||||
const original = buildListingDoc(
|
||||
{
|
||||
title: 'Original',
|
||||
description: 'Desc',
|
||||
category: 'coaching',
|
||||
agentConfig: {},
|
||||
tags: [],
|
||||
version: '1.0.0',
|
||||
},
|
||||
'user_1',
|
||||
'jarvisjr'
|
||||
);
|
||||
const updated = applyListingUpdate(original, { title: 'Updated' });
|
||||
expect(updated.title).toBe('Updated');
|
||||
expect(updated.description).toBe('Desc');
|
||||
expect(updated.updatedAt).toBeTruthy();
|
||||
expect(new Date(updated.updatedAt).toISOString()).toBe(updated.updatedAt);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildReviewDoc', () => {
|
||||
it('creates a review with correct fields', () => {
|
||||
const doc = buildReviewDoc({ rating: 4, comment: 'Good' }, 'listing_1', 'user_1', 'jarvisjr');
|
||||
expect(doc.id).toMatch(/^review_/);
|
||||
expect(doc.listingId).toBe('listing_1');
|
||||
expect(doc.rating).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildInstallDoc', () => {
|
||||
it('creates an install with correct fields', () => {
|
||||
const doc = buildInstallDoc('listing_1', 'user_1', 'jarvisjr');
|
||||
expect(doc.id).toMatch(/^install_/);
|
||||
expect(doc.listingId).toBe('listing_1');
|
||||
expect(doc.userId).toBe('user_1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildCertificationDoc', () => {
|
||||
it('creates a certification with pending status', () => {
|
||||
const doc = buildCertificationDoc('listing_1', 'jarvisjr');
|
||||
expect(doc.id).toMatch(/^cert_/);
|
||||
expect(doc.status).toBe('pending');
|
||||
expect(doc.reviewerId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildReportDoc', () => {
|
||||
it('creates a report with open status', () => {
|
||||
const doc = buildReportDoc(
|
||||
{ reason: 'spam', details: 'This is spam.' },
|
||||
'listing_1',
|
||||
'user_1',
|
||||
'jarvisjr'
|
||||
);
|
||||
expect(doc.id).toMatch(/^report_/);
|
||||
expect(doc.status).toBe('open');
|
||||
expect(doc.resolvedBy).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Query Helpers ───────────────────────────────────────────
|
||||
|
||||
describe('buildCatalogSortClause', () => {
|
||||
it('returns downloads DESC for popular', () => {
|
||||
expect(buildCatalogSortClause('popular')).toContain('downloads DESC');
|
||||
});
|
||||
|
||||
it('returns createdAt DESC for recent', () => {
|
||||
expect(buildCatalogSortClause('recent')).toContain('createdAt DESC');
|
||||
});
|
||||
|
||||
it('returns rating DESC for rating', () => {
|
||||
expect(buildCatalogSortClause('rating')).toContain('rating DESC');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildCatalogWhereClause', () => {
|
||||
it('builds basic where clause', () => {
|
||||
const { where, parameters } = buildCatalogWhereClause(
|
||||
{ sort: 'popular', limit: 20, offset: 0 },
|
||||
'jarvisjr'
|
||||
);
|
||||
expect(where).toContain('c.productId = @productId');
|
||||
expect(where).toContain("c.status = 'published'");
|
||||
expect(parameters).toContainEqual({ name: '@productId', value: 'jarvisjr' });
|
||||
});
|
||||
|
||||
it('adds category filter', () => {
|
||||
const { where, parameters } = buildCatalogWhereClause(
|
||||
{ category: 'coaching', sort: 'popular', limit: 20, offset: 0 },
|
||||
'jarvisjr'
|
||||
);
|
||||
expect(where).toContain('c.category = @category');
|
||||
expect(parameters).toContainEqual({ name: '@category', value: 'coaching' });
|
||||
});
|
||||
|
||||
it('adds search filter', () => {
|
||||
const { where, parameters } = buildCatalogWhereClause(
|
||||
{ search: 'interview', sort: 'popular', limit: 20, offset: 0 },
|
||||
'jarvisjr'
|
||||
);
|
||||
expect(where).toContain('CONTAINS(LOWER(c.title), LOWER(@search))');
|
||||
expect(parameters).toContainEqual({ name: '@search', value: 'interview' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildListWhereClause', () => {
|
||||
it('builds basic where clause', () => {
|
||||
const { where, parameters } = buildListWhereClause({ limit: 50, offset: 0 }, 'jarvisjr');
|
||||
expect(where).toContain('c.productId = @productId');
|
||||
expect(parameters).toContainEqual({ name: '@productId', value: 'jarvisjr' });
|
||||
});
|
||||
|
||||
it('adds status filter', () => {
|
||||
const { where, parameters } = buildListWhereClause(
|
||||
{ status: 'pending', limit: 50, offset: 0 },
|
||||
'jarvisjr'
|
||||
);
|
||||
expect(where).toContain('c.status = @status');
|
||||
expect(parameters).toContainEqual({ name: '@status', value: 'pending' });
|
||||
});
|
||||
|
||||
it('supports extra conditions', () => {
|
||||
const { where } = buildListWhereClause(
|
||||
{ limit: 50, offset: 0 },
|
||||
'jarvisjr',
|
||||
['c.authorId = @authorId'],
|
||||
[{ name: '@authorId', value: 'user_1' }]
|
||||
);
|
||||
expect(where).toContain('c.authorId = @authorId');
|
||||
});
|
||||
});
|
||||
194
services/platform-service/src/modules/marketplace/repository.ts
Normal file
194
services/platform-service/src/modules/marketplace/repository.ts
Normal file
@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Marketplace repository — Cosmos DB operations for listings, reviews, installs,
|
||||
* certifications, and reports.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ListingDoc,
|
||||
ReviewDoc,
|
||||
InstallDoc,
|
||||
CertificationDoc,
|
||||
ReportDoc,
|
||||
CreateListingInput,
|
||||
UpdateListingInput,
|
||||
CreateReviewInput,
|
||||
CreateReportInput,
|
||||
BrowseCatalogQuery,
|
||||
ListQuery,
|
||||
} from './types.js';
|
||||
|
||||
// ── Listings ────────────────────────────────────────────────
|
||||
|
||||
export function buildListingDoc(
|
||||
input: CreateListingInput,
|
||||
authorId: string,
|
||||
productId: string
|
||||
): ListingDoc {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: `listing_${crypto.randomUUID()}`,
|
||||
productId,
|
||||
authorId,
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
category: input.category,
|
||||
tags: input.tags ?? [],
|
||||
agentConfig: input.agentConfig,
|
||||
status: 'draft',
|
||||
version: input.version ?? '1.0.0',
|
||||
downloads: 0,
|
||||
rating: 0,
|
||||
reviewCount: 0,
|
||||
isFeatured: false,
|
||||
isVerified: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyListingUpdate(doc: ListingDoc, input: UpdateListingInput): ListingDoc {
|
||||
return {
|
||||
...doc,
|
||||
...(input.title !== undefined && { title: input.title }),
|
||||
...(input.description !== undefined && { description: input.description }),
|
||||
...(input.category !== undefined && { category: input.category }),
|
||||
...(input.tags !== undefined && { tags: input.tags }),
|
||||
...(input.agentConfig !== undefined && { agentConfig: input.agentConfig }),
|
||||
...(input.version !== undefined && { version: input.version }),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Reviews ─────────────────────────────────────────────────
|
||||
|
||||
export function buildReviewDoc(
|
||||
input: CreateReviewInput,
|
||||
listingId: string,
|
||||
userId: string,
|
||||
productId: string
|
||||
): ReviewDoc {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: `review_${crypto.randomUUID()}`,
|
||||
listingId,
|
||||
userId,
|
||||
productId,
|
||||
rating: input.rating,
|
||||
comment: input.comment,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Installs ────────────────────────────────────────────────
|
||||
|
||||
export function buildInstallDoc(listingId: string, userId: string, productId: string): InstallDoc {
|
||||
return {
|
||||
id: `install_${crypto.randomUUID()}`,
|
||||
listingId,
|
||||
userId,
|
||||
productId,
|
||||
installedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Certifications ──────────────────────────────────────────
|
||||
|
||||
export function buildCertificationDoc(listingId: string, productId: string): CertificationDoc {
|
||||
return {
|
||||
id: `cert_${crypto.randomUUID()}`,
|
||||
listingId,
|
||||
productId,
|
||||
status: 'pending',
|
||||
reviewerId: null,
|
||||
notes: '',
|
||||
submittedAt: new Date().toISOString(),
|
||||
reviewedAt: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Reports ─────────────────────────────────────────────────
|
||||
|
||||
export function buildReportDoc(
|
||||
input: CreateReportInput,
|
||||
listingId: string,
|
||||
reporterId: string,
|
||||
productId: string
|
||||
): ReportDoc {
|
||||
return {
|
||||
id: `report_${crypto.randomUUID()}`,
|
||||
listingId,
|
||||
reporterId,
|
||||
productId,
|
||||
reason: input.reason,
|
||||
details: input.details,
|
||||
status: 'open',
|
||||
resolvedBy: null,
|
||||
resolution: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
resolvedAt: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Query Helpers ───────────────────────────────────────────
|
||||
|
||||
export function buildCatalogSortClause(sort: BrowseCatalogQuery['sort']): string {
|
||||
switch (sort) {
|
||||
case 'popular':
|
||||
return 'ORDER BY c.downloads DESC';
|
||||
case 'recent':
|
||||
return 'ORDER BY c.createdAt DESC';
|
||||
case 'rating':
|
||||
return 'ORDER BY c.rating DESC';
|
||||
default:
|
||||
return 'ORDER BY c.downloads DESC';
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCatalogWhereClause(
|
||||
query: BrowseCatalogQuery,
|
||||
productId: string
|
||||
): { where: string; parameters: Array<{ name: string; value: unknown }> } {
|
||||
const conditions = ['c.productId = @productId', "c.status = 'published'"];
|
||||
const parameters: Array<{ name: string; value: unknown }> = [
|
||||
{ name: '@productId', value: productId },
|
||||
];
|
||||
|
||||
if (query.category) {
|
||||
conditions.push('c.category = @category');
|
||||
parameters.push({ name: '@category', value: query.category });
|
||||
}
|
||||
|
||||
if (query.search) {
|
||||
conditions.push('CONTAINS(LOWER(c.title), LOWER(@search))');
|
||||
parameters.push({ name: '@search', value: query.search });
|
||||
}
|
||||
|
||||
return {
|
||||
where: conditions.join(' AND '),
|
||||
parameters,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildListWhereClause(
|
||||
query: ListQuery,
|
||||
productId: string,
|
||||
extraConditions?: string[],
|
||||
extraParams?: Array<{ name: string; value: unknown }>
|
||||
): { where: string; parameters: Array<{ name: string; value: unknown }> } {
|
||||
const conditions = ['c.productId = @productId', ...(extraConditions ?? [])];
|
||||
const parameters: Array<{ name: string; value: unknown }> = [
|
||||
{ name: '@productId', value: productId },
|
||||
...(extraParams ?? []),
|
||||
];
|
||||
|
||||
if (query.status) {
|
||||
conditions.push('c.status = @status');
|
||||
parameters.push({ name: '@status', value: query.status });
|
||||
}
|
||||
|
||||
return {
|
||||
where: conditions.join(' AND '),
|
||||
parameters,
|
||||
};
|
||||
}
|
||||
167
services/platform-service/src/modules/marketplace/types.ts
Normal file
167
services/platform-service/src/modules/marketplace/types.ts
Normal file
@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Generic marketplace types — product-agnostic.
|
||||
* Listings, reviews, installs, certifications, reports.
|
||||
* Partition key: /productId for listings, /userId for installs/reviews.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────
|
||||
|
||||
export const LISTING_STATUSES = ['draft', 'pending', 'published', 'suspended'] as const;
|
||||
export const LISTING_CATEGORIES = [
|
||||
'coaching',
|
||||
'language',
|
||||
'creativity',
|
||||
'productivity',
|
||||
'wellness',
|
||||
'education',
|
||||
'business',
|
||||
'entertainment',
|
||||
'other',
|
||||
] as const;
|
||||
export const CERTIFICATION_STATUSES = ['pending', 'approved', 'rejected'] as const;
|
||||
export const REPORT_STATUSES = ['open', 'investigating', 'resolved', 'dismissed'] as const;
|
||||
export const REPORT_REASONS = [
|
||||
'inappropriate_content',
|
||||
'misleading',
|
||||
'spam',
|
||||
'intellectual_property',
|
||||
'safety_concern',
|
||||
'other',
|
||||
] as const;
|
||||
|
||||
export type ListingStatus = (typeof LISTING_STATUSES)[number];
|
||||
export type ListingCategory = (typeof LISTING_CATEGORIES)[number];
|
||||
export type CertificationStatus = (typeof CERTIFICATION_STATUSES)[number];
|
||||
export type ReportStatus = (typeof REPORT_STATUSES)[number];
|
||||
export type ReportReason = (typeof REPORT_REASONS)[number];
|
||||
|
||||
// ── Document Interfaces ─────────────────────────────────────
|
||||
|
||||
export interface ListingDoc {
|
||||
id: string;
|
||||
productId: string;
|
||||
authorId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: ListingCategory;
|
||||
tags: string[];
|
||||
agentConfig: Record<string, unknown>;
|
||||
status: ListingStatus;
|
||||
version: string;
|
||||
downloads: number;
|
||||
rating: number;
|
||||
reviewCount: number;
|
||||
isFeatured: boolean;
|
||||
isVerified: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ReviewDoc {
|
||||
id: string;
|
||||
listingId: string;
|
||||
userId: string;
|
||||
productId: string;
|
||||
rating: number;
|
||||
comment: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface InstallDoc {
|
||||
id: string;
|
||||
listingId: string;
|
||||
userId: string;
|
||||
productId: string;
|
||||
installedAt: string;
|
||||
}
|
||||
|
||||
export interface CertificationDoc {
|
||||
id: string;
|
||||
listingId: string;
|
||||
productId: string;
|
||||
status: CertificationStatus;
|
||||
reviewerId: string | null;
|
||||
notes: string;
|
||||
submittedAt: string;
|
||||
reviewedAt: string | null;
|
||||
}
|
||||
|
||||
export interface ReportDoc {
|
||||
id: string;
|
||||
listingId: string;
|
||||
reporterId: string;
|
||||
productId: string;
|
||||
reason: ReportReason;
|
||||
details: string;
|
||||
status: ReportStatus;
|
||||
resolvedBy: string | null;
|
||||
resolution: string | null;
|
||||
createdAt: string;
|
||||
resolvedAt: string | null;
|
||||
}
|
||||
|
||||
// ── Zod Schemas ─────────────────────────────────────────────
|
||||
|
||||
export const CreateListingSchema = z.object({
|
||||
title: z.string().min(1).max(200),
|
||||
description: z.string().min(1).max(5000),
|
||||
category: z.enum(LISTING_CATEGORIES),
|
||||
tags: z.array(z.string().max(50)).max(10).default([]),
|
||||
agentConfig: z.record(z.unknown()),
|
||||
version: z.string().max(20).default('1.0.0'),
|
||||
});
|
||||
|
||||
export const UpdateListingSchema = z.object({
|
||||
title: z.string().min(1).max(200).optional(),
|
||||
description: z.string().min(1).max(5000).optional(),
|
||||
category: z.enum(LISTING_CATEGORIES).optional(),
|
||||
tags: z.array(z.string().max(50)).max(10).optional(),
|
||||
agentConfig: z.record(z.unknown()).optional(),
|
||||
version: z.string().max(20).optional(),
|
||||
});
|
||||
|
||||
export const CreateReviewSchema = z.object({
|
||||
rating: z.number().min(1).max(5),
|
||||
comment: z.string().min(1).max(2000),
|
||||
});
|
||||
|
||||
export const CreateReportSchema = z.object({
|
||||
reason: z.enum(REPORT_REASONS),
|
||||
details: z.string().min(1).max(2000),
|
||||
});
|
||||
|
||||
export const ResolveReportSchema = z.object({
|
||||
resolution: z.string().min(1).max(2000),
|
||||
});
|
||||
|
||||
export const CertificationDecisionSchema = z.object({
|
||||
notes: z.string().max(2000).default(''),
|
||||
});
|
||||
|
||||
export const BrowseCatalogQuerySchema = z.object({
|
||||
category: z.enum(LISTING_CATEGORIES).optional(),
|
||||
search: z.string().max(200).optional(),
|
||||
sort: z.enum(['popular', 'recent', 'rating']).default('popular'),
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
});
|
||||
|
||||
export const ListQuerySchema = z.object({
|
||||
status: z.enum(LISTING_STATUSES).optional(),
|
||||
limit: z.coerce.number().min(1).max(100).default(50),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
});
|
||||
|
||||
// ── Inferred Types ──────────────────────────────────────────
|
||||
|
||||
export type CreateListingInput = z.infer<typeof CreateListingSchema>;
|
||||
export type UpdateListingInput = z.infer<typeof UpdateListingSchema>;
|
||||
export type CreateReviewInput = z.infer<typeof CreateReviewSchema>;
|
||||
export type CreateReportInput = z.infer<typeof CreateReportSchema>;
|
||||
export type ResolveReportInput = z.infer<typeof ResolveReportSchema>;
|
||||
export type CertificationDecisionInput = z.infer<typeof CertificationDecisionSchema>;
|
||||
export type BrowseCatalogQuery = z.infer<typeof BrowseCatalogQuerySchema>;
|
||||
export type ListQuery = z.infer<typeof ListQuerySchema>;
|
||||
Loading…
Reference in New Issue
Block a user