feat: restore universal blog, booking, ecommerce skills with framework-agnostic schema and API patterns
This commit is contained in:
400
.kilo/skills/blog/SKILL.md
Normal file
400
.kilo/skills/blog/SKILL.md
Normal file
@@ -0,0 +1,400 @@
|
||||
---
|
||||
name: blog
|
||||
description: Blog/CMS domain knowledge - posts, categories, tags, comments, authors, SEO, RSS, scheduling
|
||||
---
|
||||
|
||||
# Blog Skill
|
||||
|
||||
## Purpose
|
||||
|
||||
Universal domain knowledge for building blog and content management systems: posts, categories, tags, comments, authors, SEO optimization, RSS/sitemaps.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Content Management
|
||||
- Post CRUD operations
|
||||
- Draft / Published / Archived states
|
||||
- Content scheduling
|
||||
- Rich text / Markdown editing
|
||||
- Media embedding
|
||||
- Featured images
|
||||
|
||||
### Organization
|
||||
- Hierarchical categories
|
||||
- Flat tags
|
||||
- Author assignment
|
||||
- Content series / collections
|
||||
|
||||
### Comments
|
||||
- Comment moderation
|
||||
- Threaded (nested) comments
|
||||
- Spam filtering
|
||||
- Guest and authenticated comments
|
||||
|
||||
### SEO
|
||||
- Meta tags (title, description)
|
||||
- Open Graph / Twitter Cards
|
||||
- Structured data (Schema.org Article)
|
||||
- XML sitemap
|
||||
- RSS / Atom feeds
|
||||
- Canonical URLs
|
||||
|
||||
### Analytics
|
||||
- View counts
|
||||
- Reading time estimation
|
||||
- Popular posts
|
||||
- Related posts algorithm
|
||||
|
||||
## Database Schema (Universal)
|
||||
|
||||
Adapt table/column names to your framework conventions. Use `id` types appropriate to your DB (INTEGER / UUID / BIGSERIAL).
|
||||
|
||||
### Posts Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE posts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- or UUID / BIGSERIAL
|
||||
title TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
excerpt TEXT,
|
||||
content TEXT NOT NULL,
|
||||
featured_image TEXT,
|
||||
author_id INTEGER NOT NULL,
|
||||
category_id INTEGER,
|
||||
status TEXT DEFAULT 'draft', -- 'draft' | 'published' | 'archived'
|
||||
published_at DATETIME,
|
||||
meta_title TEXT,
|
||||
meta_description TEXT,
|
||||
canonical_url TEXT,
|
||||
reading_time INTEGER, -- minutes
|
||||
view_count INTEGER DEFAULT 0,
|
||||
allow_comments BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (author_id) REFERENCES users(id),
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_posts_slug ON posts(slug);
|
||||
CREATE INDEX idx_posts_status ON posts(status);
|
||||
CREATE INDEX idx_posts_published ON posts(published_at);
|
||||
CREATE INDEX idx_posts_author ON posts(author_id);
|
||||
CREATE INDEX idx_posts_category ON posts(category_id);
|
||||
```
|
||||
|
||||
### Categories Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
parent_id INTEGER,
|
||||
image_url TEXT,
|
||||
meta_title TEXT,
|
||||
meta_description TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (parent_id) REFERENCES categories(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_categories_parent ON categories(parent_id);
|
||||
```
|
||||
|
||||
### Tags Tables
|
||||
|
||||
```sql
|
||||
CREATE TABLE tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE post_tags (
|
||||
post_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (post_id, tag_id),
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_post_tags_post ON post_tags(post_id);
|
||||
CREATE INDEX idx_post_tags_tag ON post_tags(tag_id);
|
||||
```
|
||||
|
||||
### Comments Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE comments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
post_id INTEGER NOT NULL,
|
||||
parent_id INTEGER, -- for threaded comments; NULL = top-level
|
||||
author_name TEXT NOT NULL,
|
||||
author_email TEXT NOT NULL,
|
||||
author_url TEXT,
|
||||
content TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending', -- 'pending' | 'approved' | 'spam' | 'trash'
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (parent_id) REFERENCES comments(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_comments_post ON comments(post_id);
|
||||
CREATE INDEX idx_comments_status ON comments(status);
|
||||
```
|
||||
|
||||
### Authors Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE authors (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
bio TEXT,
|
||||
avatar TEXT,
|
||||
social_links TEXT, -- JSON {"twitter":"...","linkedin":"..."}
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Public API
|
||||
|
||||
```yaml
|
||||
GET /api/posts # List published posts (paginated)
|
||||
GET /api/posts/:slug # Get single post by slug
|
||||
GET /api/posts/author/:author # Posts by author slug
|
||||
GET /api/posts/category/:slug # Posts by category slug
|
||||
GET /api/posts/tag/:slug # Posts by tag slug
|
||||
GET /api/posts/search # Search posts (?q=query)
|
||||
|
||||
GET /api/categories # List categories (tree or flat)
|
||||
GET /api/categories/:slug # Category detail + posts
|
||||
|
||||
GET /api/tags # List tags
|
||||
GET /api/tags/:slug # Tag detail + posts
|
||||
|
||||
GET /api/posts/:slug/comments # Approved comments (paginated)
|
||||
POST /api/posts/:slug/comments # Submit new comment
|
||||
|
||||
GET /api/feed/rss # RSS feed
|
||||
GET /api/feed/atom # Atom feed
|
||||
GET /api/sitemap.xml # XML sitemap
|
||||
```
|
||||
|
||||
### Admin / CMS API
|
||||
|
||||
```yaml
|
||||
GET /api/admin/posts # List all posts (all statuses)
|
||||
POST /api/admin/posts # Create post
|
||||
PUT /api/admin/posts/:id # Update post
|
||||
DELETE /api/admin/posts/:id # Delete post
|
||||
POST /api/admin/posts/:id/publish
|
||||
POST /api/admin/posts/:id/archive
|
||||
|
||||
GET /api/admin/categories # List categories
|
||||
POST /api/admin/categories # Create category
|
||||
PUT /api/admin/categories/:id # Update category
|
||||
DELETE /api/admin/categories/:id # Delete category
|
||||
|
||||
GET /api/admin/tags # List tags
|
||||
POST /api/admin/tags # Create tag
|
||||
PUT /api/admin/tags/:id # Update tag
|
||||
DELETE /api/admin/tags/:id # Delete tag
|
||||
|
||||
GET /api/admin/comments # List comments (all statuses)
|
||||
PUT /api/admin/comments/:id/approve
|
||||
PUT /api/admin/comments/:id/spam
|
||||
DELETE /api/admin/comments/:id
|
||||
|
||||
POST /api/admin/media/upload # Upload media
|
||||
GET /api/admin/media # List media
|
||||
DELETE /api/admin/media/:id # Delete media
|
||||
```
|
||||
|
||||
## Markdown & Rich Content
|
||||
|
||||
Use any framework-appropriate parser (e.g. `marked`, `commonmark`, `python-markdown`).
|
||||
|
||||
```javascript
|
||||
// Example with marked + highlight.js
|
||||
const marked = require('marked');
|
||||
const hljs = require('highlight.js');
|
||||
|
||||
marked.use({
|
||||
renderer: {
|
||||
code(code, language) {
|
||||
const highlighted = language
|
||||
? hljs.highlight(code, { language }).value
|
||||
: code;
|
||||
return `<pre><code class="hljs ${language}">${highlighted}</code></pre>`;
|
||||
},
|
||||
image(href, title, alt) {
|
||||
return `<figure><img src="${href}" alt="${alt}" loading="lazy"><figcaption>${alt}</figcaption></figure>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function calculateReadingTime(content) {
|
||||
const words = content.split(/\s+/).length;
|
||||
return Math.ceil(words / 200); // 200 wpm average
|
||||
}
|
||||
```
|
||||
|
||||
## SEO Implementation
|
||||
|
||||
### Meta Tags Generator
|
||||
|
||||
```javascript
|
||||
function generateMeta(post, siteConfig) {
|
||||
return {
|
||||
title: post.meta_title || `${post.title} | ${siteConfig.name}`,
|
||||
description: post.meta_description || post.excerpt,
|
||||
canonical: post.canonical_url || `${siteConfig.url}/posts/${post.slug}`,
|
||||
ogType: 'article',
|
||||
ogImage: post.featured_image,
|
||||
articlePublishedTime: post.published_at,
|
||||
articleAuthor: post.author?.name
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Schema.org Article JSON-LD
|
||||
|
||||
```javascript
|
||||
function generateArticleSchema(post) {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: post.title,
|
||||
image: post.featured_image,
|
||||
author: { "@type": "Person", name: post.author.name },
|
||||
datePublished: post.published_at,
|
||||
dateModified: post.updated_at,
|
||||
description: post.excerpt
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Sitemap Template
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url><loc>{{siteUrl}}/</loc><changefreq>daily</changefreq><priority>1.0</priority></url>
|
||||
{{#posts}}
|
||||
<url>
|
||||
<loc>{{siteUrl}}/posts/{{slug}}</loc>
|
||||
<lastmod>{{updated_at}}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
{{/posts}}
|
||||
</urlset>
|
||||
```
|
||||
|
||||
## Comment Moderation
|
||||
|
||||
### Spam Detection Signals
|
||||
|
||||
```javascript
|
||||
function detectSpam(comment) {
|
||||
const signals = [];
|
||||
|
||||
if (countLinks(comment.content) > 3) signals.push('excessive_links');
|
||||
if (isDuplicate(comment)) signals.push('duplicate');
|
||||
if (isBlacklisted(comment.ip_address)) signals.push('blacklisted_ip');
|
||||
if (containsSpamWords(comment.content)) signals.push('spam_words');
|
||||
|
||||
return { isSpam: signals.length >= 2, score: signals.length, signals };
|
||||
}
|
||||
```
|
||||
|
||||
## Content Scheduling
|
||||
|
||||
```javascript
|
||||
async function publishScheduledPosts(db) {
|
||||
const now = new Date();
|
||||
const posts = await db.posts.findAll({
|
||||
where: { status: 'draft', published_at: { [Op.lte]: now } }
|
||||
});
|
||||
for (const post of posts) {
|
||||
await post.update({ status: 'published' });
|
||||
// notify subscribers, regenerate sitemap, etc.
|
||||
}
|
||||
}
|
||||
// Run via cron / scheduler every minute or use framework queue system.
|
||||
```
|
||||
|
||||
## Related Posts Algorithm
|
||||
|
||||
```javascript
|
||||
async function getRelatedPosts(db, post, limit = 5) {
|
||||
return db.posts.findAll({
|
||||
where: {
|
||||
id: { [Op.ne]: post.id },
|
||||
status: 'published',
|
||||
[Op.or]: [
|
||||
{ category_id: post.category_id },
|
||||
{ '$tags.id$': post.tags.map(t => t.id) }
|
||||
]
|
||||
},
|
||||
include: [db.tags],
|
||||
limit,
|
||||
order: [['published_at', 'DESC']]
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Caching Strategy
|
||||
|
||||
```javascript
|
||||
async function getPopularPosts(cache, db, ttlSeconds = 3600) {
|
||||
const key = 'popular-posts';
|
||||
const cached = await cache.get(key);
|
||||
if (cached) return cached;
|
||||
|
||||
const posts = await db.posts.findAll({
|
||||
where: { status: 'published' },
|
||||
order: [['view_count', 'DESC']],
|
||||
limit: 10
|
||||
});
|
||||
|
||||
await cache.set(key, posts, ttlSeconds);
|
||||
return posts;
|
||||
}
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Sanitize all user HTML input (use allow-list approach, e.g. DOMPurify)
|
||||
- [ ] Validate author permissions before edit / delete
|
||||
- [ ] Rate-limit comment submissions (e.g. 5 per minute per IP)
|
||||
- [ ] CSRF protection on all state-changing forms
|
||||
- [ ] CAPTCHA for guest comments
|
||||
- [ ] No raw SQL concatenation — use parameterized queries
|
||||
|
||||
## Integration Points
|
||||
|
||||
| Concern | Options |
|
||||
|---------|---------|
|
||||
| Email subscribers | SendGrid, Mailchimp, Resend, framework mailer |
|
||||
| CDN / image storage | Cloudflare R2, AWS S3, MinIO |
|
||||
| Analytics | Plausible, Google Analytics, Umami |
|
||||
| Search | Algolia, Meilisearch, Elasticsearch, Postgres full-text |
|
||||
| Comments external | Disqus, Facebook Comments, Giscus |
|
||||
|
||||
## Handoff Protocol
|
||||
|
||||
After implementation:
|
||||
1. End-to-end test: create → publish → view → comment
|
||||
2. Verify SEO meta tags and Open Graph
|
||||
3. Validate RSS feed and sitemap XML
|
||||
4. Test comment spam filtering
|
||||
5. Confirm scheduled publishing works
|
||||
6. Security audit via `@security-auditor`
|
||||
547
.kilo/skills/booking/SKILL.md
Normal file
547
.kilo/skills/booking/SKILL.md
Normal file
@@ -0,0 +1,547 @@
|
||||
---
|
||||
name: booking
|
||||
description: Booking domain knowledge - appointments, services, staff, schedules, availability, reservations, notifications, calendar sync
|
||||
---
|
||||
|
||||
# Booking Skill
|
||||
|
||||
## Purpose
|
||||
|
||||
Universal domain knowledge for building booking and appointment systems: services, staff, schedules, availability, reservations, notifications, calendar sync.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Service Management
|
||||
- Service categories
|
||||
- Service duration and pricing
|
||||
- Service variants (different durations / prices)
|
||||
- Required resources / rooms
|
||||
- Active / inactive toggle
|
||||
|
||||
### Staff Management
|
||||
- Staff profiles and roles (admin / manager / staff)
|
||||
- Specializations (linked to services)
|
||||
- Availability schedules
|
||||
- Time-off / vacation tracking
|
||||
|
||||
### Booking Flow
|
||||
- Service selection
|
||||
- Staff selection (optional or auto-assign)
|
||||
- Date / time slot selection
|
||||
- Customer details (guest or registered)
|
||||
- Confirmation
|
||||
- Payment (optional)
|
||||
|
||||
### Scheduling
|
||||
- Working hours per day of week
|
||||
- Break times
|
||||
- Days off
|
||||
- Multi-location support
|
||||
- Timezone handling
|
||||
- Advance booking limits (min / max)
|
||||
|
||||
### Notifications
|
||||
- Email confirmations
|
||||
- SMS reminders
|
||||
- Calendar attachments (iCal / Google Calendar)
|
||||
- Push notifications (if applicable)
|
||||
|
||||
### Admin Dashboard
|
||||
- Booking list with filters (status, date, staff)
|
||||
- Availability editor
|
||||
- Customer CRM view
|
||||
- Revenue reports
|
||||
- Staff utilization
|
||||
|
||||
## Database Schema (Universal)
|
||||
|
||||
Use `id` types appropriate to your DB (INTEGER / UUID / BIGSERIAL). Adapt table/column names to your framework conventions.
|
||||
|
||||
### Services
|
||||
|
||||
```sql
|
||||
CREATE TABLE service_categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
image_url TEXT,
|
||||
sort_order INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE services (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id INTEGER,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
duration INTEGER NOT NULL, -- minutes
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
buffer_time INTEGER DEFAULT 0, -- minutes between appointments
|
||||
max_per_slot INTEGER DEFAULT 1,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
image_url TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (category_id) REFERENCES service_categories(id)
|
||||
);
|
||||
|
||||
-- Optional: per-staff overrides
|
||||
CREATE TABLE service_staff (
|
||||
service_id INTEGER NOT NULL,
|
||||
staff_id INTEGER NOT NULL,
|
||||
custom_price DECIMAL(10,2),
|
||||
custom_duration INTEGER,
|
||||
PRIMARY KEY (service_id, staff_id),
|
||||
FOREIGN KEY (service_id) REFERENCES services(id),
|
||||
FOREIGN KEY (staff_id) REFERENCES staff(id)
|
||||
);
|
||||
```
|
||||
|
||||
### Staff
|
||||
|
||||
```sql
|
||||
CREATE TABLE staff (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
phone TEXT,
|
||||
avatar_url TEXT,
|
||||
bio TEXT,
|
||||
role TEXT DEFAULT 'staff', -- 'admin' | 'manager' | 'staff'
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE staff_schedules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
staff_id INTEGER NOT NULL,
|
||||
day_of_week INTEGER NOT NULL, -- 0=Sun ... 6=Sat
|
||||
start_time TEXT NOT NULL, -- '09:00'
|
||||
end_time TEXT NOT NULL, -- '17:00'
|
||||
break_start TEXT, -- nullable
|
||||
break_end TEXT, -- nullable
|
||||
is_working BOOLEAN DEFAULT 1,
|
||||
FOREIGN KEY (staff_id) REFERENCES staff(id)
|
||||
);
|
||||
|
||||
CREATE TABLE staff_time_off (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
staff_id INTEGER NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
reason TEXT,
|
||||
FOREIGN KEY (staff_id) REFERENCES staff(id)
|
||||
);
|
||||
```
|
||||
|
||||
### Bookings
|
||||
|
||||
```sql
|
||||
CREATE TABLE bookings (
|
||||
id TEXT PRIMARY KEY, -- UUID recommended
|
||||
booking_number TEXT UNIQUE NOT NULL,
|
||||
service_id INTEGER NOT NULL,
|
||||
staff_id INTEGER,
|
||||
customer_id INTEGER,
|
||||
|
||||
-- Guest booking support
|
||||
customer_name TEXT NOT NULL,
|
||||
customer_email TEXT NOT NULL,
|
||||
customer_phone TEXT,
|
||||
customer_notes TEXT,
|
||||
|
||||
booking_date DATE NOT NULL,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT NOT NULL,
|
||||
|
||||
status TEXT DEFAULT 'pending', -- 'pending' | 'confirmed' | 'completed' | 'cancelled' | 'no_show'
|
||||
|
||||
service_price DECIMAL(10,2) NOT NULL,
|
||||
addons_total DECIMAL(10,2) DEFAULT 0,
|
||||
discount DECIMAL(10,2) DEFAULT 0,
|
||||
total DECIMAL(10,2) NOT NULL,
|
||||
|
||||
payment_method TEXT, -- 'cash' | 'card' | 'online'
|
||||
payment_status TEXT DEFAULT 'pending', -- 'pending' | 'paid' | 'refunded'
|
||||
|
||||
source TEXT DEFAULT 'website', -- 'website' | 'phone' | 'walk_in'
|
||||
notes TEXT,
|
||||
internal_notes TEXT,
|
||||
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (service_id) REFERENCES services(id),
|
||||
FOREIGN KEY (staff_id) REFERENCES staff(id),
|
||||
FOREIGN KEY (customer_id) REFERENCES customers(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_bookings_date ON bookings(booking_date);
|
||||
CREATE INDEX idx_bookings_staff ON bookings(staff_id);
|
||||
CREATE INDEX idx_bookings_status ON bookings(status);
|
||||
```
|
||||
|
||||
### Customers
|
||||
|
||||
```sql
|
||||
CREATE TABLE customers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
phone TEXT,
|
||||
date_of_birth DATE,
|
||||
notes TEXT,
|
||||
total_visits INTEGER DEFAULT 0,
|
||||
total_spent DECIMAL(10,2) DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_customers_email ON customers(email);
|
||||
CREATE INDEX idx_customers_phone ON customers(phone);
|
||||
```
|
||||
|
||||
### Settings (Key-Value)
|
||||
|
||||
```sql
|
||||
CREATE TABLE booking_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
type TEXT DEFAULT 'string' -- 'string' | 'number' | 'boolean' | 'json'
|
||||
);
|
||||
```
|
||||
|
||||
Default settings to insert:
|
||||
|
||||
| key | value | type |
|
||||
|---|---|---|
|
||||
| timezone | Europe/Moscow | string |
|
||||
| currency | RUB | string |
|
||||
| booking_interval | 30 | number |
|
||||
| min_booking_notice | 60 | number |
|
||||
| max_booking_advance | 30 | number |
|
||||
| require_phone | 1 | boolean |
|
||||
| require_payment | 0 | boolean |
|
||||
| deposit_percentage | 20 | number |
|
||||
| cancellation_hours | 24 | number |
|
||||
| reminder_hours | 2 | number |
|
||||
| confirmation_email | 1 | boolean |
|
||||
| confirmation_sms | 0 | boolean |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Public API
|
||||
|
||||
```yaml
|
||||
GET /api/services # Active services
|
||||
GET /api/services/:id # Service detail
|
||||
GET /api/categories # Service categories
|
||||
|
||||
GET /api/staff # Active staff
|
||||
GET /api/staff/:id # Staff detail
|
||||
GET /api/staff/:id/availability # Availability slots
|
||||
|
||||
GET /api/availability # Available slots (?service_id=&date=)
|
||||
POST /api/availability/check # Check specific slot
|
||||
|
||||
POST /api/bookings # Create booking (guest)
|
||||
GET /api/bookings/:id # Booking detail
|
||||
POST /api/bookings/:id/cancel # Cancel booking
|
||||
POST /api/bookings/:id/reschedule # Reschedule
|
||||
|
||||
GET /api/customer/bookings # My bookings (auth)
|
||||
POST /api/customer/register # Register customer
|
||||
```
|
||||
|
||||
### Admin API
|
||||
|
||||
```yaml
|
||||
GET /api/admin/services # All services
|
||||
POST /api/admin/services # Create
|
||||
PUT /api/admin/services/:id # Update
|
||||
DELETE /api/admin/services/:id # Delete
|
||||
|
||||
GET /api/admin/categories # Categories
|
||||
POST /api/admin/categories # Create
|
||||
PUT /api/admin/categories/:id # Update
|
||||
DELETE /api/admin/categories/:id # Delete
|
||||
|
||||
GET /api/admin/staff # All staff
|
||||
POST /api/admin/staff # Add
|
||||
PUT /api/admin/staff/:id # Update
|
||||
DELETE /api/admin/staff/:id # Remove
|
||||
PUT /api/admin/staff/:id/schedule # Update schedule
|
||||
POST /api/admin/staff/:id/time-off # Add time off
|
||||
|
||||
GET /api/admin/bookings # List (filtered)
|
||||
GET /api/admin/bookings/:id # Detail
|
||||
PUT /api/admin/bookings/:id/confirm
|
||||
PUT /api/admin/bookings/:id/complete
|
||||
PUT /api/admin/bookings/:id/cancel
|
||||
PUT /api/admin/bookings/:id/no-show
|
||||
|
||||
GET /api/admin/calendar # Calendar view
|
||||
GET /api/admin/calendar/staff/:id # Staff calendar
|
||||
|
||||
GET /api/admin/customers # Customers
|
||||
GET /api/admin/customers/:id # Detail + history
|
||||
|
||||
GET /api/admin/reports/revenue # Revenue report
|
||||
GET /api/admin/reports/services # Service popularity
|
||||
GET /api/admin/reports/staff # Utilization
|
||||
GET /api/admin/reports/trends # Booking trends
|
||||
|
||||
GET /api/admin/settings # All settings
|
||||
PUT /api/admin/settings # Update
|
||||
```
|
||||
|
||||
## Availability Algorithm
|
||||
|
||||
### Generate Available Slots
|
||||
|
||||
```javascript
|
||||
function getAvailableSlots(service, staffSchedule, existingBookings, date) {
|
||||
const dayOfWeek = new Date(date).getDay();
|
||||
const schedule = staffSchedule.find(s => s.day_of_week === dayOfWeek && s.is_working);
|
||||
if (!schedule) return [];
|
||||
|
||||
const slots = [];
|
||||
let current = parseTime(schedule.start_time);
|
||||
const end = parseTime(schedule.end_time);
|
||||
const duration = service.duration + (service.buffer_time || 0);
|
||||
|
||||
while (addMinutes(current, duration) <= end) {
|
||||
const slotStart = formatTime(current);
|
||||
const slotEnd = formatTime(addMinutes(current, service.duration));
|
||||
|
||||
const isBooked = existingBookings.some(b =>
|
||||
b.start_time < slotEnd && b.end_time > slotStart
|
||||
);
|
||||
|
||||
const isBreak = schedule.break_start && schedule.break_end &&
|
||||
slotStart < schedule.break_end && slotEnd > schedule.break_start;
|
||||
|
||||
const slotDateTime = new Date(`${date}T${slotStart}`);
|
||||
const isTooSoon = slotDateTime < addMinutes(new Date(), settings.min_booking_notice);
|
||||
const isPast = slotDateTime < new Date();
|
||||
|
||||
if (!isBooked && !isBreak && !isTooSoon && !isPast) {
|
||||
slots.push({ start_time: slotStart, end_time: slotEnd, available: true });
|
||||
}
|
||||
|
||||
current = addMinutes(current, duration);
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
```
|
||||
|
||||
### Create Booking
|
||||
|
||||
```javascript
|
||||
async function createBooking(db, data) {
|
||||
const service = await db.services.findById(data.service_id);
|
||||
if (!service || !service.is_active) throw new Error('Service unavailable');
|
||||
|
||||
const staff = data.staff_id
|
||||
? await db.staff.findById(data.staff_id)
|
||||
: await autoAssignStaff(db, service.id, data.date, data.time);
|
||||
if (!staff) throw new Error('No staff available');
|
||||
|
||||
const available = await checkAvailability(db, service.id, staff.id, data.date, data.time);
|
||||
if (!available) throw new Error('Slot already booked');
|
||||
|
||||
const endTime = addMinutes(parseTime(data.time), service.duration);
|
||||
|
||||
const booking = await db.bookings.create({
|
||||
id: generateUUID(),
|
||||
booking_number: generateBookingNumber(),
|
||||
service_id: service.id,
|
||||
staff_id: staff.id,
|
||||
customer_name: data.customer_name,
|
||||
customer_email: data.customer_email,
|
||||
customer_phone: data.customer_phone,
|
||||
booking_date: data.date,
|
||||
start_time: data.time,
|
||||
end_time: formatTime(endTime),
|
||||
status: 'pending',
|
||||
service_price: service.price,
|
||||
total: service.price
|
||||
});
|
||||
|
||||
// Optional: lock slot in availability cache / table
|
||||
await sendConfirmation(booking, service, staff);
|
||||
return booking;
|
||||
}
|
||||
```
|
||||
|
||||
## Booking Status Flow
|
||||
|
||||
```
|
||||
pending → confirmed → completed
|
||||
↓ ↓ ↓
|
||||
cancelled cancelled cancelled → refunded
|
||||
↓
|
||||
no_show
|
||||
```
|
||||
|
||||
Valid transitions:
|
||||
|
||||
| From | To |
|
||||
|---|---|
|
||||
| pending | confirmed, cancelled |
|
||||
| confirmed | completed, cancelled, no_show |
|
||||
| completed | refunded |
|
||||
| cancelled | refunded |
|
||||
| no_show | — |
|
||||
| refunded | — |
|
||||
|
||||
```javascript
|
||||
const STATUS_FLOW = {
|
||||
pending: ['confirmed', 'cancelled'],
|
||||
confirmed: ['completed', 'cancelled', 'no_show'],
|
||||
completed: ['refunded'],
|
||||
cancelled: ['refunded'],
|
||||
no_show: [],
|
||||
refunded: []
|
||||
};
|
||||
```
|
||||
|
||||
## Notifications
|
||||
|
||||
### Email Templates
|
||||
|
||||
```javascript
|
||||
function bookingConfirmation(booking, service, staff) {
|
||||
return {
|
||||
subject: `Booking Confirmed - ${service.name}`,
|
||||
html: `
|
||||
<h1>Your booking is confirmed!</h1>
|
||||
<p>Booking #${booking.booking_number}</p>
|
||||
<ul>
|
||||
<li>Service: ${service.name}</li>
|
||||
<li>Staff: ${staff.name}</li>
|
||||
<li>Date: ${formatDate(booking.booking_date)}</li>
|
||||
<li>Time: ${booking.start_time} - ${booking.end_time}</li>
|
||||
<li>Price: ${formatCurrency(booking.total)}</li>
|
||||
</ul>
|
||||
<p><a href="${config.siteUrl}/booking/${booking.id}">Manage booking</a></p>
|
||||
`
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### SMS Templates
|
||||
|
||||
```javascript
|
||||
const smsTemplates = {
|
||||
confirmation: (b, s) =>
|
||||
`Your ${s.name} booking confirmed for ${formatDate(b.booking_date)} at ${b.start_time}. #${b.booking_number}`,
|
||||
reminder: (b, s) =>
|
||||
`Reminder: ${s.name} appointment in ${settings.reminder_hours}h at ${b.start_time}.`,
|
||||
cancellation: b =>
|
||||
`Your booking #${b.booking_number} has been cancelled.`
|
||||
};
|
||||
```
|
||||
|
||||
## Calendar Integration
|
||||
|
||||
### iCal Export
|
||||
|
||||
```javascript
|
||||
function generateICal(booking, service, staff) {
|
||||
return `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
DTSTART:${toICalDate(booking.booking_date, booking.start_time)}
|
||||
DTEND:${toICalDate(booking.booking_date, booking.end_time)}
|
||||
SUMMARY:${service.name} with ${staff.name}
|
||||
DESCRIPTION:Booking #${booking.booking_number}
|
||||
STATUS:CONFIRMED
|
||||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
}
|
||||
```
|
||||
|
||||
## Reports
|
||||
|
||||
### Revenue Report
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
DATE(booking_date) AS date,
|
||||
COUNT(*) AS bookings,
|
||||
SUM(total) AS revenue,
|
||||
AVG(total) AS avg_booking
|
||||
FROM bookings
|
||||
WHERE status IN ('completed', 'confirmed')
|
||||
AND booking_date BETWEEN ? AND ?
|
||||
GROUP BY DATE(booking_date)
|
||||
ORDER BY date;
|
||||
```
|
||||
|
||||
### Staff Utilization
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
s.id,
|
||||
s.name,
|
||||
COUNT(b.id) AS bookings,
|
||||
SUM(
|
||||
CAST(strftime('%s', b.end_time) AS INTEGER) -
|
||||
CAST(strftime('%s', b.start_time) AS INTEGER)
|
||||
) / 3600.0 AS hours_booked
|
||||
FROM staff s
|
||||
LEFT JOIN bookings b ON b.staff_id = s.id
|
||||
AND b.status IN ('completed', 'confirmed')
|
||||
AND b.booking_date BETWEEN ? AND ?
|
||||
GROUP BY s.id
|
||||
ORDER BY bookings DESC;
|
||||
```
|
||||
|
||||
### Service Popularity
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
s.id,
|
||||
s.name,
|
||||
COUNT(b.id) AS bookings,
|
||||
SUM(b.total) AS revenue
|
||||
FROM services s
|
||||
LEFT JOIN bookings b ON b.service_id = s.id
|
||||
AND b.status IN ('completed', 'confirmed')
|
||||
AND b.booking_date BETWEEN ? AND ?
|
||||
GROUP BY s.id
|
||||
ORDER BY bookings DESC;
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Validate service exists and is active before booking
|
||||
- [ ] Prevent double-booking via DB unique constraint or atomic check
|
||||
- [ ] Rate-limit booking creation (e.g. 10/hour per IP)
|
||||
- [ ] Sanitize customer notes / internal notes
|
||||
- [ ] No raw SQL — parameterized queries only
|
||||
- [ ] Admin endpoints protected by role middleware
|
||||
|
||||
## Integration Points
|
||||
|
||||
| Concern | Options |
|
||||
|---------|---------|
|
||||
| Payment | Stripe, YooKassa, PayPal |
|
||||
| SMS | Twilio, SMS.ru, Vonage |
|
||||
| Email | SendGrid, Mailgun, Resend, framework mailer |
|
||||
| Calendar | Google Calendar API, iCal attachments |
|
||||
| Analytics | Google Analytics, Yandex Metrika |
|
||||
|
||||
## Handoff Protocol
|
||||
|
||||
After implementation:
|
||||
1. End-to-end test: select service → staff → slot → book → confirm
|
||||
2. Verify no double-booking with concurrent requests
|
||||
3. Test status transitions and notifications
|
||||
4. Validate calendar attachment downloads correctly
|
||||
5. Confirm reports return accurate numbers
|
||||
6. Security audit via `@security-auditor`
|
||||
489
.kilo/skills/ecommerce/SKILL.md
Normal file
489
.kilo/skills/ecommerce/SKILL.md
Normal file
@@ -0,0 +1,489 @@
|
||||
---
|
||||
name: ecommerce
|
||||
description: E-commerce domain knowledge - products, variants, categories, cart, orders, payments, inventory, discounts
|
||||
---
|
||||
|
||||
# E-commerce Skill
|
||||
|
||||
## Purpose
|
||||
|
||||
Universal domain knowledge for building e-commerce systems: product catalogs, shopping carts, order processing, payment integration, inventory management, discounts.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Product Catalog
|
||||
- Product CRUD with variants (size, color, material)
|
||||
- Categories (hierarchical)
|
||||
- Tags / attributes
|
||||
- Pricing: base, compare-at, cost, per-variant adjustments
|
||||
- Image galleries
|
||||
- Search and faceted filtering
|
||||
|
||||
### Shopping Cart
|
||||
- Add / remove / update quantities
|
||||
- Guest (session) and persistent (registered) carts
|
||||
- Price recalculation
|
||||
- Discount codes
|
||||
- Shipping estimation
|
||||
|
||||
### Order Processing
|
||||
- Order creation from cart
|
||||
- Status workflow with history
|
||||
- Invoice generation
|
||||
- Email notifications
|
||||
- Guest checkout support
|
||||
|
||||
### Payments
|
||||
- Stripe, PayPal, YooKassa integration patterns
|
||||
- Payment status tracking
|
||||
- Refund processing
|
||||
- Webhook handling (idempotent)
|
||||
|
||||
### Inventory
|
||||
- Stock tracking per SKU / variant
|
||||
- Low-stock alerts
|
||||
- Inventory adjustments with reason
|
||||
- Concurrent purchase safety (locks or optimistic)
|
||||
|
||||
### Discounts
|
||||
- Coupon codes (percentage / fixed / free shipping)
|
||||
- Usage limits (global, per-user, per-code)
|
||||
- Date validity
|
||||
- Minimum order requirements
|
||||
|
||||
## Database Schema (Universal)
|
||||
|
||||
Use `id` types appropriate to your DB (INTEGER / UUID / BIGSERIAL). Adapt naming to your framework conventions.
|
||||
|
||||
### Products
|
||||
|
||||
```sql
|
||||
CREATE TABLE categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
parent_id INTEGER,
|
||||
description TEXT,
|
||||
image_url TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (parent_id) REFERENCES categories(id)
|
||||
);
|
||||
|
||||
CREATE TABLE products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sku TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
compare_at_price DECIMAL(10,2),
|
||||
cost_price DECIMAL(10,2),
|
||||
quantity INTEGER DEFAULT 0, -- base stock if no variants
|
||||
category_id INTEGER,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id)
|
||||
);
|
||||
|
||||
CREATE TABLE product_variants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
product_id INTEGER NOT NULL,
|
||||
sku TEXT UNIQUE NOT NULL,
|
||||
name TEXT NOT NULL, -- e.g. "Red / Large"
|
||||
price_adjustment DECIMAL(10,2) DEFAULT 0,
|
||||
quantity INTEGER DEFAULT 0,
|
||||
attributes TEXT, -- JSON {"color":"red","size":"L"}
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
);
|
||||
|
||||
CREATE TABLE product_images (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
product_id INTEGER,
|
||||
variant_id INTEGER,
|
||||
url TEXT NOT NULL,
|
||||
alt_text TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id),
|
||||
FOREIGN KEY (variant_id) REFERENCES product_variants(id)
|
||||
);
|
||||
```
|
||||
|
||||
### Cart
|
||||
|
||||
```sql
|
||||
CREATE TABLE carts (
|
||||
id TEXT PRIMARY KEY, -- UUID
|
||||
user_id INTEGER,
|
||||
session_id TEXT,
|
||||
currency TEXT DEFAULT 'USD',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE cart_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cart_id TEXT NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
variant_id INTEGER,
|
||||
quantity INTEGER NOT NULL DEFAULT 1,
|
||||
price DECIMAL(10,2) NOT NULL, -- snapshot at add time
|
||||
FOREIGN KEY (cart_id) REFERENCES carts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id),
|
||||
FOREIGN KEY (variant_id) REFERENCES product_variants(id)
|
||||
);
|
||||
```
|
||||
|
||||
### Orders
|
||||
|
||||
```sql
|
||||
CREATE TABLE orders (
|
||||
id TEXT PRIMARY KEY, -- UUID
|
||||
order_number TEXT UNIQUE NOT NULL,
|
||||
user_id INTEGER,
|
||||
email TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending', -- see status table below
|
||||
subtotal DECIMAL(10,2) NOT NULL,
|
||||
tax DECIMAL(10,2) DEFAULT 0,
|
||||
shipping DECIMAL(10,2) DEFAULT 0,
|
||||
discount DECIMAL(10,2) DEFAULT 0,
|
||||
total DECIMAL(10,2) NOT NULL,
|
||||
currency TEXT DEFAULT 'USD',
|
||||
shipping_address TEXT, -- JSON or normalized columns
|
||||
billing_address TEXT, -- JSON or normalized columns
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE order_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id TEXT NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
variant_id INTEGER,
|
||||
name TEXT NOT NULL, -- snapshot
|
||||
sku TEXT NOT NULL, -- snapshot
|
||||
quantity INTEGER NOT NULL,
|
||||
price DECIMAL(10,2) NOT NULL, -- unit price snapshot
|
||||
total DECIMAL(10,2) NOT NULL, -- price * quantity
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id),
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
);
|
||||
|
||||
CREATE TABLE order_status_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
created_by INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id)
|
||||
);
|
||||
```
|
||||
|
||||
### Payments
|
||||
|
||||
```sql
|
||||
CREATE TABLE payments (
|
||||
id TEXT PRIMARY KEY, -- UUID
|
||||
order_id TEXT NOT NULL,
|
||||
provider TEXT NOT NULL, -- 'stripe' | 'paypal' | 'yookassa'
|
||||
transaction_id TEXT,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
currency TEXT DEFAULT 'USD',
|
||||
status TEXT DEFAULT 'pending', -- 'pending' | 'completed' | 'failed' | 'refunded'
|
||||
metadata TEXT, -- JSON
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id)
|
||||
);
|
||||
```
|
||||
|
||||
### Inventory
|
||||
|
||||
```sql
|
||||
CREATE TABLE inventory_transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
product_id INTEGER,
|
||||
variant_id INTEGER,
|
||||
quantity INTEGER NOT NULL,
|
||||
type TEXT NOT NULL, -- 'purchase' | 'sale' | 'adjustment' | 'return'
|
||||
reference TEXT, -- order_id or PO_id
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id),
|
||||
FOREIGN KEY (variant_id) REFERENCES product_variants(id)
|
||||
);
|
||||
```
|
||||
|
||||
### Discounts
|
||||
|
||||
```sql
|
||||
CREATE TABLE discount_codes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT UNIQUE NOT NULL,
|
||||
type TEXT NOT NULL, -- 'percentage' | 'fixed' | 'free_shipping'
|
||||
value DECIMAL(10,2) NOT NULL, -- percent or amount
|
||||
min_order_amount DECIMAL(10,2) DEFAULT 0,
|
||||
max_discount DECIMAL(10,2),
|
||||
usage_limit INTEGER, -- total global uses
|
||||
usage_limit_per_customer INTEGER,
|
||||
used_count INTEGER DEFAULT 0,
|
||||
starts_at DATETIME,
|
||||
expires_at DATETIME,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## Order Statuses
|
||||
|
||||
| Status | Description | Next |
|
||||
|---|---|---|
|
||||
| `pending` | Created, awaiting payment | `processing`, `cancelled` |
|
||||
| `processing` | Payment confirmed, preparing | `shipped`, `on_hold` |
|
||||
| `shipped` | Dispatched to customer | `delivered` |
|
||||
| `delivered` | Customer received | `completed` |
|
||||
| `completed` | Order fulfilled | — |
|
||||
| `on_hold` | Manual review / fraud check | `processing`, `cancelled` |
|
||||
| `cancelled` | Order cancelled | `refunded` |
|
||||
| `refunded` | Money returned | — |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Public / Storefront
|
||||
|
||||
```yaml
|
||||
GET /api/products # List products (paginated, filtered)
|
||||
GET /api/products/:slug # Product detail with variants
|
||||
GET /api/categories # Categories tree
|
||||
GET /api/categories/:slug # Category + products
|
||||
GET /api/tags # Tags
|
||||
|
||||
GET /api/cart # Current cart
|
||||
POST /api/cart/items # Add to cart
|
||||
PUT /api/cart/items/:id # Update quantity
|
||||
DELETE /api/cart/items/:id # Remove item
|
||||
POST /api/cart/coupon # Apply / remove coupon
|
||||
|
||||
POST /api/checkout # Create order from cart
|
||||
POST /api/checkout/guest # Guest checkout
|
||||
GET /api/orders/:id # Order detail (guest by token)
|
||||
```
|
||||
|
||||
### Admin / Management
|
||||
|
||||
```yaml
|
||||
GET /api/admin/products # List products
|
||||
POST /api/admin/products # Create product
|
||||
PUT /api/admin/products/:id # Update
|
||||
DELETE /api/admin/products/:id # Delete
|
||||
|
||||
GET /api/admin/categories # Categories
|
||||
POST /api/admin/categories # Create
|
||||
PUT /api/admin/categories/:id # Update
|
||||
DELETE /api/admin/categories/:id # Delete
|
||||
|
||||
GET /api/admin/orders # Orders (filtered)
|
||||
GET /api/admin/orders/:id # Order detail
|
||||
PUT /api/admin/orders/:id/status
|
||||
POST /api/admin/orders/:id/refund
|
||||
|
||||
GET /api/admin/inventory # Inventory snapshot
|
||||
PUT /api/admin/inventory/:id # Update stock
|
||||
POST /api/admin/inventory/adjust # Adjustment with reason
|
||||
|
||||
GET /api/admin/discounts # Discount codes
|
||||
POST /api/admin/discounts # Create
|
||||
PUT /api/admin/discounts/:id # Update
|
||||
```
|
||||
|
||||
## Payment Integration
|
||||
|
||||
### Stripe Pattern
|
||||
|
||||
```javascript
|
||||
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
||||
|
||||
async function createPaymentIntent(order) {
|
||||
const intent = await stripe.paymentIntents.create({
|
||||
amount: Math.round(order.total * 100), // cents
|
||||
currency: order.currency.toLowerCase(),
|
||||
metadata: { orderId: order.id },
|
||||
receipt_email: order.email
|
||||
});
|
||||
return { clientSecret: intent.client_secret, paymentIntentId: intent.id };
|
||||
}
|
||||
|
||||
async function handleWebhook(signature, payload) {
|
||||
const event = stripe.webhooks.constructEvent(
|
||||
payload, signature, process.env.STRIPE_WEBHOOK_SECRET
|
||||
);
|
||||
switch (event.type) {
|
||||
case 'payment_intent.succeeded':
|
||||
await markOrderPaid(event.data.object.metadata.orderId);
|
||||
break;
|
||||
case 'payment_intent.payment_failed':
|
||||
await markOrderFailed(event.data.object.metadata.orderId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PayPal Pattern
|
||||
|
||||
```javascript
|
||||
const paypal = require('@paypal/checkout-server-sdk');
|
||||
|
||||
async function createPayPalOrder(order) {
|
||||
const request = new paypal.orders.OrdersCreateRequest();
|
||||
request.requestBody({
|
||||
intent: 'CAPTURE',
|
||||
purchase_units: [{
|
||||
amount: {
|
||||
currency_code: order.currency,
|
||||
value: order.total.toFixed(2)
|
||||
},
|
||||
reference_id: order.id
|
||||
}]
|
||||
});
|
||||
const response = await client.execute(request);
|
||||
return response.result;
|
||||
}
|
||||
|
||||
async function capturePayPalOrder(orderId) {
|
||||
const request = new paypal.orders.OrdersCaptureRequest(orderId);
|
||||
const response = await client.execute(request);
|
||||
return response.result;
|
||||
}
|
||||
```
|
||||
|
||||
## Inventory Safety
|
||||
|
||||
### Stock Deduction at Checkout
|
||||
|
||||
```javascript
|
||||
async function deductStock(db, orderId) {
|
||||
await db.transaction(async (trx) => {
|
||||
for (const item of orderItems) {
|
||||
const table = item.variant_id ? 'product_variants' : 'products';
|
||||
const idField = item.variant_id ? 'id' : 'id';
|
||||
const idValue = item.variant_id || item.product_id;
|
||||
|
||||
const result = await trx(table)
|
||||
.where({ id: idValue, quantity: db.raw('quantity >= ?', [item.quantity]) })
|
||||
.decrement('quantity', item.quantity);
|
||||
|
||||
if (result === 0) throw new Error(`Insufficient stock for SKU ${item.sku}`);
|
||||
|
||||
await trx('inventory_transactions').insert({
|
||||
product_id: item.product_id,
|
||||
variant_id: item.variant_id,
|
||||
quantity: -item.quantity,
|
||||
type: 'sale',
|
||||
reference: orderId
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Low-Stock Alert Query
|
||||
|
||||
```sql
|
||||
SELECT id, sku, name, quantity
|
||||
FROM products
|
||||
WHERE quantity <= ? AND is_active = 1;
|
||||
|
||||
-- For variants
|
||||
SELECT v.id, v.sku, p.name || ' - ' || v.name AS full_name, v.quantity
|
||||
FROM product_variants v
|
||||
JOIN products p ON p.id = v.product_id
|
||||
WHERE v.quantity <= ? AND v.is_active = 1;
|
||||
```
|
||||
|
||||
## Discount Logic
|
||||
|
||||
```javascript
|
||||
function applyDiscount(cart, code) {
|
||||
if (!code.is_active) throw new Error('Code inactive');
|
||||
if (code.starts_at && now < code.starts_at) throw new Error('Not started');
|
||||
if (code.expires_at && now > code.expires_at) throw new Error('Expired');
|
||||
if (code.usage_limit && code.used_count >= code.usage_limit) throw new Error('Limit reached');
|
||||
if (cart.subtotal < code.min_order_amount) throw new Error('Minimum order not met');
|
||||
|
||||
let discount = 0;
|
||||
if (code.type === 'percentage') {
|
||||
discount = cart.subtotal * (code.value / 100);
|
||||
} else if (code.type === 'fixed') {
|
||||
discount = Math.min(code.value, cart.subtotal);
|
||||
} else if (code.type === 'free_shipping') {
|
||||
discount = cart.shipping;
|
||||
}
|
||||
|
||||
if (code.max_discount) discount = Math.min(discount, code.max_discount);
|
||||
return discount;
|
||||
}
|
||||
```
|
||||
|
||||
## Email Templates
|
||||
|
||||
### Order Confirmation
|
||||
|
||||
```html
|
||||
<h1>Thank you for your order!</h1>
|
||||
<p>Order #{{order_number}}</p>
|
||||
<ul>
|
||||
{{#items}}
|
||||
<li>{{name}} ({{sku}}) × {{quantity}} — {{currency}}{{total}}</li>
|
||||
{{/items}}
|
||||
</ul>
|
||||
<p>Subtotal: {{currency}}{{subtotal}}</p>
|
||||
<p>Shipping: {{currency}}{{shipping}}</p>
|
||||
<p>Discount: -{{currency}}{{discount}}</p>
|
||||
<p><strong>Total: {{currency}}{{total}}</strong></p>
|
||||
<p>We'll email tracking info once shipped.</p>
|
||||
```
|
||||
|
||||
### Shipping Notification
|
||||
|
||||
```html
|
||||
<h1>Your order has shipped!</h1>
|
||||
<p>Order #{{order_number}}</p>
|
||||
<p>Carrier: {{carrier}}</p>
|
||||
<p>Tracking: <a href="{{tracking_url}}">{{tracking_number}}</a></p>
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Never store credit card numbers
|
||||
- [ ] Use PCI-compliant payment providers only
|
||||
- [ ] CSRF protection on all state-changing cart / checkout endpoints
|
||||
- [ ] Rate-limit checkout attempts (e.g. 5/minute per IP)
|
||||
- [ ] Validate all discount constraints server-side
|
||||
- [ ] Inventory deduction inside atomic transaction
|
||||
- [ ] Webhook signatures verified before acting
|
||||
- [ ] Order totals recalculated server-side; never trust client total
|
||||
|
||||
## Integration Points
|
||||
|
||||
| Concern | Options |
|
||||
|---------|---------|
|
||||
| Payment | Stripe, PayPal, YooKassa, Square |
|
||||
| Shipping rates | ShipStation, EasyPost, carrier APIs |
|
||||
| Tax calculation | TaxJar, Avalara, manual rules |
|
||||
| Email | SendGrid, Mailgun, Resend |
|
||||
| Search | Algolia, Meilisearch, Postgres full-text |
|
||||
| Analytics | Google Analytics, Mixpanel, Plausible |
|
||||
| CDN / images | Cloudflare R2, AWS S3, MinIO |
|
||||
|
||||
## Handoff Protocol
|
||||
|
||||
After implementation:
|
||||
1. End-to-end test: add to cart → apply coupon → checkout → pay → order confirmation
|
||||
2. Verify concurrent stock safety (simulate two checkouts for last item)
|
||||
3. Test payment webhooks and status transitions
|
||||
4. Validate email templates render correctly
|
||||
5. Check discount edge cases (expired, over-limit, minimum not met)
|
||||
6. Security audit via `@security-auditor`
|
||||
Reference in New Issue
Block a user