diff --git a/.kilo/skills/blog/SKILL.md b/.kilo/skills/blog/SKILL.md new file mode 100644 index 0000000..f9ac104 --- /dev/null +++ b/.kilo/skills/blog/SKILL.md @@ -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 `
${highlighted}
`; + }, + image(href, title, alt) { + return `
${alt}
${alt}
`; + } + } +}); + +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 + + + {{siteUrl}}/daily1.0 + {{#posts}} + + {{siteUrl}}/posts/{{slug}} + {{updated_at}} + weekly + 0.8 + + {{/posts}} + +``` + +## 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` diff --git a/.kilo/skills/booking/SKILL.md b/.kilo/skills/booking/SKILL.md new file mode 100644 index 0000000..9a031f3 --- /dev/null +++ b/.kilo/skills/booking/SKILL.md @@ -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: ` +

Your booking is confirmed!

+

Booking #${booking.booking_number}

+ +

Manage booking

+ ` + }; +} +``` + +### 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` diff --git a/.kilo/skills/ecommerce/SKILL.md b/.kilo/skills/ecommerce/SKILL.md new file mode 100644 index 0000000..e743948 --- /dev/null +++ b/.kilo/skills/ecommerce/SKILL.md @@ -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 +

Thank you for your order!

+

Order #{{order_number}}

+ +

Subtotal: {{currency}}{{subtotal}}

+

Shipping: {{currency}}{{shipping}}

+

Discount: -{{currency}}{{discount}}

+

Total: {{currency}}{{total}}

+

We'll email tracking info once shipped.

+``` + +### Shipping Notification + +```html +

Your order has shipped!

+

Order #{{order_number}}

+

Carrier: {{carrier}}

+

Tracking: {{tracking_number}}

+``` + +## 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`