Files
APAW/.kilo/commands/commerce.md

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
read edit write bash glob grep task
allow allow allow allow allow allow
backend-developer frontend-developer system-analyst lead-developer sdet-engineer code-skeptic the-fixer release-manager security-auditor browser-automation
allow allow allow allow allow allow allow allow allow allow

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