Skip to content

YeboMart Data Models — Full Schema

Complete Prisma schema with all fields, types, and relationships.


Schema Overview

Shop (1) ──┬── (*) User (Staff)
           ├── (*) Product ──── (*) SaleItem
           ├── (*) Sale ──────── (*) SaleItem
           ├── (*) StockLog
           ├── (*) Expense
           ├── (*) DailyReport
           ├── (*) AIConversation
           ├── (*) AuditLog
           ├── (*) SyncQueue
           ├── (*) Customer ──── (*) CustomerCredit
           ├── (*) Return ────── (*) ReturnItem
           ├── (*) Supplier ──── (*) SupplierProduct
           └── (*) PurchaseOrder ─ (*) POItem

Core Models

Shop

The central entity representing a business.

prisma
model Shop {
  id            String    @id @default(cuid())
  name          String
  ownerName     String
  ownerPhone    String    @unique  // E.164 format (+26876123456)
  ownerEmail    String?
  password      String    // bcrypt hash
  
  // Business Type
  businessType  String    @default("general")
  // Options: general, tuckshop, grocery, hardware, pharmacy,
  //          salon, electronics, clothing, restaurant
  
  // AI Assistant
  assistantName String    @default("Yebo")
  
  // Country & Localization
  countryCode       String    @default("SZ")   // ISO: SZ, ZA, KE, NG, etc.
  phoneCountryCode  String    @default("+268")
  currencySymbol    String    @default("E")    // E, R, KSh, ₦, etc.
  currency          String    @default("SZL")  // ISO currency code
  timezone          String    @default("Africa/Mbabane")
  
  // Business Settings
  address       String?
  logoUrl       String?
  
  // License & Subscription
  tier          ShopTier  @default(LITE)
  status        ShopStatus @default(ACTIVE)
  licenseKey    String?   @unique
  licenseExpiry DateTime?
  
  // Usage Tracking (reset monthly)
  monthlyTransactions Int  @default(0)
  monthlyStockMoves   Int  @default(0)
  monthlyAiQueries    Int  @default(0)
  lastBillingReset    DateTime @default(now())
  
  // Timestamps
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  
  // Relations
  users         User[]
  products      Product[]
  sales         Sale[]
  expenses      Expense[]
  stockLogs     StockLog[]
  dailyReports  DailyReport[]
  syncQueue     SyncQueue[]
  aiConversations AIConversation[]
  auditLogs     AuditLog[]
  customers     Customer[]
  returns       Return[]
  suppliers     Supplier[]
  purchaseOrders PurchaseOrder[]
}

enum ShopTier {
  LITE        // E99/mo  - Basic POS
  STARTER     // E499/mo - + Barcode, Alerts
  BUSINESS    // E2499/mo - + WhatsApp, Analytics
  PRO         // E4999/mo - + AI Voice, Multi-loc
  ENTERPRISE  // E9999/mo - + API, Support
}

enum ShopStatus {
  ACTIVE
  SUSPENDED
}

User (Staff)

Staff members with role-based permissions.

prisma
model User {
  id            String    @id @default(cuid())
  shopId        String
  name          String
  phone         String
  email         String?
  password      String?   // Optional - staff can use PIN
  pin           String?   // 4-digit PIN for quick auth
  role          UserRole  @default(CASHIER)
  isActive      Boolean   @default(true)
  
  // Granular Permissions
  canDiscount   Boolean   @default(false)
  canVoid       Boolean   @default(false)
  canViewReports Boolean  @default(false)
  canManageStock Boolean  @default(false)
  
  // Session
  refreshToken  String?
  lastLoginAt   DateTime?
  
  // Timestamps
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  
  // Relations
  shop          Shop      @relation(fields: [shopId], references: [id], onDelete: Cascade)
  sales         Sale[]
  stockLogs     StockLog[]
  auditLogs     AuditLog[]
  
  @@unique([shopId, phone])
}

enum UserRole {
  OWNER     // Full access
  MANAGER   // Most features except billing
  CASHIER   // POS only
}

Product

Inventory items with flexible pricing.

