34 KiB
34 KiB
description, mode, model, color, permission
| description | mode | model | color | permission | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Create full-stack e-commerce site with Node.js, Vue, SQLite, admin panel, payments, and Docker deployment | commerce | qwen/qwen3-coder:free | #F59E0B |
|
E-commerce Workflow
Create a full-stack e-commerce site with product catalog, shopping cart, checkout, payment integration, and admin panel. Fully tested and production-ready.
Parameters
project_name: Store name (required)ui_framework: UI framework - 'vuetify', 'quasar', 'primevue' (default: 'vuetify')payment_provider: Payment - 'stripe', 'paypal', 'both' (default: 'stripe')currency: Default currency - 'USD', 'EUR', etc. (default: 'USD')docker: Create Docker deployment (default: true)issue: Gitea issue number for tracking (optional)
Overview
Requirements → Architecture → Products → Cart → Checkout → Payments → Admin → Tests → Docker → Docs
Technology Stack
Frontend
| Component | Technology |
|---|---|
| Framework | Vue.js 3 (Composition API) |
| UI Library | Vuetify/Quasar/PrimeVue |
| State | Pinia |
| Router | Vue Router |
| HTTP | Axios |
Backend
| Component | Technology |
|---|---|
| Runtime | Node.js 20.x |
| Framework | Express.js |
| Database | SQLite (better-sqlite3) |
| Auth | JWT + bcrypt |
| Payment | Stripe/PayPal SDK |
Step 1: Requirements Analysis
Agent: @RequirementRefiner
E-commerce Requirements Checklist
## User Stories
### Product Catalog
- [ ] View products with pagination
- [ ] Filter by category
- [ ] Search products
- [ ] View product details
- [ ] Product variants (size, color)
- [ ] Product images gallery
### Shopping Cart
- [ ] Add to cart
- [ ] Update quantity
- [ ] Remove from cart
- [ ] Apply discount code
- [ ] Persistent cart (session/database)
- [ ] Price calculations
### Checkout
- [ ] Guest checkout
- [ ] User registration
- [ ] Shipping address
- [ ] Billing address
- [ ] Shipping method selection
- [ ] Order summary
- [ ] Order confirmation
### Payment
- [ ] Credit card payment (Stripe)
- [ ] PayPal payment
- [ ] Payment confirmation
- [ ] Receipt email
### User Account
- [ ] Registration
- [ ] Login/Logout
- [ ] Order history
- [ ] Saved addresses
- [ ] Wishlist
### Admin
- [ ] Product management (CRUD)
- [ ] Category management
- [ ] Order management
- [ ] Customer management
- [ ] Discount codes
- [ ] Analytics dashboard
### Non-Functional
- [ ] Responsive design
- [ ] Cross-browser support
- [ ] Performance (<3s load)
- [ ] Security (HTTPS, CSRF)
- [ ] SEO optimization
Step 2: Architecture Design
Agent: @SystemAnalyst
Project Structure
{project_name}/
├── backend/
│ ├── src/
│ │ ├── config/
│ │ │ ├── database.js
│ │ │ ├── auth.js
│ │ │ ├── stripe.js
│ │ │ └── email.js
│ │ ├── db/
│ │ │ ├── migrations/
│ │ │ └── seeds/
│ │ ├── models/
│ │ │ ├── Product.js
│ │ │ ├── Category.js
│ │ │ ├── Cart.js
│ │ │ ├── Order.js
│ │ │ ├── User.js
│ │ │ └── Payment.js
│ │ ├── routes/
│ │ │ ├── api/
│ │ │ │ ├── products.js
│ │ │ │ ├── categories.js
│ │ │ │ ├── cart.js
│ │ │ │ ├── orders.js
│ │ │ │ └── auth.js
│ │ │ └── admin/
│ │ │ ├── products.js
│ │ │ ├── orders.js
│ │ │ ├── customers.js
│ │ │ └── analytics.js
│ │ ├── services/
│ │ │ ├── payment/
│ │ │ │ ├── stripe.js
│ │ │ │ └── paypal.js
│ │ │ ├── email.js
│ │ │ └── inventory.js
│ │ └── middleware/
│ │ ├── auth.js
│ │ ├── admin.js
│ │ └── validation.js
│ └── tests/
├── frontend/
│ ├── src/
│ │ ├── views/
│ │ │ ├── public/
│ │ │ │ ├── Home.vue
│ │ │ │ ├── Products.vue
│ │ │ │ ├── Product.vue
│ │ │ │ ├── Cart.vue
│ │ │ │ ├── Checkout.vue
│ │ │ │ └── Order.vue
│ │ │ ├── account/
│ │ │ │ ├── Login.vue
│ │ │ │ ├── Register.vue
│ │ │ │ ├── Orders.vue
│ │ │ │ └── Wishlist.vue
│ │ │ └── admin/
│ │ │ ├── Dashboard.vue
│ │ │ ├── Products.vue
│ │ │ ├── Orders.vue
│ │ │ ├── Customers.vue
│ │ │ └── Settings.vue
│ │ ├── components/
│ │ │ ├── product/
│ │ │ │ ├── ProductCard.vue
│ │ │ │ ├── ProductGrid.vue
│ │ │ │ └── ProductFilters.vue
│ │ │ ├── cart/
│ │ │ │ ├── CartItem.vue
│ │ │ │ ├── CartSummary.vue
│ │ │ │ └── DiscountCode.vue
│ │ │ └── checkout/
│ │ │ ├── AddressForm.vue
│ │ │ ├── PaymentForm.vue
│ │ │ └── OrderSummary.vue
│ │ ├── stores/
│ │ │ ├── cart.js
│ │ │ ├── auth.js
│ │ │ └── products.js
│ │ └── router/
│ │ └── index.js
│ └── tests/
├── database/
│ └── shop.db
├── docker/
│ ├── Dockerfile.backend
│ ├── Dockerfile.frontend
│ └── docker-compose.yml
└── docs/
├── API.md
└── DEPLOYMENT.md
Database Schema
Use the schema from .kilo/skills/ecommerce/SKILL.md for products, categories, cart, orders, payments.
Step 3: Backend Implementation
Agent: @BackendDeveloper
Product API
// backend/src/routes/api/products.js
const router = require('express').Router();
const { query, validationResult } = require('express-validator');
// GET /api/products - List products with pagination and filters
router.get('/',
[
query('page').optional().isInt({ min: 1 }),
query('limit').optional().isInt({ min: 1, max: 100 }),
query('category').optional().isInt(),
query('search').optional().isString(),
query('minPrice').optional().isFloat(),
query('maxPrice').optional().isFloat(),
query('sort').optional().isIn(['price', 'name', 'created'])
],
async (req, res, next) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { page = 1, limit = 20, category, search, minPrice, maxPrice, sort } = req.query;
const products = await productService.findAll({
page: parseInt(page),
limit: parseInt(limit),
category: category ? parseInt(category) : undefined,
search,
minPrice: minPrice ? parseFloat(minPrice) : undefined,
maxPrice: maxPrice ? parseFloat(maxPrice) : undefined,
sort
});
res.json(products);
} catch (error) {
next(error);
}
}
);
// GET /api/products/:slug - Get product details
router.get('/:slug', async (req, res, next) => {
try {
const product = await productService.findBySlug(req.params.slug);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
// Get related products
const related = await productService.getRelated(product.id);
res.json({ product, related });
} catch (error) {
next(error);
}
});
module.exports = router;
Cart API
// backend/src/routes/api/cart.js
const router = require('express').Router();
const cartService = require('../../services/cart');
// GET /api/cart - Get current cart
router.get('/', async (req, res, next) => {
try {
const cartId = req.session.cartId || req.headers['x-cart-id'];
const cart = await cartService.getOrCreateCart(cartId, req.user?.id);
res.json(cart);
} catch (error) {
next(error);
}
});
// POST /api/cart/items - Add item to cart
router.post('/items',
[
body('productId').isInt(),
body('variantId').optional().isInt(),
body('quantity').isInt({ min: 1 })
],
async (req, res, next) => {
try {
const cartId = req.session.cartId || req.headers['x-cart-id'];
const { productId, variantId, quantity } = req.body;
const cart = await cartService.addItem(
cartId,
parseInt(productId),
variantId ? parseInt(variantId) : null,
parseInt(quantity)
);
req.session.cartId = cart.id;
res.json(cart);
} catch (error) {
next(error);
}
}
);
// PUT /api/cart/items/:id - Update quantity
router.put('/items/:id',
[body('quantity').isInt({ min: 0 })],
async (req, res, next) => {
try {
const { quantity } = req.body;
const cart = await cartService.updateItem(
parseInt(req.params.id),
parseInt(quantity)
);
res.json(cart);
} catch (error) {
next(error);
}
}
);
// DELETE /api/cart/items/:id - Remove item
router.delete('/items/:id', async (req, res, next) => {
try {
const cart = await cartService.removeItem(parseInt(req.params.id));
res.json(cart);
} catch (error) {
next(error);
}
});
// POST /api/cart/coupon - Apply discount
router.post('/coupon',
[body('code').isString()],
async (req, res, next) => {
try {
const { code } = req.body;
const cartId = req.session.cartId;
const cart = await cartService.applyCoupon(cartId, code);
res.json(cart);
} catch (error) {
next(error);
}
}
);
module.exports = router;
Checkout API
// backend/src/routes/api/checkout.js
const router = require('express').Router();
const checkoutService = require('../../services/checkout');
const stripeService = require('../../services/payment/stripe');
const { requireAuth, optionalAuth } = require('../../middleware/auth');
// POST /api/checkout - Create order from cart
router.post('/',
optionalAuth,
[
body('email').isEmail(),
body('shippingAddress').isObject(),
body('billingAddress').optional().isObject(),
body('shippingMethod').isString(),
body('paymentMethod').isIn(['stripe', 'paypal'])
],
async (req, res, next) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const cartId = req.session.cartId;
const userId = req.user?.id;
const order = await checkoutService.createOrder({
cartId,
userId,
email: req.body.email,
shippingAddress: req.body.shippingAddress,
billingAddress: req.body.billingAddress || req.body.shippingAddress,
shippingMethod: req.body.shippingMethod,
paymentMethod: req.body.paymentMethod
});
// Create payment intent
const payment = await stripeService.createPaymentIntent(order);
res.status(201).json({
order,
payment,
clientSecret: payment.clientSecret
});
} catch (error) {
next(error);
}
}
);
// POST /api/checkout/guest - Guest checkout
router.post('/guest',
[body('email').isEmail()],
async (req, res, next) => {
// Same as above but always creates guest order
}
);
module.exports = router;
Order Status Transitions
// backend/src/services/orders.js
const ORDER_STATUSES = {
pending: { next: ['processing', 'cancelled'] },
processing: { next: ['on_hold', 'shipped'] },
on_hold: { next: ['processing', 'cancelled'] },
shipped: { next: ['delivered'] },
delivered: { next: ['completed'] },
completed: { next: [] },
cancelled: { next: ['refunded'] },
refunded: { next: [] }
};
async function updateStatus(orderId, newStatus) {
const order = await this.findById(orderId);
if (!ORDER_STATUSES[order.status].next.includes(newStatus)) {
throw new Error(`Cannot transition from ${order.status} to ${newStatus}`);
}
await this.db.orders.update(orderId, { status: newStatus });
// Send notification
await this.notificationService.send(order.userId, {
type: 'order_status',
orderId,
status: newStatus
});
return this.findById(orderId);
}
Step 4: Frontend Implementation
Agent: @FrontendDeveloper
Product Catalog
<!-- frontend/src/views/public/Products.vue -->
<template>
<v-container>
<!-- Filters Sidebar -->
<v-row>
<v-col cols="12" md="3">
<ProductFilters
:categories="categories"
:price-range="priceRange"
@filter="applyFilters"
/>
</v-col>
<!-- Product Grid -->
<v-col cols="12" md="9">
<v-row>
<v-col
v-for="product in products"
:key="product.id"
cols="12" sm="6" lg="4"
>
<ProductCard
:product="product"
@add-to-cart="addToCart(product)"
/>
</v-col>
</v-row>
<v-pagination
v-model="page"
:length="totalPages"
@input="loadProducts"
/>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useCartStore } from '@/stores/cart';
import { useProductStore } from '@/stores/products';
import ProductCard from '@/components/product/ProductCard.vue';
import ProductFilters from '@/components/product/ProductFilters.vue';
const route = useRoute();
const cartStore = useCartStore();
const productStore = useProductStore();
const products = ref([]);
const categories = ref([]);
const page = ref(1);
const totalPages = ref(1);
const priceRange = ref({ min: 0, max: 1000 });
async function loadProducts() {
const response = await productStore.fetchProducts({
page: page.value,
...route.query
});
products.value = response.data;
totalPages.value = response.meta.totalPages;
}
async function addToCart(product) {
await cartStore.addItem(product.id, null, 1);
}
async function applyFilters(filters) {
const query = { ...route.query, ...filters };
await router.push({ query });
}
</script>
Shopping Cart
<!-- frontend/src/views/public/Cart.vue -->
<template>
<v-container>
<v-row v-if="cart.items.length === 0">
<v-col>
<v-alert type="info">Your cart is empty</v-alert>
<v-btn to="/products">Continue Shopping</v-btn>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12" md="8">
<v-card>
<v-list>
<v-list-item
v-for="item in cart.items"
:key="item.id"
>
<CartItem
:item="item"
@update="updateQuantity"
@remove="removeItem"
/>
</v-list-item>
</v-list>
</v-card>
</v-col>
<v-col cols="12" md="4">
<CartSummary :cart="cart">
<DiscountCode @apply="applyDiscount" />
<v-btn
color="primary"
block
to="/checkout"
:disabled="!canCheckout"
>
Proceed to Checkout
</v-btn>
</CartSummary>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { computed } from 'vue';
import { useCartStore } from '@/stores/cart';
import CartItem from '@/components/cart/CartItem.vue';
import CartSummary from '@/components/cart/CartSummary.vue';
import DiscountCode from '@/components/cart/DiscountCode.vue';
const cartStore = useCartStore();
const cart = computed(() => cartStore.cart);
const canCheckout = computed(() =>
cart.value.items.length > 0 && cart.value.total > 0
);
async function updateQuantity(itemId, quantity) {
await cartStore.updateItem(itemId, quantity);
}
async function removeItem(itemId) {
await cartStore.removeItem(itemId);
}
async function applyDiscount(code) {
await cartStore.applyDiscount(code);
}
</script>
Checkout
<!-- frontend/src/views/public/Checkout.vue -->
<template>
<v-container>
<v-stepper v-model="step">
<v-stepper-header>
<v-stepper-item :value="1" title="Address" />
<v-stepper-item :value="2" title="Shipping" />
<v-stepper-item :value="3" title="Payment" />
<v-stepper-item :value="4" title="Confirm" />
</v-stepper-header>
<v-stepper-window>
<!-- Step 1: Address -->
<v-stepper-window-item :value="1">
<AddressForm v-model="order.shippingAddress" />
<v-checkbox v-model="sameBilling" label="Billing same as shipping" />
<AddressForm
v-if="!sameBilling"
v-model="order.billingAddress"
label="Billing Address"
/>
<v-btn color="primary" @click="step = 2">Continue</v-btn>
</v-stepper-window-item>
<!-- Step 2: Shipping -->
<v-stepper-window-item :value="2">
<v-radio-group v-model="order.shippingMethod">
<v-radio
v-for="method in shippingMethods"
:key="method.id"
:value="method.id"
:label="`${method.name} - $${method.price}`"
/>
</v-radio-group>
<v-btn @click="step = 1">Back</v-btn>
<v-btn color="primary" @click="step = 3">Continue</v-btn>
</v-stepper-window-item>
<!-- Step 3: Payment -->
<v-stepper-window-item :value="3">
<PaymentForm
v-model="order.paymentMethod"
:client-secret="clientSecret"
@payment-complete="handlePaymentComplete"
/>
<v-btn @click="step = 2">Back</v-btn>
</v-stepper-window-item>
<!-- Step 4: Confirm -->
<v-stepper-window-item :value="4">
<OrderSummary :order="order" />
<v-btn color="primary" @click="placeOrder" :loading="loading">
Place Order
</v-btn>
</v-stepper-window-item>
</v-stepper-window>
</v-stepper>
</v-container>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useCartStore } from '@/stores/cart';
import { checkoutService } from '@/api/checkout';
import AddressForm from '@/components/checkout/AddressForm.vue';
import PaymentForm from '@/components/checkout/PaymentForm.vue';
import OrderSummary from '@/components/checkout/OrderSummary.vue';
const router = useRouter();
const cartStore = useCartStore();
const step = ref(1);
const loading = ref(false);
const sameBilling = ref(true);
const order = ref({
email: '',
shippingAddress: {},
billingAddress: {},
shippingMethod: 'standard',
paymentMethod: 'stripe'
});
const clientSecret = ref('');
onMounted(async () => {
// Get shipping methods
});
async function handlePaymentComplete(paymentIntent) {
step.value = 4;
}
async function placeOrder() {
loading.value = true;
try {
const result = await checkoutService.createOrder({
...order.value,
cartId: cartStore.cart.id
});
// Clear cart
await cartStore.clear();
// Navigate to order confirmation
router.push(`/order/${result.order.id}`);
} finally {
loading.value = false;
}
}
</script>
Admin Dashboard
<!-- frontend/src/views/admin/Dashboard.vue -->
<template>
<v-container>
<v-row>
<!-- Stats Cards -->
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-text>
<div class="text-overline">Total Sales</div>
<div class="text-h4">${{ stats.totalSales }}</div>
<div class="text-caption text-success">
+{{ stats.salesGrowth }}%
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-text>
<div class="text-overline">Orders</div>
<div class="text-h4">{{ stats.totalOrders }}</div>
<div class="text-caption text-info">
{{ stats.pendingOrders }} pending
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-text>
<div class="text-overline">Customers</div>
<div class="text-h4">{{ stats.totalCustomers }}</div>
<div class="text-caption text-success">
+{{ stats.newCustomers }} new
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-text>
<div class="text-overline">Products</div>
<div class="text-h4">{{ stats.totalProducts }}</div>
<div class="text-caption text-warning">
{{ stats.lowStock }} low stock
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row>
<!-- Recent Orders -->
<v-col cols="12" md="8">
<v-card>
<v-card-title>Recent Orders</v-card-title>
<v-data-table
:headers="orderHeaders"
:items="recentOrders"
:loading="loading"
>
<template #item.status="{ item }">
<v-chip :color="getStatusColor(item.status)">
{{ item.status }}
</v-chip>
</template>
<template #item.total="{ item }">
${{ item.total.toFixed(2) }}
</template>
<template #item.actions="{ item }">
<v-btn icon="mdi-eye" size="small" :to="`/admin/orders/${item.id}`" />
</template>
</v-data-table>
</v-card>
</v-col>
<!-- Low Stock Alert -->
<v-col cols="12" md="4">
<v-card>
<v-card-title>Low Stock</v-card-title>
<v-list>
<v-list-item
v-for="product in lowStockProducts"
:key="product.id"
>
<v-list-item-title>{{ product.name }}</v-list-item-title>
<v-list-item-subtitle>
{{ product.stock }} units left
</v-list-item-subtitle>
<template #append>
<v-btn size="small" :to="`/admin/products/${product.id}`">
Edit
</v-btn>
</template>
</v-list-item>
</v-list>
</v-card>
</v-col>
</v-row>
<!-- Sales Chart -->
<v-row>
<v-col cols="12">
<v-card>
<v-card-title>Sales Overview</v-card-title>
<SalesChart :data="salesData" />
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useAdminStore } from '@/stores/admin';
import SalesChart from '@/components/admin/SalesChart.vue';
const adminStore = useAdminStore();
const loading = ref(false);
const stats = ref({});
const recentOrders = ref([]);
const lowStockProducts = ref([]);
const salesData = ref([]);
const orderHeaders = [
{ title: 'Order', key: 'order_number' },
{ title: 'Customer', key: 'customer.email' },
{ title: 'Total', key: 'total' },
{ title: 'Status', key: 'status' },
{ title: 'Date', key: 'created_at' },
{ title: 'Actions', key: 'actions', sortable: false }
];
function getStatusColor(status) {
const colors = {
pending: 'warning',
processing: 'info',
shipped: 'primary',
delivered: 'success',
cancelled: 'error'
};
return colors[status] || 'grey';
}
onMounted(async () => {
loading.value = true;
const data = await adminStore.fetchDashboard();
stats.value = data.stats;
recentOrders.value = data.recentOrders;
lowStockProducts.value = data.lowStock;
salesData.value = data.salesChart;
loading.value = false;
});
</script>
Step 5: Payment Integration
Agent: @BackendDeveloper
Stripe Integration
// backend/src/services/payment/stripe.js
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
class StripeService {
async createPaymentIntent(order) {
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(order.total * 100), // Convert to cents
currency: order.currency.toLowerCase(),
metadata: {
orderId: order.id,
orderNumber: order.order_number
},
receipt_email: order.email
});
// Save payment record
await db.payments.create({
id: paymentIntent.id,
order_id: order.id,
provider: 'stripe',
amount: order.total,
currency: order.currency,
status: 'pending'
});
return {
clientSecret: paymentIntent.client_secret,
paymentIntentId: paymentIntent.id
};
}
async handleWebhook(signature, payload) {
const event = stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
switch (event.type) {
case 'payment_intent.succeeded':
await this.handlePaymentSuccess(event.data.object);
break;
case 'payment_intent.payment_failed':
await this.handlePaymentFailure(event.data.object);
break;
case 'charge.refunded':
await this.handleRefund(event.data.object);
break;
}
return { received: true };
}
async handlePaymentSuccess(paymentIntent) {
const orderId = paymentIntent.metadata.orderId;
// Update payment status
await db.payments.update(
{ id: paymentIntent.id },
{ status: 'completed' }
);
// Update order status
await orderService.updateStatus(orderId, 'processing');
// Send confirmation email
await emailService.sendOrderConfirmation(orderId);
// Deduct inventory
await inventoryService.deductForOrder(orderId);
}
async refundOrder(orderId, amount = null) {
const payment = await db.payments.findOne({ order_id: orderId });
const refund = await stripe.refunds.create({
payment_intent: payment.id,
amount: amount ? Math.round(amount * 100) : undefined
});
// Update payment status
await db.payments.update(
{ id: payment.id },
{ status: 'refunded' }
);
// Update order status
await orderService.updateStatus(orderId, 'refunded');
return refund;
}
}
module.exports = new StripeService();
Webhook Handler
// backend/src/routes/webhooks/stripe.js
const router = require('express').Router();
const stripeService = require('../../services/payment/stripe');
// Stripe webhook endpoint
router.post('/stripe',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['stripe-signature'];
try {
await stripeService.handleWebhook(signature, req.body);
res.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(400).json({ error: error.message });
}
}
);
module.exports = router;
Step 6: E2E Testing
Agent: @SDETEngineer
// tests/e2e/shop.spec.js
import { test, expect } from '@playwright/test';
test.describe('E-commerce Flow', () => {
test('complete purchase flow', async ({ page }) => {
// 1. Browse products
await page.goto('/products');
await expect(page.locator('.product-card')).toHaveCountGreaterThanOrEqual(1);
// 2. View product details
await page.click('.product-card:first-child');
await expect(page).toHaveURL(/\/products\/\d+/);
await expect(page.locator('.product-title')).toBeVisible();
// 3. Add to cart
await page.click('button:has-text("Add to Cart")');
await expect(page.locator('.cart-count')).toHaveText('1');
// 4. View cart
await page.click('.cart-icon');
await expect(page).toHaveURL('/cart');
await expect(page.locator('.cart-item')).toHaveCount(1);
// 5. Proceed to checkout
await page.click('button:has-text("Checkout")');
await expect(page).toHaveURL('/checkout');
// 6. Fill shipping address
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="firstName"]', 'John');
await page.fill('input[name="lastName"]', 'Doe');
await page.fill('input[name="address1"]', '123 Main St');
await page.fill('input[name="city"]', 'New York');
await page.fill('input[name="postalCode"]', '10001');
await page.selectOption('select[name="country"]', 'US');
await page.click('button:has-text("Continue")');
// 7. Select shipping
await page.click('input[value="standard"]');
await page.click('button:has-text("Continue")');
// 8. Enter payment (test card)
await page.fill('[name="cardNumber"]', '4242424242424242');
await page.fill('[name="expiry"]', '12/25');
await page.fill('[name="cvc"]', '123');
await page.click('button:has-text("Pay")');
// 9. Confirm order
await expect(page.locator('.order-confirmation')).toBeVisible();
await expect(page.locator('.order-number')).toBeVisible();
});
test('search and filter products', async ({ page }) => {
await page.goto('/products');
// Search
await page.fill('input[name="search"]', 'laptop');
await page.press('input[name="search"]', 'Enter');
await expect(page.locator('.product-card')).toHaveCountGreaterThanOrEqual(1);
// Filter by category
await page.click('text=Electronics');
await expect(page).toHaveURL(/category=electronics/);
// Filter by price
await page.fill('input[name="minPrice"]', '100');
await page.fill('input[name="maxPrice"]', '500');
await page.click('button:has-text("Apply")');
await expect(page).toHaveURL(/minPrice=100/);
});
test('cart persistence', async ({ page }) => {
// Add item to cart
await page.goto('/products/1');
await page.click('button:has-text("Add to Cart")');
// Refresh page
await page.reload();
await expect(page.locator('.cart-count')).toHaveText('1');
// Navigate away and back
await page.goto('/');
await page.goto('/cart');
await expect(page.locator('.cart-item')).toHaveCount(1);
});
});
test.describe('Admin Panel', () => {
test.beforeEach(async ({ page }) => {
// Login as admin
await page.goto('/admin/login');
await page.fill('input[name="email"]', 'admin@example.com');
await page.fill('input[name="password"]', 'admin123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/admin/dashboard');
});
test('create product', async ({ page }) => {
await page.goto('/admin/products/new');
await page.fill('input[name="name"]', 'Test Product');
await page.fill('input[name="sku"]', 'TEST-001');
await page.fill('input[name="price"]', '99.99');
await page.fill('textarea[name="description"]', 'Test description');
await page.selectOption('select[name="category"]', '1');
await page.fill('input[name="stock"]', '10');
await page.click('button:has-text("Save")');
await expect(page).toHaveURL(/\/admin\/products\/\d+/);
});
test('process order', async ({ page }) => {
await page.goto('/admin/orders');
await page.click('tr:first-child td:last-child button');
// Update status
await page.selectOption('select[name="status"]', 'processing');
await page.click('button:has-text("Update")');
await expect(page.locator('.status-chip')).toHaveText('processing');
});
});
Step 7: Docker & Deployment
Same as landing-page workflow but with e-commerce specific configurations.
Step 8: Documentation
API Documentation
# E-commerce API
## Public Endpoints
### Products
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/products | List products |
| GET | /api/products/:slug | Get product |
| GET | /api/categories | List categories |
| GET | /api/categories/:slug | Get category |
### Cart
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/cart | Get cart |
| POST | /api/cart/items | Add item |
| PUT | /api/cart/items/:id | Update quantity |
| DELETE | /api/cart/items/:id | Remove item |
| POST | /api/cart/coupon | Apply discount |
### Checkout
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | /api/checkout | Create order |
| POST | /api/checkout/guest | Guest checkout |
| GET | /api/orders/:id | Get order |
## Admin Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/admin/products | List all products |
| POST | /api/admin/products | Create product |
| PUT | /api/admin/products/:id | Update product |
| DELETE | /api/admin/products/:id | Delete product |
| GET | /api/admin/orders | List orders |
| PUT | /api/admin/orders/:id/status | Update order status |
| GET | /api/admin/customers | List customers |
| GET | /api/admin/analytics | Get analytics |
Post to Gitea
After each step, post progress:
post_gitea_comment(issue_number, """## ✅ E-commerce Step Complete
**Step**: {step_name}
**Duration**: {duration}
### Completed
{completed_items}
### Next
{next_step}
**Status**: {status}
""")
Quality Gates
| Gate | Criteria |
|---|---|
| Products | CRUD working, pagination, search |
| Cart | Add/remove/update, persist across sessions |
| Checkout | Complete flow working |
| Payment | Stripe integration working |
| Admin | All management functions working |
| Tests | E2E tests passing |
| Docker | Containers building and running |