Admin Settings - Module Specification
1. Overview
1.1 Purpose
Admin Settings manages store locations, brand configuration, and system-wide settings. Provides centralized configuration for multi-store operations including business hours, contact information, social media links, and brand customization.
1.2 Scope
In Scope:
- Store management (locations, hours, contact, social media, financing offers)
- Brand configuration (logos, social links, disclaimers, notification banner)
- System-wide display settings (average rating, payment icons)
Out of Scope:
- Virtual/SEO locations (→ CMS module)
- User management (→ Auth module)
- Media file storage (→ Media module)
- Content pages and blog (→ CMS module)
2. Dependencies
2.1 Upstream (What This Module Needs)
| Module/Service |
What We Need |
Interface |
| Media |
Store images, logos, payment icons |
Server Actions |
2.2 Downstream (What Needs This Module)
| Module/Service |
What They Need |
Interface |
| Customer Website |
Store locations, brand config, hours |
Server Actions |
| Inventory |
Store assignment for puppies |
Server Actions |
| CRM |
Store info for leads |
Server Actions |
| CMS |
Brand settings for content display |
Server Actions |
3. Data Ownership
3.1 Entities This Module Owns
| Entity |
Description |
Key Fields |
| Store |
Physical store location |
id, name, address, hours, phone, coordinates, socialLinks |
| Brand |
Brand-wide configuration |
id, name, logos, socialLinks, disclaimers, notificationBanner |
| FinancingOffer |
Financing provider per store |
id, storeId, providerName, requirements, applicationLink |
3.2 Entities This Module Uses (Read-Only)
| Entity |
Owner |
How We Use It |
| Media |
Media |
Logos, store images, payment icons |
3.3 Data Schema
import type { LexicalEditorState } from '@/modules/rich-text-editor';
import type { Media } from '@/modules/media';
interface Store {
id: string;
slug: string;
name: string;
// Address
addressStreet: string;
addressCity: string;
addressState: string;
addressZip: string;
countyPermit?: string;
// Coordinates
latitude: number;
longitude: number;
// Maps
locationIframeUrl?: string; // Store-specific map embed
googleMapsPlaceId?: string; // For reviews integration
googleMapsLink?: string;
appleMapsUrl?: string;
// Hours (flexible per day)
hours: StoreHours;
// Contact
phoneNumber: string;
// Display
order: number; // Display order in listings
exteriorImageId?: Media['id'];
interiorImageId?: Media['id'];
// Social media
socialLinks: SocialLinks;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
interface StoreHours {
monday: DayHours | null; // null = closed
tuesday: DayHours | null;
wednesday: DayHours | null;
thursday: DayHours | null;
friday: DayHours | null;
saturday: DayHours | null;
sunday: DayHours | null;
}
interface DayHours {
open: string; // "09:00" format
close: string; // "21:00" format
}
interface SocialLinks {
facebook?: string;
instagram?: string;
pinterest?: string;
twitter?: string;
tiktok?: string;
}
interface Brand {
id: string;
name: string;
// Logos
logoDarkId: Media['id'];
footerLogoId: Media['id'];
// Images
subscribeImageId?: Media['id']; // Newsletter section
missingPhotoFillerId?: Media['id']; // Placeholder image
allCardsAcceptedIconId?: Media['id']; // Payment methods
// Maps
allLocationsEmbedUrl: string; // Google Maps embed for all stores
// Display
averageReviewRating: string; // e.g., "4.9"
notificationBanner?: LexicalEditorState; // Top banner announcement
// Legal
footerDisclaimer: LexicalEditorState;
footerSecondDisclaimer?: LexicalEditorState;
// Social media (brand-level)
socialLinks: SocialLinks;
createdAt: Date;
updatedAt: Date;
}
interface FinancingOffer {
id: string;
storeId: Store['id'];
providerName: string;
requirements: string[]; // List of requirements
applicationLink: string;
bannerImageId?: Media['id'];
order: number; // Display order
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
4. Service Level Objectives
| Objective |
Target |
Measurement |
| Settings Read Latency |
< 50ms |
Application monitoring |
| Settings Update Latency |
< 200ms |
Application monitoring |
| Availability |
99.9% |
Health checks |
| Cache Hit Rate |
> 95% |
Application monitoring |
Note: Brand and Store settings are heavily cached as they rarely change. Cache invalidation on update.
5. Interface Contract
5.1 Internal Service Interface
interface AdminSettingsService {
// Store operations
getStore(id: string, include?: StoreInclude): Promise<Store>;
getStoreBySlug(slug: string, include?: StoreInclude): Promise<Store>;
listStores(filters?: StoreFilters, include?: StoreInclude): Promise<Store[]>;
createStore(data: CreateStoreDTO): Promise<Store>;
updateStore(id: string, data: UpdateStoreDTO): Promise<Store>;
deleteStore(id: string): Promise<void>;
// Brand operations (single brand per instance)
getBrand(include?: BrandInclude): Promise<Brand>;
updateBrand(data: UpdateBrandDTO): Promise<Brand>;
// Financing offer operations
getFinancingOffer(id: string): Promise<FinancingOffer>;
listFinancingOffers(storeId: string): Promise<FinancingOffer[]>;
createFinancingOffer(data: CreateFinancingOfferDTO): Promise<FinancingOffer>;
updateFinancingOffer(id: string, data: UpdateFinancingOfferDTO): Promise<FinancingOffer>;
deleteFinancingOffer(id: string): Promise<void>;
}
// Include patterns
interface StoreInclude {
financingOffers?: boolean;
media?: boolean; // Hydrate exteriorImage, interiorImage
}
interface BrandInclude {
media?: boolean; // Hydrate all logo/image fields
}
// Filters
interface StoreFilters {
isActive?: boolean;
search?: string; // Search by name, city
}
// DTOs
interface CreateStoreDTO {
slug: string;
name: string;
addressStreet: string;
addressCity: string;
addressState: string;
addressZip: string;
countyPermit?: string;
latitude: number;
longitude: number;
locationIframeUrl?: string;
googleMapsPlaceId?: string;
googleMapsLink?: string;
appleMapsUrl?: string;
hours: StoreHours;
phoneNumber: string;
order?: number;
exteriorImageId?: Media['id'];
interiorImageId?: Media['id'];
socialLinks?: SocialLinks;
isActive?: boolean;
}
interface UpdateStoreDTO extends Partial<CreateStoreDTO> {}
interface UpdateBrandDTO {
name?: string;
logoDarkId?: Media['id'];
footerLogoId?: Media['id'];
subscribeImageId?: Media['id'];
missingPhotoFillerId?: Media['id'];
allCardsAcceptedIconId?: Media['id'];
allLocationsEmbedUrl?: string;
averageReviewRating?: string;
notificationBanner?: LexicalEditorState;
footerDisclaimer?: LexicalEditorState;
footerSecondDisclaimer?: LexicalEditorState;
socialLinks?: SocialLinks;
}
interface CreateFinancingOfferDTO {
storeId: Store['id'];
providerName: string;
requirements: string[];
applicationLink: string;
bannerImageId?: Media['id'];
order?: number;
isActive?: boolean;
}
interface UpdateFinancingOfferDTO extends Partial<Omit<CreateFinancingOfferDTO, 'storeId'>> {}
5.2 Remote Procedure Interface
Communication Pattern: Next.js Server Actions
| Procedure |
Input |
Output |
Auth |
Description |
| getStore |
{ id, include? } |
Store |
Public |
Get store details |
| getStoreBySlug |
{ slug, include? } |
Store |
Public |
Get store by slug |
| listStores |
{ filters?, include? } |
Store[] |
Public |
List all stores |
| createStore |
CreateStoreDTO |
Store |
Admin |
Create new store |
| updateStore |
{ id, data } |
Store |
Admin |
Update store |
| deleteStore |
{ id } |
void |
Admin |
Delete store |
| getBrand |
{ include? } |
Brand |
Public |
Get brand settings |
| updateBrand |
UpdateBrandDTO |
Brand |
Admin |
Update brand settings |
| listFinancingOffers |
{ storeId } |
FinancingOffer[] |
Public |
Get financing for store |
| createFinancingOffer |
CreateFinancingOfferDTO |
FinancingOffer |
Admin |
Add financing offer |
| updateFinancingOffer |
{ id, data } |
FinancingOffer |
Admin |
Update financing offer |
| deleteFinancingOffer |
{ id } |
void |
Admin |
Delete financing offer |
6. Error Handling Strategy
| Error Code |
Condition |
User Message |
| STORE_NOT_FOUND |
Store doesn't exist |
"Store not found" |
| STORE_SLUG_EXISTS |
Duplicate store slug |
"A store with this URL already exists" |
| STORE_INVALID_HOURS |
Invalid hours format |
"Invalid store hours format" |
| STORE_INVALID_COORDINATES |
Invalid lat/lng |
"Invalid coordinates provided" |
| BRAND_NOT_FOUND |
Brand not configured |
"Brand settings not found" |
| FINANCING_NOT_FOUND |
Financing offer doesn't exist |
"Financing offer not found" |
| FINANCING_INVALID_STORE |
Store doesn't exist for offer |
"Invalid store for financing offer" |
| SETTINGS_UNAUTHORIZED |
Insufficient permissions |
"You don't have permission to modify settings" |
interface ErrorResponse {
code: string;
message: string;
field?: string; // For validation errors
details?: Record<string, unknown>;
}
7. Observability
7.1 Logging
| Event |
Level |
Fields to Include |
| Store created |
INFO |
store_id, store_name, user_id |
| Store updated |
INFO |
store_id, changed_fields, user_id |
| Store deleted |
INFO |
store_id, user_id |
| Brand updated |
INFO |
changed_fields, user_id |
| Financing offer created |
INFO |
offer_id, store_id, provider_name, user_id |
| Financing offer deleted |
INFO |
offer_id, store_id, user_id |
| Settings cache invalidated |
DEBUG |
cache_key, reason |
7.2 Metrics
| Metric |
Type |
Description |
| settings_reads_total |
Counter |
Total reads by entity type |
| settings_updates_total |
Counter |
Total updates by entity type |
| settings_cache_hits |
Counter |
Cache hit count |
| settings_cache_misses |
Counter |
Cache miss count |
| settings_read_duration_ms |
Histogram |
Read latency |
8. Testing Strategy
| Test Type |
Coverage Target |
Tools |
Focus Areas |
| Unit |
80%+ |
Vitest |
Hours validation, coordinate parsing, DTO validation |
| Integration |
Critical paths |
Vitest |
Store CRUD, Brand updates, cache invalidation |
| E2E |
Happy paths |
Playwright |
Admin settings UI, store management |
9. Feature Inventory
| Feature ID |
Name |
Status |
Priority |
| FEATURE-047 |
Store CRUD |
Draft |
Must Have |
| FEATURE-048 |
Store Hours Management |
Draft |
Must Have |
| FEATURE-049 |
Brand Settings Management |
Draft |
Must Have |
| FEATURE-050 |
Financing Offers Management |
Draft |
Should Have |
| FEATURE-051 |
Store Locator Map Embed |
Draft |
Should Have |
10. Access Control
Operation Permissions
| Operation |
Public |
Sales |
Kennel Staff |
Photographer |
Admin |
| View stores |
Yes |
Yes |
Yes |
Yes |
Yes |
| View brand settings |
Yes |
Yes |
Yes |
Yes |
Yes |
| View financing offers |
Yes |
Yes |
Yes |
Yes |
Yes |
| Create store |
No |
No |
No |
No |
Yes |
| Update store |
No |
No |
No |
No |
Yes |
| Delete store |
No |
No |
No |
No |
Yes |
| Update brand settings |
No |
No |
No |
No |
Yes |
| Manage financing offers |
No |
No |
No |
No |
Yes |
Note: All write operations are Admin-only. Read operations are public for customer website display.
11. Decisions
D1: Single Brand Per Instance (2026-01-20)
Status: Accepted
Context: Need brand configuration but each deployment is a separate brand
Decision: One Brand entity per instance, no multi-brand support
Consequences: Simpler schema, getBrand() returns single record, no brand selection needed
D2: Flexible Day-Based Hours (2026-01-20)
Status: Accepted
Context: Stores have varying hours per day of week
Decision: Use StoreHours object with each day as separate field, null indicates closed
Consequences: Flexible per-day configuration, easy to display, supports holiday closures
D3: Financing Offers as Separate Entity (2026-01-20)
Status: Accepted
Context: Stores can have multiple financing providers with different requirements
Decision: FinancingOffer as separate entity linked to Store, not embedded array
Consequences: Easier CRUD, can query across stores, supports ordering and activation
D4: Rich Text for Legal Disclaimers (2026-01-20)
Status: Accepted
Context: Footer disclaimers are long legal text that may need formatting
Decision: Use LexicalEditorState for disclaimer fields instead of plain strings
Consequences: Supports bold, links, paragraphs in legal text; consistent with other rich text in system
12. References
Change Log
| Version |
Date |
Author |
Changes |
| 1.0 |
2026-01-20 |
Claude |
Initial module spec - Draft |