prisma
model Product {
  id          String   @id @default(cuid())
  shopId      String
  barcode     String?
  sku         String?              // Internal SKU
  name        String
  description String?
  category    String?
  attributes  Json?                // Dynamic attributes {size, brand, color}
  
  // Pricing (all in local currency)
  costPrice       Float            // Purchase/cost price
  sellPrice       Float            // Retail unit price
  wholesalePrice  Float?           // Bulk discount price
  wholesaleMinQty Int?             // Min qty for wholesale
  packPrice       Float?           // Pack price (e.g., 6-pack)
  packSize        Int?             // Units per pack
  
  // Stock (always in single units)
  quantity    Int      @default(0)
  reorderAt   Int      @default(10) // Low stock threshold
  unit        String   @default("each") // each, kg, litre, pack
  
  // Flags
  isActive    Boolean  @default(true)
  trackStock  Boolean  @default(true)
  imageUrl    String?
  
  // Timestamps
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  
  // Offline Sync
  localId     String?  // Client-side ID
  syncedAt    DateTime?
  
  // Relations
  shop        Shop     @relation(fields: [shopId], references: [id], onDelete: Cascade)
  saleItems   SaleItem[]
  stockLogs   StockLog[]
  suppliers   SupplierProduct[]
  
  @@unique([shopId, barcode])
  @@index([shopId, name])
  @@index([shopId, category])
}

Transaction Models

Sale

Individual sales transactions.

prisma
model Sale {
  id            String   @id @default(cuid())
  shopId        String
  userId        String?           // Cashier
  customerId    String?           // Credit customer
  
  // Totals
  subtotal      Float
  discount      Float    @default(0)
  tax           Float    @default(0)
  totalAmount   Float
  
  // Payment
  paymentMethod PaymentMethod
  amountPaid    Float
  change        Float    @default(0)
  
  // Status
  status        SaleStatus @default(COMPLETED)
  voidReason    String?
  receiptNumber String?   // Format: RCP-YYMMDD-XXXX
  
  // Timestamps
  createdAt     DateTime @default(now())
  
  // Offline Sync
  localId       String?
  offlineAt     DateTime?
  syncedAt      DateTime?
  
  // Relations
  shop          Shop     @relation(fields: [shopId], references: [id], onDelete: Cascade)
  user          User?    @relation(fields: [userId], references: [id])
  customer      Customer? @relation("CustomerSales", fields: [customerId], references: [id])
  items         SaleItem[]
  creditEntries CustomerCredit[]
  
  @@index([shopId, createdAt])
  @@index([shopId, status])
  @@index([customerId])
}

enum PaymentMethod {
  CASH
  MOMO       // MTN Mobile Money
  EMALI      // Eswatini Mobile Money
  CARD
  MIXED
  CREDIT     // Store credit
}

enum SaleStatus {
  PENDING
  COMPLETED
  VOIDED
  REFUNDED
}

SaleItem

Line items within a sale.

prisma
model SaleItem {
  id          String   @id @default(cuid())
  saleId      String
  productId   String
  
  // Snapshot at time of sale
  productName String
  quantity    Int
  unitPrice   Float
  costPrice   Float    // For profit calculation
  discount    Float    @default(0)
  totalPrice  Float
  
  // Relations
  sale        Sale     @relation(fields: [saleId], references: [id], onDelete: Cascade)
  product     Product  @relation(fields: [productId], references: [id])
  
  @@index([saleId])
}

Inventory Models

StockLog

Audit trail for all inventory changes.

prisma
model StockLog {
  id          String    @id @default(cuid())
  shopId      String
  productId   String
  userId      String?   // Who made the change
  
  type        StockLogType
  quantity    Int       // +/- change amount
  
  // Audit trail
  previousQty Int
  newQty      Int
  
  note        String?
  reference   String?   // Sale ID, PO number, etc.
  
  // Timestamps
  createdAt   DateTime  @default(now())
  
  // Offline Sync
  localId     String?
  syncedAt    DateTime?
  
  // Relations
  shop        Shop      @relation(fields: [shopId], references: [id], onDelete: Cascade)
  product     Product   @relation(fields: [productId], references: [id], onDelete: Cascade)
  user        User?     @relation(fields: [userId], references: [id])
  
  @@index([shopId, createdAt])
  @@index([productId, createdAt])
}

enum StockLogType {
  SALE        // Sold
  RESTOCK     // Received
  ADJUSTMENT  // Manual
  DAMAGED     // Written off
  EXPIRED     // Expired
  TRANSFER    // Between locations
  RETURN      // Customer return
  INITIAL     // First count
}

Customer & Credit Models

Customer

Store credit customers.

prisma
model Customer {
  id          String   @id @default(cuid())
  shopId      String
  
  name        String
  phone       String?
  email       String?
  address     String?
  
  // Credit Management
  creditLimit Float    @default(0)
  balance     Float    @default(0)  // Positive = owes shop
  
  isActive    Boolean  @default(true)
  
  // Timestamps
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  
  // Relations
  shop        Shop     @relation(fields: [shopId], references: [id], onDelete: Cascade)
  credits     CustomerCredit[]
  sales       Sale[]   @relation("CustomerSales")
  
  @@unique([shopId, phone])
  @@index([shopId, name])
}

