YeboShops Services
Complete documentation of every service in the YeboShops API.
Service Overview
| Service | Purpose | Key Methods |
|---|---|---|
| SecurePaymentService | Escrow payment processing | create, accept, refuse, complete, dispute |
| WalletService | User wallet management | createWallet, updateBalance, confirmBalance |
| EmbeddingService | Vector search with pgvector | generateEmbeddings, searchProducts |
| SearchService | AI-powered product search | search, expandQuery, getTrending |
| ChatService | Buyer-seller messaging | createMessage, getMessages, getUserChats |
| ProductService | Product CRUD & AI processing | create, update, processWebhook |
| ShopService | Shop management | create, update, getProducts |
| NotificationService | Push notifications | createNotification, markRead |
SecurePaymentService
The escrow payment service handles the complete lifecycle of P2P transactions.
Payment Flow
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ PENDING │────►│ACCEPTED │────►│COMPLETED│ │CANCELLED│
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ ▲
│ │ │
▼ ▼ │
┌─────────┐ ┌─────────┐ │
│ REFUSED │ │DISPUTED │─────────────────────────┘
└─────────┘ └─────────┘Key Methods
typescript
// Create a secure payment (debits buyer's wallet)
static async createSecurePayment(data: CreateSecurePaymentRequest): Promise<SecurePayment> {
// 1. Validate buyer exists
// 2. Validate shop exists and is active
// 3. Check buyer's wallet balance
// 4. Debit buyer's wallet
// 5. Generate unique completion code (6 digits)
// 6. Generate unique payment ID (SP + timestamp + random)
// 7. Create SecurePayment record with PENDING status
// 8. Log payment action for audit trail
}
// Shop owner accepts payment (adds to unconfirmed balance)
static async acceptSecurePayment(data: AcceptSecurePaymentRequest): Promise<SecurePayment> {
// 1. Find payment by paymentId
// 2. Verify payment is PENDING
// 3. Verify user is shop owner
// 4. Add amount to shop owner's unconfirmed balance
// 5. Update status to ACCEPTED
}
// Complete payment using completion code (confirms balance)
static async completeSecurePayment(data: CompleteSecurePaymentRequest): Promise<SecurePayment> {
// 1. Find payment by completion code
// 2. Verify payment is ACCEPTED (not PENDING)
// 3. Verify user is shop owner
// 4. Move amount from unconfirmed to confirmed balance
// 5. Update status to COMPLETED
}
// Refuse payment (refunds buyer)
static async refuseSecurePayment(data: RefuseSecurePaymentRequest): Promise<SecurePayment> {
// 1. Find payment by paymentId
// 2. Verify payment is PENDING
// 3. Verify user is shop owner
// 4. Refund buyer's wallet
// 5. Update status to REFUSED
}Payment States
| Status | Description | Next States |
|---|---|---|
PENDING | Payment created, awaiting seller response | ACCEPTED, REFUSED |
ACCEPTED | Seller accepted, funds in escrow | COMPLETED, DISPUTED |
REFUSED | Seller refused, buyer refunded | — |
COMPLETED | Transaction finalized | — |
DISPUTED | Under dispute review | CANCELLED |
CANCELLED | Cancelled, funds returned | — |
WalletService
Manages user wallets with confirmed and unconfirmed balances.
Key Methods
typescript
// Create wallet from user's country profile
static async createWalletFromUserProfile(userId: string): Promise<Wallet> {
const user = await db.user.findUnique({ include: { country: true } });
const currency = user.country?.currency?.code;
return this.createWalletForUser(userId, currency);
}
// Ensure user has a wallet (create if doesn't exist)
static async ensureUserHasWallet(userId: string): Promise<Wallet> {
const existing = await db.wallet.findFirst({ where: { userId, isActive: true } });
if (existing) return existing;
return this.createWalletFromUserProfile(userId);
}
// Update balance with transaction logging
static async updateBalance(
walletId: string,
amount: number,
type: 'credit' | 'debit',
description?: string,
reference?: string,
referenceType?: WalletRefType
): Promise<Wallet> {
// Calculate new balance
// Verify sufficient funds for debit
// Update wallet balance
// Create WalletTransaction record
}
// Confirm balance (move from unconfirmed to confirmed)
static async confirmBalance(walletId: string, amount: number): Promise<Wallet> {
// Verify sufficient unconfirmed balance
// Add to confirmed balance
// Subtract from unconfirmed balance
// Create transaction record
}Balance Types
| Type | Purpose |
|---|---|
balance | Confirmed, withdrawable funds |
unconfirmedBalance | Pending transactions (escrow) |
EmbeddingService
Handles vector embeddings using Voyage AI and pgvector for semantic search.
Configuration
typescript
const VOYAGE_API_KEY = process.env.VOYAGE_API_KEY;
const VOYAGE_MODEL = 'voyage-3-lite'; // 512 dimensions
const BATCH_SIZE = 50;Key Methods
typescript
// Generate embeddings via Voyage AI
static async generateEmbeddings(texts: string[]): Promise<number[][]> {
const response = await fetch('https://api.voyageai.com/v1/embeddings', {
method: 'POST',
headers: {
'Authorization': `Bearer ${VOYAGE_API_KEY}`,
},
body: JSON.stringify({
input: texts,
model: VOYAGE_MODEL,
}),
});
return result.data.map(item => item.embedding);
}
// Create embedding text for a product
static createEmbeddingText(product: any): string {
return [
product.title,
product.description,
product.category,
product.tags?.join(' '),
product.condition,
product.color?.join(' ')
].filter(Boolean).join(' ');
}
// Search products using vector similarity
static async searchProducts(
queryEmbedding: number[],
filters: SearchFilters,
limit: number = 20
): Promise<any[]> {
// Raw SQL with pgvector cosine distance
const results = await prisma.$queryRawUnsafe(`
SELECT
p.id, p.title, p.slug, p.category,
(p.embedding::vector <=> $1::vector) as distance,
(1 - (p.embedding::vector <=> $1::vector)) as score
FROM "Product" p
WHERE p.status = 'PUBLISHED'
AND p."isActive" = true
AND p.embedding IS NOT NULL
ORDER BY p.embedding::vector <=> $1::vector
LIMIT $2
`, JSON.stringify(queryEmbedding), limit);
return results;
}
// Find similar products
static async findSimilarProducts(productId: string, limit: number = 10): Promise<any[]> {
// Uses vector similarity between products
}Vector Index
sql
-- HNSW index for fast cosine similarity search
CREATE INDEX product_embedding_cosine_idx
ON "Product" USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);SearchService
Combines AI query expansion with vector search for intelligent product discovery.
Key Methods
typescript
// Expand natural language query using Gemini
static async expandQuery(query: string): Promise<SearchIntent> {
const prompt = `Analyze this search query for an African classifieds marketplace...`;
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent`, {
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
temperature: 0.1,
responseMimeType: 'application/json'
}
})
});
return {
keywords: ['blue', 'sneakers', 'shoes'],
categories: ['Fashion & Apparel'],
priceRange: { max: 50 },
condition: null,
intent: 'looking for affordable blue sneakers',
confidence: 0.9
};
}
// Vector-first search with keyword fallback
static async search(query: string, filters: SearchFilters): Promise<SearchResponse> {
// 1. Generate query embedding
const [queryEmbedding] = await EmbeddingService.generateEmbeddings([query]);
// 2. Search using vector similarity
let vectorResults = await EmbeddingService.searchProducts(queryEmbedding, filters);
// 3. If insufficient results, add keyword fallback
if (vectorResults.length < 3) {
const keywordProducts = await prisma.product.findMany({
where: {
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ description: { contains: query, mode: 'insensitive' } },
{ tags: { hasSome: keywords } }
]
}
});
results.push(...keywordProducts);
}
return { results, totalCount, query, searchTime };
}ChatService
Handles buyer-seller messaging with product context awareness.
Key Methods
typescript
// Create a new message
static async createMessage(data: CreateMessageData, userId: string): Promise<CreateMessageResponse> {
// 1. Find or create chat
// 2. Handle product context (extract from message or inherit from chat)
// 3. Determine sender type (USER or SHOP)
// 4. Create message with attachments
// 5. Create notification for recipient
// 6. Generate suggested actions
}
// Get messages with enhanced pagination
static async getMessages(chatId: string, query: GetMessagesQuery): Promise<GetMessagesResponse> {
// Cursor-based pagination with 'before' and 'after'
// Pre-fetch all senders for efficiency
// Enhance messages with computed data
}
// Get user's chats with unread counts
static async getUserChatsWithUnreadCounts(userId: string): Promise<{
chats: EnrichedChat[];
pagination: { nextCursor: string | null; hasMore: boolean };
}> {
// Aggregate unread counts
// Get latest messages and product contexts
// Determine recipient based on viewer's perspective
}Message Types
typescript
enum MessageType {
TEXT // Regular text message
PRODUCT_SHARE // Sharing a product into the chat
PRODUCT_INQUIRY // Initial inquiry about a product
IMAGE // Image attachment
SYSTEM // System messages
}ProductService
Manages product lifecycle including AI-powered processing.
Key Methods
typescript
// Create a product
async function createProduct(params: CreateProductParams) {
// 1. Validate shop ownership
// 2. Generate unique slug
// 3. Create product in DRAFT status
// 4. Create ProductStats record
}
// Process AI webhook data
async function processProductMediaWebhook(params: { productId: string; processedData: any }) {
// 1. Map webhook data to product fields
// 2. Update title, description, category, tags
// 3. Mark AI processing as completed
// 4. Change status from DRAFT to PUBLISHED
// 5. Generate and store embedding
}
// Get products for feed
async function getFeedProducts(query: any, userId?: string) {
// Filter by status: PUBLISHED, isActive: true
// Order by viewCount, favoriteCount, createdAt
// Return with sanitized data for frontend
}Product Status Flow
DRAFT ──► PUBLISHED ──► ARCHIVED
│
▼
REJECTED