Inventory - Module Specification
1. Overview
1.1 Purpose
Central management system for puppy inventory, breed information, and breed collections. Provides the data backbone for all puppy-related operations including catalog display, pricing management, status tracking, and legacy system synchronization.
1.2 Scope
In Scope:
- Puppy CRUD operations and lifecycle management
- Breed and breed collection management
- Pricing management with history tracking
- Status workflow with role-based visibility
- Legacy POS system synchronization via webhook
- Bulk operations for inventory management
Out of Scope:
- Store and brand settings (→ Admin Settings module)
- Lead management and follow-up (→ CRM module)
- Purchase transactions (→ Ecommerce module)
- Media file storage (→ Media module)
2. Dependencies
2.1 Upstream (What This Module Needs)
| Module/Service |
What We Need |
Interface |
| Media |
Image/video metadata and R2 URLs |
Server Actions |
| Admin Settings |
Store configuration, brand settings |
Server Actions |
| Rich Text Editor |
Lexical editor for descriptions |
React Component |
2.2 Downstream (What Needs This Module)
| Module/Service |
What They Need |
Interface |
| Customer Website |
Puppy data, breed info, availability |
Server Actions |
| CRM |
Puppy references for leads |
Server Actions |
| Analytics |
Inventory events, pricing changes |
Event emission |
| Social Media |
Puppy data for content pipeline |
Server Actions |
3. Data Ownership
3.1 Entities This Module Owns
| Entity |
Description |
Key Fields |
| Puppy |
Individual puppy record |
id, name, breed, gender, price, wholesalePrice, status, storeId, supplierId |
| Breed |
Breed information |
id, name, slug, description, hybrid |
| BreedCollection |
Grouping of breeds |
id, name, slug, description |
| Supplier |
Breeder/source of puppies |
id, name, contactName, phone, email, location |
| PriceHistory |
Historical price changes |
id, puppyId, price, changedAt, changedBy |
| StatusHistory |
Historical status changes |
id, puppyId, status, changedAt, changedBy, reason |
3.2 Entities This Module Uses (Read-Only)
| Entity |
Owner |
How We Use It |
| Media |
Media |
Attach images/videos to puppies and breeds |
| Store |
Admin Settings |
Associate puppies with store locations |
| User |
Auth |
Track who made changes (changedBy) |
3.3 Data Schema
import type { LexicalEditorState } from '@/modules/rich-text-editor';
import type { Media } from '@/modules/media';
import type { Store } from '@/modules/admin-settings';
import type { User } from '@/modules/auth';
// Puppy statuses with role-based visibility
type PuppyStatus =
| 'arrived' // Sales, Kennel Staff, Admin
| 'available' // Sales, Kennel Staff, Admin
| 'sold' // Admin only
| 'unavailable' // Kennel Staff, Admin
| 'expired' // Admin only
| 'donated'; // Admin only
interface Puppy {
id: string;
slug: string; // URL-friendly identifier
name: string;
breedId: Breed['id'];
supplierId: Supplier['id'];
gender: 'male' | 'female';
dateOfBirth: Date;
color: string;
wholesalePrice: number; // Cost from supplier (Admin only)
price: number; // Retail price
salePrice?: number; // Discounted price (if on sale)
status: PuppyStatus;
storeId: Store['id'];
mediaIds: Media['id'][];
description?: LexicalEditorState;
microchipNumber?: string;
registrationNumber?: string;
legacyId?: string; // For POS sync
arrivedAt: Date;
soldAt?: Date;
metadata?: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
}
interface Breed {
id: string;
name: string;
slug: string;
description?: LexicalEditorState;
hybrid: boolean; // true for designer breeds (e.g., Goldendoodle)
mediaIds: Media['id'][];
createdAt: Date;
updatedAt: Date;
}
interface BreedCollection {
id: string;
name: string;
slug: string;
description?: LexicalEditorState;
breedIds: Breed['id'][]; // Many-to-many relationship
displayOrder: number;
createdAt: Date;
updatedAt: Date;
}
// Note: Breeds can belong to multiple BreedCollections
// e.g., "Goldendoodle" can be in both "Hypoallergenic" and "Designer Breeds"
interface Supplier {
id: string;
name: string; // Internal/display name
contactName: string;
phone: string;
email: string;
location?: string; // General location (city, state)
// USDA compliance fields
usdaNumber: string; // USDA license number
usdaName: string; // USDA registered name
usdaAddress: string; // USDA registered address
notes?: LexicalEditorState;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
interface PriceHistory {
id: string;
puppyId: Puppy['id'];
previousPrice: number;
newPrice: number;
changedAt: Date;
changedBy: User['id'];
reason?: string;
}
interface StatusHistory {
id: string;
puppyId: Puppy['id'];
previousStatus: PuppyStatus;
newStatus: PuppyStatus;
changedAt: Date;
changedBy: User['id'];
reason?: string;
}
4. Service Level Objectives
| Objective |
Target |
Measurement |
| API Latency (p95) |
< 200ms |
Application monitoring |
| Availability |
99.9% |
Health checks |
| Data Consistency |
100% |
Audit logs |
| Sync Latency |
< 5 minutes |
Webhook monitoring |
5. Interface Contract
5.1 Internal Service Interface
// Include patterns for optional relationship hydration
interface PuppyInclude {
breed?: boolean;
supplier?: boolean;
store?: boolean;
media?: boolean;
}
interface BreedInclude {
collections?: boolean;
media?: boolean;
}
interface BreedCollectionInclude {
breeds?: boolean;
}
interface InventoryService {
// Puppy operations
getPuppy(id: string, include?: PuppyInclude): Promise<Puppy>;
getPuppyBySlug(slug: string, include?: PuppyInclude): Promise<Puppy>;
listPuppies(filters: PuppyFilters, pagination: Pagination, include?: PuppyInclude): Promise<PaginatedResult<Puppy>>;
createPuppy(data: CreatePuppyDTO): Promise<Puppy>;
updatePuppy(id: string, data: UpdatePuppyDTO): Promise<Puppy>;
updatePuppyStatus(id: string, status: PuppyStatus, reason?: string): Promise<Puppy>;
updatePuppyPrice(id: string, price: number, reason?: string): Promise<Puppy>;
// Breed operations
getBreed(id: string, include?: BreedInclude): Promise<Breed>;
getBreedBySlug(slug: string, include?: BreedInclude): Promise<Breed>;
listBreeds(filters?: BreedFilters, include?: BreedInclude): Promise<Breed[]>;
createBreed(data: CreateBreedDTO): Promise<Breed>;
updateBreed(id: string, data: UpdateBreedDTO): Promise<Breed>;
// Breed Collection operations
getBreedCollection(id: string, include?: BreedCollectionInclude): Promise<BreedCollection>;
getBreedCollectionBySlug(slug: string, include?: BreedCollectionInclude): Promise<BreedCollection>;
listBreedCollections(include?: BreedCollectionInclude): Promise<BreedCollection[]>;
createBreedCollection(data: CreateBreedCollectionDTO): Promise<BreedCollection>;
updateBreedCollection(id: string, data: UpdateBreedCollectionDTO): Promise<BreedCollection>;
// Supplier operations
getSupplier(id: string): Promise<Supplier>;
listSuppliers(filters?: SupplierFilters): Promise<Supplier[]>;
createSupplier(data: CreateSupplierDTO): Promise<Supplier>;
updateSupplier(id: string, data: UpdateSupplierDTO): Promise<Supplier>;
// History queries
getPriceHistory(puppyId: string): Promise<PriceHistory[]>;
getStatusHistory(puppyId: string): Promise<StatusHistory[]>;
// Bulk operations
bulkUpdateStatus(puppyIds: string[], status: PuppyStatus, reason?: string): Promise<void>;
bulkUpdatePrice(puppyIds: string[], priceAdjustment: PriceAdjustment): Promise<void>;
}
// When includes are specified, relationships are hydrated on the returned object
// Example: getPuppy(id, { breed: true, media: true })
// Returns Puppy with puppy.breed as Breed object and puppy.media as Media[]
// Unincluded relationships remain as IDs (breedId, supplierId, etc.)
interface PuppyFilters {
storeId?: string;
breedId?: string;
breedCollectionId?: string;
status?: PuppyStatus | PuppyStatus[];
gender?: 'male' | 'female';
minPrice?: number;
maxPrice?: number;
onSale?: boolean; // Filter puppies with salePrice set
newArrivals?: boolean; // Filter puppies with arrivedAt within 7 days
search?: string;
}
interface PriceAdjustment {
type: 'fixed' | 'percentage';
value: number; // Amount or percentage
direction: 'increase' | 'decrease';
}
5.2 Remote Procedure Interface
Communication Pattern: Next.js Server Actions
| Procedure |
Input |
Output |
Auth |
Description |
| getPuppy |
{ id } |
Puppy |
Public |
Get puppy details |
| getPuppyBySlug |
{ slug } |
Puppy |
Public |
Get puppy by URL slug |
| listPuppies |
PuppyFilters |
PaginatedResult |
Public |
List puppies with filters |
| getBreed |
{ id } |
Breed |
Public |
Get breed details |
| getBreedBySlug |
{ slug } |
Breed |
Public |
Get breed by URL slug |
| listBreeds |
BreedFilters |
Breed[] |
Public |
List all breeds |
| getBreedCollection |
{ id } |
BreedCollection |
Public |
Get collection details |
| listBreedCollections |
- |
BreedCollection[] |
Public |
List all collections |
| createPuppy |
CreatePuppyDTO |
Puppy |
Admin |
Create new puppy |
| updatePuppy |
{ id, data } |
Puppy |
Admin |
Update puppy details |
| updatePuppyStatus |
{ id, status, reason } |
Puppy |
Staff+ |
Change puppy status |
| updatePuppyPrice |
{ id, price, reason } |
Puppy |
Admin |
Change puppy price |
| syncFromLegacy |
LegacySyncPayload |
SyncResult |
Webhook |
Receive legacy POS data |
5.3 Webhook Endpoint
| Endpoint |
Method |
Auth |
Description |
| /api/webhooks/legacy-sync |
POST |
API Key |
Receive puppy data from legacy POS |
// Legacy POS pushes full inventory snapshot as array of raw JSON values
interface LegacySyncPayload {
timestamp: Date;
puppies: LegacyPuppyRecord[];
}
interface LegacyPuppyRecord {
breed: string;
dob: string; // Date of birth
sex: string;
color: string;
microchip: string;
price: number;
wholesale_price: number; // Cost from breeder
arrivalDate: string;
storeName: string;
status: string;
sale_comment?: string; // Used for sale pricing notes
first_price?: number; // Original price (for sale tracking)
current_weight?: number;
registration?: string;
sire_breed?: string;
sire_weight?: number;
dam_breed?: string;
dam_weight?: number;
video?: string; // Video URL
// Breeder/supplier information
breeder_name?: string;
breeder_usda_number?: string;
breeder_usda_name?: string;
breeder_usda_address?: string;
}
interface SyncResult {
success: boolean;
processed: number;
created: number;
updated: number;
skipped: number;
errors?: string[];
}
6. Error Handling Strategy
| Error Code |
Condition |
User Message |
| INVENTORY_PUPPY_NOT_FOUND |
Puppy doesn't exist |
"Puppy not found" |
| INVENTORY_BREED_NOT_FOUND |
Breed doesn't exist |
"Breed not found" |
| INVENTORY_COLLECTION_NOT_FOUND |
Collection doesn't exist |
"Collection not found" |
| INVENTORY_INVALID_STATUS |
Invalid status transition |
"Cannot change to this status" |
| INVENTORY_INVALID_PRICE |
Price validation failed |
"Price must be greater than zero" |
| INVENTORY_DUPLICATE_LEGACY_ID |
Legacy ID already exists |
"This puppy already exists in the system" |
| INVENTORY_UNAUTHORIZED |
Insufficient permissions |
"You don't have permission to perform this action" |
interface ErrorResponse {
code: string;
message: string;
field?: string;
details?: Record<string, unknown>;
}
7. Observability
7.1 Logging
| Event |
Level |
Fields to Include |
| Puppy created |
INFO |
puppy_id, breed_id, store_id, user_id |
| Puppy status changed |
INFO |
puppy_id, old_status, new_status, user_id, reason |
| Puppy price changed |
INFO |
puppy_id, old_price, new_price, user_id, reason |
| Legacy sync received |
INFO |
legacy_id, action, sync_result |
| Legacy sync failed |
ERROR |
legacy_id, action, error_code, error_message |
| Bulk operation completed |
INFO |
operation, puppy_count, user_id |
7.2 Metrics
| Metric |
Type |
Description |
| inventory_puppies_total |
Gauge |
Total puppies by status |
| inventory_sync_requests |
Counter |
Legacy sync webhook calls |
| inventory_sync_errors |
Counter |
Failed sync attempts |
| inventory_price_changes |
Counter |
Price modifications |
| inventory_status_changes |
Counter |
Status transitions |
7.3 Tracing
- Span name:
inventory.{operation}
- Required attributes:
user_id, puppy_id, operation, store_id
8. Testing Strategy
| Test Type |
Coverage Target |
Tools |
Focus Areas |
| Unit |
80%+ |
Vitest |
Status transitions, price validation, filters |
| Integration |
Critical paths |
Vitest |
Database operations, sync webhook |
| E2E |
Happy paths |
Playwright |
CRUD workflows, bulk operations |
9. Feature Inventory
Puppy Management
| Feature ID |
Name |
Status |
Priority |
| FEATURE-030 |
Puppy CRUD |
Draft |
Must Have |
| FEATURE-031 |
Puppy Status Workflow |
Draft |
Must Have |
| FEATURE-032 |
Puppy Pricing Management |
Draft |
Must Have |
| FEATURE-033 |
Price History Tracking |
Draft |
Must Have |
| FEATURE-034 |
Status History Tracking |
Draft |
Must Have |
| FEATURE-035 |
Legacy POS Sync Webhook |
Draft |
Must Have |
| FEATURE-036 |
Bulk Status Update |
Draft |
Should Have |
| FEATURE-037 |
Bulk Price Adjustment |
Draft |
Should Have |
Breed Management
| Feature ID |
Name |
Status |
Priority |
| FEATURE-038 |
Breed CRUD |
Draft |
Must Have |
| FEATURE-039 |
Breed Collection CRUD |
Draft |
Must Have |
| FEATURE-040 |
Breed-Collection Many-to-Many |
Draft |
Must Have |
Supplier Management
| Feature ID |
Name |
Status |
Priority |
| FEATURE-041 |
Supplier CRUD |
Draft |
Must Have |
10. Access Control
Role-Based Puppy Visibility
| Role |
Visible Statuses |
| Sales |
arrived, available |
| Kennel Staff |
arrived, available, unavailable |
| Admin |
All statuses |
Operation Permissions
| Operation |
Sales |
Kennel Staff |
Admin |
| View puppies (filtered) |
Yes |
Yes |
Yes |
| View wholesale price |
No |
No |
Yes |
| View supplier info |
No |
No |
Yes |
| Update puppy details |
No |
No |
Yes |
| Change status |
No |
Limited* |
Yes |
| Change price |
No |
No |
Yes |
| Bulk operations |
No |
No |
Yes |
| Manage breeds |
No |
No |
Yes |
| Manage suppliers |
No |
No |
Yes |
*Kennel Staff can change status between: arrived ↔ available ↔ unavailable
11. Decisions
D1: Store Ownership Moved to Admin Settings (2026-01-19)
Status: Accepted
Context: Store and brand configuration is more administrative than inventory-related
Decision: Move Store entity to Admin Settings module; Inventory references stores via storeId
Consequences: Cleaner separation; Inventory focused on puppy data only
D2: Minimal Breed Schema (2026-01-19)
Status: Accepted
Context: Competitor sites have extensive breed data, but we need to ship Phase 1
Decision: Start with minimal breed fields (name, slug, description, hybrid, mediaIds); expand later based on content needs
Consequences: Faster initial development; may need migration for additional fields
D3: Many-to-Many Breed Collections (2026-01-19)
Status: Accepted
Context: Breeds can logically belong to multiple groupings (e.g., "Hypoallergenic" and "Small Dogs")
Decision: Implement many-to-many relationship between Breeds and BreedCollections
Consequences: More flexible categorization; slightly more complex queries
D4: Lexical for Rich Text (2026-01-19)
Status: Accepted
Context: Description fields need rich text formatting
Decision: Use Lexical editor; store as JSON in description fields
Consequences: Consistent editor experience; requires Rich Text Editor module
D5: Legacy Sync via Webhook (2026-01-19)
Status: Accepted
Context: Need to sync puppy data from existing POS system during transition
Decision: Expose webhook endpoint for legacy system to push updates
Consequences: Legacy system must be modified to call webhook; one-way sync only
12. References
Change Log
| Version |
Date |
Author |
Changes |
| 1.0 |
2026-01-19 |
Claude |
Initial module spec - Draft |
| 1.1 |
2026-01-20 |
Claude |
Added references to Media, Admin Settings, Rich Text Editor modules |
| 1.2 |
2026-01-20 |
Claude |
Updated relationship types with explicit module imports (Media['id'], Store['id'], etc.) |