CustomerCredit

Individual credit transactions.

prisma
model CustomerCredit {
  id          String   @id @default(cuid())
  shopId      String
  customerId  String
  
  type        CreditType
  amount      Float
  saleId      String?   // If credit from sale
  note        String?
  userId      String?   // Who processed
  
  createdAt   DateTime @default(now())
  
  // Relations
  customer    Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
  sale        Sale?    @relation(fields: [saleId], references: [id])
  
  @@index([customerId, createdAt])
  @@index([shopId, createdAt])
}

enum CreditType {
  PURCHASE    // Bought on credit
  PAYMENT     // Paid down balance
  ADJUSTMENT  // Manual
  REFUND      // Added from refund
}

Returns & Refunds

Return

Refund and exchange tracking.

prisma
model Return {
  id            String       @id @default(cuid())
  shopId        String
  saleId        String?      // Original sale
  customerId    String?
  userId        String?      // Staff processing
  
  returnNumber  String?
  reason        String       // Defective, Wrong item, etc.
  
  // Resolution
  type          ReturnType
  refundAmount  Float        @default(0)
  
  status        ReturnStatus @default(PENDING)
  notes         String?
  
  createdAt     DateTime     @default(now())
  processedAt   DateTime?
  
  // Relations
  shop          Shop     @relation(fields: [shopId], references: [id], onDelete: Cascade)
  items         ReturnItem[]
  exchangeItems ReturnExchangeItem[]
  
  @@index([shopId, createdAt])
  @@index([saleId])
}

model ReturnItem {
  id          String   @id @default(cuid())
  returnId    String
  productId   String
  
  productName String
  quantity    Int
  unitPrice   Float
  
  restockable Boolean  @default(true)
  restocked   Boolean  @default(false)
  
  return      Return   @relation(fields: [returnId], references: [id], onDelete: Cascade)
  
  @@index([returnId])
}

model ReturnExchangeItem {
  id          String   @id @default(cuid())
  returnId    String
  productId   String
  
  productName String
  quantity    Int
  unitPrice   Float
  
  return      Return   @relation(fields: [returnId], references: [id], onDelete: Cascade)
  
  @@index([returnId])
}

enum ReturnType {
  REFUND        // Money back
  EXCHANGE      // Swap product
  STORE_CREDIT  // Credit account
}

enum ReturnStatus {
  PENDING
  APPROVED
  COMPLETED
  REJECTED
}

Supplier & Procurement

Supplier

Vendor management.

prisma
model Supplier {
  id          String   @id @default(cuid())
  shopId      String
  
  name        String
  contactName String?
  phone       String?   // International format
  email       String?
  website     String?
  
  // Address
  address     String?
  city        String?
  country     String?   // "South Africa", "China"
  postalCode  String?
  
  // Business Details
  taxId       String?
  currency    String?   // Supplier's currency
  paymentTerms String?  // "Net 30", "COD"
  leadTimeDays Int?
  notes       String?
  
  isActive    Boolean  @default(true)
  
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  
  // Relations
  shop        Shop     @relation(fields: [shopId], references: [id], onDelete: Cascade)
  products    SupplierProduct[]
  orders      PurchaseOrder[]
  
  @@unique([shopId, phone])
  @@index([shopId, name])
}

model SupplierProduct {
  id          String   @id @default(cuid())
  supplierId  String
  productId   String
  
  costPrice   Float
  minOrder    Int      @default(1)
  leadDays    Int?
  sku         String?  // Supplier's SKU
  isPreferred Boolean  @default(false)
  
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  
  // Relations
  supplier    Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade)
  product     Product  @relation(fields: [productId], references: [id], onDelete: Cascade)
  
  @@unique([supplierId, productId])
}

model PurchaseOrder {
  id          String   @id @default(cuid())
  shopId      String
  supplierId  String
  
  orderNumber String?
  status      POStatus @default(DRAFT)
  
  subtotal    Float
  tax         Float    @default(0)
  totalAmount Float
  
  orderDate   DateTime @default(now())
  expectedDate DateTime?
  receivedDate DateTime?
  
  notes       String?
  
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  
  // Relations
  shop        Shop     @relation(fields: [shopId], references: [id], onDelete: Cascade)
  supplier    Supplier @relation(fields: [supplierId], references: [id])
  items       POItem[]
  
  @@index([shopId, createdAt])
  @@index([supplierId])
}

model POItem {
  id          String   @id @default(cuid())
  poId        String
  productId   String
  
  productName String
  quantity    Int
  unitCost    Float
  totalCost   Float
  receivedQty Int      @default(0)
  
  po          PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade)
  
  @@index([poId])
}

enum POStatus {
  DRAFT
  SENT
  PARTIAL     // Partially received
  RECEIVED
  CANCELLED
}

Supporting Models

Expense

Operating expense tracking.

prisma
model Expense {
  id          String   @id @default(cuid())
  shopId      String
  userId      String?
  
  category    ExpenseCategory
  amount      Float
  description String?
  date        DateTime
  receiptUrl  String?
  
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  
  shop        Shop     @relation(fields: [shopId], references: [id], onDelete: Cascade)
  
  @@index([shopId, date])
}

enum ExpenseCategory {
  RENT
  UTILITIES
  SUPPLIES
  WAGES
  TRANSPORT
  MARKETING
  REPAIRS
  OTHER
}

DailyReport

Pre-generated daily summaries.

prisma
model DailyReport {
  id              String   @id @default(cuid())
  shopId          String
  date            DateTime @db.Date
  
  // Sales Summary
  totalSales      Float
  totalTransactions Int
  averageBasket   Float
  
  // Costs & Profit
  totalCost       Float
  grossProfit     Float
  totalExpenses   Float
  netProfit       Float
  
  // Payment Breakdown
  cashSales       Float    @default(0)
  momoSales       Float    @default(0)
  emaliSales      Float    @default(0)
  cardSales       Float    @default(0)
  
  // JSON Data
  topProducts     Json     // [{id, name, quantity, revenue}]
  lowStock        Json     // [{id, name, quantity, reorderAt}]
  
  // AI
  aiInsight       String?
  
  // WhatsApp
  sentViaWhatsApp Boolean  @default(false)
  sentAt          DateTime?
  
  createdAt       DateTime @default(now())
  
  shop            Shop     @relation(fields: [shopId], references: [id], onDelete: Cascade)
  
  @@unique([shopId, date])
  @@index([shopId, date])
}

AIConversation

AI chat history.

prisma
model AIConversation {
  id          String   @id @default(cuid())
  shopId      String
  
  userMessage String
  aiResponse  String
  type        AIQueryType @default(TEXT)
  audioUrl    String?
  context     Json?    // Data used for response
  
  createdAt   DateTime @default(now())
  
  shop        Shop     @relation(fields: [shopId], references: [id], onDelete: Cascade)
  
  @@index([shopId, createdAt])
}

enum AIQueryType {
  TEXT
  VOICE
  INSIGHT
}

AuditLog

Security audit trail.

prisma
model AuditLog {
  id          String   @id @default(cuid())
  shopId      String
  userId      String
  
  action      String   // LOGIN, SALE_VOID, PRODUCT_CREATE
  entityType  String   // product, sale, user
  entityId    String?
  details     Json?
  ipAddress   String?
  
  createdAt   DateTime @default(now())
  
  shop        Shop     @relation(fields: [shopId], references: [id], onDelete: Cascade)
  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  
  @@index([shopId, createdAt])
  @@index([userId, createdAt])
  @@index([action])
}

SyncQueue

Offline synchronization queue.

prisma
model SyncQueue {
  id          String   @id @default(cuid())
  shopId      String
  
  entityType  String   // product, sale, stockLog
  entityId    String   // Local ID
  operation   SyncOperation
  data        Json     // Full entity data
  
  status      SyncStatus @default(PENDING)
  attempts    Int      @default(0)
  error       String?
  
  queuedAt    DateTime @default(now())
  processedAt DateTime?
  
  shop        Shop     @relation(fields: [shopId], references: [id], onDelete: Cascade)
  
  @@index([shopId, status])
}

enum SyncOperation {
  CREATE
  UPDATE
  DELETE
}

enum SyncStatus {
  PENDING
  PROCESSING
  COMPLETED
  FAILED
  CONFLICT
}

Admin

Platform administrators.

prisma
model Admin {
  id        String   @id @default(cuid())
  email     String   @unique
  password  String   // bcrypt hash
  name      String
  role      AdminRole @default(ADMIN)
  isActive  Boolean  @default(true)
  
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum AdminRole {
  SUPER_ADMIN
  ADMIN
  SUPPORT
}

One chat. Everything done.