@su-record/vibe 2.5.12 → 2.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/vibe.analyze.md +3 -3
- package/commands/vibe.review.md +3 -3
- package/commands/vibe.run.md +75 -9
- package/commands/vibe.spec.md +7 -7
- package/commands/vibe.utils.md +62 -5
- package/dist/cli/setup/GlobalInstaller.d.ts +24 -0
- package/dist/cli/setup/GlobalInstaller.d.ts.map +1 -0
- package/dist/cli/setup/GlobalInstaller.js +130 -0
- package/dist/cli/setup/GlobalInstaller.js.map +1 -0
- package/dist/cli/setup/LanguageDetector.d.ts +16 -0
- package/dist/cli/setup/LanguageDetector.d.ts.map +1 -0
- package/dist/cli/setup/LanguageDetector.js +49 -0
- package/dist/cli/setup/LanguageDetector.js.map +1 -0
- package/dist/cli/setup/LegacyMigration.d.ts +25 -0
- package/dist/cli/setup/LegacyMigration.d.ts.map +1 -0
- package/dist/cli/setup/LegacyMigration.js +162 -0
- package/dist/cli/setup/LegacyMigration.js.map +1 -0
- package/dist/cli/setup/ProjectSetup.d.ts +30 -0
- package/dist/cli/setup/ProjectSetup.d.ts.map +1 -0
- package/dist/cli/setup/ProjectSetup.js +238 -0
- package/dist/cli/setup/ProjectSetup.js.map +1 -0
- package/dist/cli/setup/index.d.ts +14 -0
- package/dist/cli/setup/index.d.ts.map +1 -0
- package/dist/cli/setup/index.js +18 -0
- package/dist/cli/setup/index.js.map +1 -0
- package/dist/cli/setup.d.ts +10 -77
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +15 -592
- package/dist/cli/setup.js.map +1 -1
- package/dist/lib/llm/auth/ApiKeyManager.d.ts +21 -0
- package/dist/lib/llm/auth/ApiKeyManager.d.ts.map +1 -0
- package/dist/lib/llm/auth/ApiKeyManager.js +43 -0
- package/dist/lib/llm/auth/ApiKeyManager.js.map +1 -0
- package/dist/lib/llm/auth/ConfigManager.d.ts +29 -0
- package/dist/lib/llm/auth/ConfigManager.d.ts.map +1 -0
- package/dist/lib/llm/auth/ConfigManager.js +67 -0
- package/dist/lib/llm/auth/ConfigManager.js.map +1 -0
- package/dist/lib/llm/auth/index.d.ts +25 -0
- package/dist/lib/llm/auth/index.d.ts.map +1 -0
- package/dist/lib/llm/auth/index.js +83 -0
- package/dist/lib/llm/auth/index.js.map +1 -0
- package/dist/lib/llm/index.d.ts +10 -0
- package/dist/lib/llm/index.d.ts.map +1 -0
- package/dist/lib/llm/index.js +12 -0
- package/dist/lib/llm/index.js.map +1 -0
- package/dist/lib/llm/types.d.ts +96 -0
- package/dist/lib/llm/types.d.ts.map +1 -0
- package/dist/lib/llm/types.js +17 -0
- package/dist/lib/llm/types.js.map +1 -0
- package/dist/lib/llm/utils/index.d.ts +6 -0
- package/dist/lib/llm/utils/index.d.ts.map +1 -0
- package/dist/lib/llm/utils/index.js +6 -0
- package/dist/lib/llm/utils/index.js.map +1 -0
- package/dist/lib/llm/utils/retry.d.ts +25 -0
- package/dist/lib/llm/utils/retry.d.ts.map +1 -0
- package/dist/lib/llm/utils/retry.js +72 -0
- package/dist/lib/llm/utils/retry.js.map +1 -0
- package/dist/lib/llm/utils/stream.d.ts +13 -0
- package/dist/lib/llm/utils/stream.d.ts.map +1 -0
- package/dist/lib/llm/utils/stream.js +110 -0
- package/dist/lib/llm/utils/stream.js.map +1 -0
- package/dist/orchestrator/AgentExecutor.d.ts +23 -0
- package/dist/orchestrator/AgentExecutor.d.ts.map +1 -0
- package/dist/orchestrator/AgentExecutor.js +231 -0
- package/dist/orchestrator/AgentExecutor.js.map +1 -0
- package/dist/orchestrator/AgentManager.d.ts +73 -0
- package/dist/orchestrator/AgentManager.d.ts.map +1 -0
- package/dist/orchestrator/AgentManager.js +184 -0
- package/dist/orchestrator/AgentManager.js.map +1 -0
- package/dist/orchestrator/LLMCluster.d.ts +70 -0
- package/dist/orchestrator/LLMCluster.d.ts.map +1 -0
- package/dist/orchestrator/LLMCluster.js +91 -0
- package/dist/orchestrator/LLMCluster.js.map +1 -0
- package/dist/orchestrator/MultiLlmResearch.d.ts +27 -0
- package/dist/orchestrator/MultiLlmResearch.d.ts.map +1 -0
- package/dist/orchestrator/MultiLlmResearch.js +145 -0
- package/dist/orchestrator/MultiLlmResearch.js.map +1 -0
- package/dist/orchestrator/SessionStore.d.ts +41 -0
- package/dist/orchestrator/SessionStore.d.ts.map +1 -0
- package/dist/orchestrator/SessionStore.js +117 -0
- package/dist/orchestrator/SessionStore.js.map +1 -0
- package/dist/orchestrator/SmartRouter.d.ts +68 -0
- package/dist/orchestrator/SmartRouter.d.ts.map +1 -0
- package/dist/orchestrator/SmartRouter.js +256 -0
- package/dist/orchestrator/SmartRouter.js.map +1 -0
- package/dist/orchestrator/backgroundAgent.d.ts +10 -27
- package/dist/orchestrator/backgroundAgent.d.ts.map +1 -1
- package/dist/orchestrator/backgroundAgent.js +11 -345
- package/dist/orchestrator/backgroundAgent.js.map +1 -1
- package/dist/orchestrator/index.d.ts +3 -0
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +4 -0
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/orchestrator.d.ts +19 -154
- package/dist/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator.js +90 -514
- package/dist/orchestrator/orchestrator.js.map +1 -1
- package/dist/orchestrator/parallelResearch.d.ts +5 -12
- package/dist/orchestrator/parallelResearch.d.ts.map +1 -1
- package/dist/orchestrator/parallelResearch.js +10 -193
- package/dist/orchestrator/parallelResearch.js.map +1 -1
- package/hooks/scripts/generate-brand-assets.js +472 -0
- package/package.json +1 -1
- package/skills/brand-assets.md +141 -0
- package/skills/commerce-patterns.md +361 -0
- package/skills/e2e-commerce.md +304 -0
- package/skills/frontend-design.md +92 -0
- package/skills/seo-checklist.md +244 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: commerce-patterns
|
|
3
|
+
description: "E-commerce domain patterns - cart, payment, inventory with transaction safety"
|
|
4
|
+
triggers: [commerce, ecommerce, cart, payment, checkout, inventory, stock, order, pg, toss, stripe]
|
|
5
|
+
priority: 70
|
|
6
|
+
---
|
|
7
|
+
# Commerce Patterns Skill
|
|
8
|
+
|
|
9
|
+
E-commerce domain patterns for reliable transactions.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
- Shopping cart implementation
|
|
14
|
+
- Payment integration (PG, Stripe, Toss)
|
|
15
|
+
- Inventory/stock management
|
|
16
|
+
- Order processing systems
|
|
17
|
+
|
|
18
|
+
## Core Patterns
|
|
19
|
+
|
|
20
|
+
### 1. Cart (Shopping Cart)
|
|
21
|
+
|
|
22
|
+
#### State Model
|
|
23
|
+
```typescript
|
|
24
|
+
interface CartItem {
|
|
25
|
+
productId: string;
|
|
26
|
+
variantId?: string;
|
|
27
|
+
quantity: number;
|
|
28
|
+
price: number; // Snapshot at add time
|
|
29
|
+
originalPrice: number; // For comparison
|
|
30
|
+
addedAt: Date;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Cart {
|
|
34
|
+
id: string;
|
|
35
|
+
userId?: string; // null for guest
|
|
36
|
+
sessionId: string; // For guest merge
|
|
37
|
+
items: CartItem[];
|
|
38
|
+
couponCode?: string;
|
|
39
|
+
updatedAt: Date;
|
|
40
|
+
expiresAt: Date; // Cart expiration
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### Key Patterns
|
|
45
|
+
|
|
46
|
+
| Pattern | Description |
|
|
47
|
+
|---------|-------------|
|
|
48
|
+
| **Guest → User Merge** | Merge localStorage cart on login |
|
|
49
|
+
| **Price Snapshot** | Store price at add time, revalidate at checkout |
|
|
50
|
+
| **Expiration** | Clear abandoned carts after N days |
|
|
51
|
+
| **Validation** | Check stock/price at checkout entry |
|
|
52
|
+
|
|
53
|
+
#### Implementation
|
|
54
|
+
```typescript
|
|
55
|
+
class CartService {
|
|
56
|
+
async addItem(cartId: string, item: AddItemRequest): Promise<Cart> {
|
|
57
|
+
// 1. Validate product exists and in stock
|
|
58
|
+
const product = await this.productService.get(item.productId);
|
|
59
|
+
if (!product || product.stock < item.quantity) {
|
|
60
|
+
throw new OutOfStockError();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Snapshot current price
|
|
64
|
+
const cartItem: CartItem = {
|
|
65
|
+
...item,
|
|
66
|
+
price: product.currentPrice,
|
|
67
|
+
originalPrice: product.originalPrice,
|
|
68
|
+
addedAt: new Date(),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// 3. Add or update quantity
|
|
72
|
+
return this.cartRepository.upsertItem(cartId, cartItem);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async mergeGuestCart(userId: string, sessionId: string): Promise<Cart> {
|
|
76
|
+
const guestCart = await this.cartRepository.findBySession(sessionId);
|
|
77
|
+
const userCart = await this.cartRepository.findByUser(userId);
|
|
78
|
+
|
|
79
|
+
if (!guestCart) return userCart;
|
|
80
|
+
|
|
81
|
+
// Merge: user cart takes priority for duplicates
|
|
82
|
+
const merged = this.mergeItems(userCart.items, guestCart.items);
|
|
83
|
+
await this.cartRepository.delete(guestCart.id);
|
|
84
|
+
|
|
85
|
+
return this.cartRepository.update(userCart.id, { items: merged });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2. Payment
|
|
91
|
+
|
|
92
|
+
#### State Machine
|
|
93
|
+
```
|
|
94
|
+
PENDING → PROCESSING → AUTHORIZED → CAPTURED → COMPLETED
|
|
95
|
+
↘ FAILED
|
|
96
|
+
↘ CANCELED
|
|
97
|
+
COMPLETED → REFUND_REQUESTED → REFUNDED (partial/full)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Idempotency Pattern (Critical)
|
|
101
|
+
```typescript
|
|
102
|
+
interface PaymentRequest {
|
|
103
|
+
orderId: string;
|
|
104
|
+
amount: number;
|
|
105
|
+
currency: string;
|
|
106
|
+
idempotencyKey: string; // REQUIRED: `order_${orderId}_${timestamp}`
|
|
107
|
+
paymentMethod: PaymentMethod;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class PaymentService {
|
|
111
|
+
async processPayment(request: PaymentRequest): Promise<PaymentResult> {
|
|
112
|
+
// 1. Check idempotency - prevent duplicate charges
|
|
113
|
+
const existing = await this.paymentRepository.findByIdempotencyKey(
|
|
114
|
+
request.idempotencyKey
|
|
115
|
+
);
|
|
116
|
+
if (existing) {
|
|
117
|
+
return existing.result; // Return cached result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. Create payment record (PENDING)
|
|
121
|
+
const payment = await this.paymentRepository.create({
|
|
122
|
+
...request,
|
|
123
|
+
status: 'PENDING',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// 3. Call PG adapter
|
|
128
|
+
const pgResult = await this.pgAdapter.authorize(request);
|
|
129
|
+
|
|
130
|
+
// 4. Update status
|
|
131
|
+
return this.paymentRepository.update(payment.id, {
|
|
132
|
+
status: pgResult.success ? 'AUTHORIZED' : 'FAILED',
|
|
133
|
+
pgTransactionId: pgResult.transactionId,
|
|
134
|
+
result: pgResult,
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
await this.paymentRepository.update(payment.id, {
|
|
138
|
+
status: 'FAILED',
|
|
139
|
+
error: error.message,
|
|
140
|
+
});
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### PG Adapter Pattern
|
|
148
|
+
```typescript
|
|
149
|
+
interface PGAdapter {
|
|
150
|
+
authorize(request: PaymentRequest): Promise<PGResult>;
|
|
151
|
+
capture(transactionId: string): Promise<PGResult>;
|
|
152
|
+
cancel(transactionId: string): Promise<PGResult>;
|
|
153
|
+
refund(transactionId: string, amount?: number): Promise<PGResult>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Implementations
|
|
157
|
+
class TossPaymentsAdapter implements PGAdapter { /* ... */ }
|
|
158
|
+
class StripeAdapter implements PGAdapter { /* ... */ }
|
|
159
|
+
class PortOneAdapter implements PGAdapter { /* ... */ }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### Webhook Handling
|
|
163
|
+
```typescript
|
|
164
|
+
class PaymentWebhookHandler {
|
|
165
|
+
async handle(event: WebhookEvent): Promise<void> {
|
|
166
|
+
// 1. Verify signature
|
|
167
|
+
if (!this.verifySignature(event)) {
|
|
168
|
+
throw new UnauthorizedError();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. Idempotency check - process each event only once
|
|
172
|
+
const processed = await this.eventStore.find(event.id);
|
|
173
|
+
if (processed) {
|
|
174
|
+
return; // Already processed
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 3. Process event
|
|
178
|
+
await this.processEvent(event);
|
|
179
|
+
|
|
180
|
+
// 4. Mark as processed
|
|
181
|
+
await this.eventStore.save({
|
|
182
|
+
eventId: event.id,
|
|
183
|
+
processedAt: new Date(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 3. Inventory (Stock Management)
|
|
190
|
+
|
|
191
|
+
#### Reservation Pattern (Two-Phase)
|
|
192
|
+
```typescript
|
|
193
|
+
interface StockReservation {
|
|
194
|
+
id: string;
|
|
195
|
+
productId: string;
|
|
196
|
+
quantity: number;
|
|
197
|
+
orderId: string;
|
|
198
|
+
status: 'RESERVED' | 'COMMITTED' | 'RELEASED';
|
|
199
|
+
expiresAt: Date; // Auto-release if not committed
|
|
200
|
+
createdAt: Date;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
class InventoryService {
|
|
204
|
+
// Phase 1: Reserve stock (at checkout start)
|
|
205
|
+
async reserve(orderId: string, items: OrderItem[]): Promise<void> {
|
|
206
|
+
for (const item of items) {
|
|
207
|
+
// Atomic decrement with check
|
|
208
|
+
const result = await this.db.query(`
|
|
209
|
+
UPDATE products
|
|
210
|
+
SET reserved_stock = reserved_stock + $1
|
|
211
|
+
WHERE id = $2
|
|
212
|
+
AND (available_stock - reserved_stock) >= $1
|
|
213
|
+
RETURNING *
|
|
214
|
+
`, [item.quantity, item.productId]);
|
|
215
|
+
|
|
216
|
+
if (result.rowCount === 0) {
|
|
217
|
+
// Rollback previous reservations
|
|
218
|
+
await this.releaseAll(orderId);
|
|
219
|
+
throw new InsufficientStockError(item.productId);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await this.reservationRepository.create({
|
|
223
|
+
orderId,
|
|
224
|
+
productId: item.productId,
|
|
225
|
+
quantity: item.quantity,
|
|
226
|
+
status: 'RESERVED',
|
|
227
|
+
expiresAt: addMinutes(new Date(), 15), // 15min hold
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Phase 2: Commit stock (after payment success)
|
|
233
|
+
async commit(orderId: string): Promise<void> {
|
|
234
|
+
const reservations = await this.reservationRepository.findByOrder(orderId);
|
|
235
|
+
|
|
236
|
+
for (const reservation of reservations) {
|
|
237
|
+
await this.db.query(`
|
|
238
|
+
UPDATE products
|
|
239
|
+
SET
|
|
240
|
+
available_stock = available_stock - $1,
|
|
241
|
+
reserved_stock = reserved_stock - $1
|
|
242
|
+
WHERE id = $2
|
|
243
|
+
`, [reservation.quantity, reservation.productId]);
|
|
244
|
+
|
|
245
|
+
await this.reservationRepository.update(reservation.id, {
|
|
246
|
+
status: 'COMMITTED',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Rollback: Release stock (payment failed or timeout)
|
|
252
|
+
async release(orderId: string): Promise<void> {
|
|
253
|
+
const reservations = await this.reservationRepository.findByOrder(orderId);
|
|
254
|
+
|
|
255
|
+
for (const reservation of reservations) {
|
|
256
|
+
if (reservation.status === 'RESERVED') {
|
|
257
|
+
await this.db.query(`
|
|
258
|
+
UPDATE products
|
|
259
|
+
SET reserved_stock = reserved_stock - $1
|
|
260
|
+
WHERE id = $2
|
|
261
|
+
`, [reservation.quantity, reservation.productId]);
|
|
262
|
+
|
|
263
|
+
await this.reservationRepository.update(reservation.id, {
|
|
264
|
+
status: 'RELEASED',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### Concurrency Control
|
|
273
|
+
```typescript
|
|
274
|
+
// Option 1: Optimistic Locking
|
|
275
|
+
await db.query(`
|
|
276
|
+
UPDATE products
|
|
277
|
+
SET stock = stock - $1, version = version + 1
|
|
278
|
+
WHERE id = $2 AND version = $3 AND stock >= $1
|
|
279
|
+
`, [quantity, productId, expectedVersion]);
|
|
280
|
+
|
|
281
|
+
// Option 2: Pessimistic Locking (for high contention)
|
|
282
|
+
await db.query(`
|
|
283
|
+
SELECT * FROM products WHERE id = $1 FOR UPDATE
|
|
284
|
+
`, [productId]);
|
|
285
|
+
|
|
286
|
+
// Option 3: Redis Distributed Lock
|
|
287
|
+
const lock = await redlock.lock(`stock:${productId}`, 5000);
|
|
288
|
+
try {
|
|
289
|
+
// Update stock
|
|
290
|
+
} finally {
|
|
291
|
+
await lock.unlock();
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Order Flow (Complete)
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
299
|
+
│ CHECKOUT FLOW │
|
|
300
|
+
├─────────────────────────────────────────────────────────────┤
|
|
301
|
+
│ │
|
|
302
|
+
│ 1. Cart Validation │
|
|
303
|
+
│ └─ Revalidate prices, check stock availability │
|
|
304
|
+
│ │
|
|
305
|
+
│ 2. Order Creation (PENDING) │
|
|
306
|
+
│ └─ Generate orderId, snapshot cart │
|
|
307
|
+
│ │
|
|
308
|
+
│ 3. Stock Reservation │
|
|
309
|
+
│ └─ Reserve inventory (15min hold) │
|
|
310
|
+
│ │
|
|
311
|
+
│ 4. Payment Processing │
|
|
312
|
+
│ ├─ Success → Order PAID, Stock COMMIT │
|
|
313
|
+
│ └─ Failure → Order FAILED, Stock RELEASE │
|
|
314
|
+
│ │
|
|
315
|
+
│ 5. Order Confirmation │
|
|
316
|
+
│ └─ Send notification, trigger fulfillment │
|
|
317
|
+
│ │
|
|
318
|
+
└─────────────────────────────────────────────────────────────┘
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Common Bugs & Prevention
|
|
322
|
+
|
|
323
|
+
| Bug | Cause | Prevention |
|
|
324
|
+
|-----|-------|------------|
|
|
325
|
+
| **Duplicate payment** | User double-click, webhook retry | Idempotency key |
|
|
326
|
+
| **Negative stock** | Race condition | Atomic update with check |
|
|
327
|
+
| **Price mismatch** | Price changed during checkout | Snapshot + revalidation |
|
|
328
|
+
| **Orphan reservation** | Payment timeout without release | TTL + scheduled cleanup |
|
|
329
|
+
| **Lost webhook** | Network failure | Retry + idempotent handler |
|
|
330
|
+
| **Order state corruption** | Concurrent updates | State machine + versioning |
|
|
331
|
+
|
|
332
|
+
## Integration with /vibe.run
|
|
333
|
+
|
|
334
|
+
During commerce feature implementation:
|
|
335
|
+
|
|
336
|
+
1. **Phase 1**: Define domain models (Cart, Order, Payment, Inventory)
|
|
337
|
+
2. **Phase 2**: Implement core services with patterns above
|
|
338
|
+
3. **Phase 3**: Add PG adapter and webhook handlers
|
|
339
|
+
4. **Phase 4**: Implement compensating transactions (Saga)
|
|
340
|
+
5. **Phase 5**: E2E test critical flows
|
|
341
|
+
|
|
342
|
+
## Checklist
|
|
343
|
+
|
|
344
|
+
### Cart
|
|
345
|
+
- [ ] Guest/user cart merge on login
|
|
346
|
+
- [ ] Price snapshot at add time
|
|
347
|
+
- [ ] Stock validation at checkout
|
|
348
|
+
- [ ] Cart expiration cleanup
|
|
349
|
+
|
|
350
|
+
### Payment
|
|
351
|
+
- [ ] Idempotency key on all requests
|
|
352
|
+
- [ ] Webhook signature verification
|
|
353
|
+
- [ ] Duplicate event handling
|
|
354
|
+
- [ ] Timeout/retry strategy
|
|
355
|
+
- [ ] Refund flow tested
|
|
356
|
+
|
|
357
|
+
### Inventory
|
|
358
|
+
- [ ] Two-phase reservation (reserve → commit/release)
|
|
359
|
+
- [ ] Atomic stock updates
|
|
360
|
+
- [ ] Reservation TTL and cleanup job
|
|
361
|
+
- [ ] Concurrency control tested
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: e2e-commerce
|
|
3
|
+
description: "E2E test scenarios for commerce checkout and payment flows"
|
|
4
|
+
triggers: [e2e commerce, checkout test, payment test, order flow test]
|
|
5
|
+
priority: 65
|
|
6
|
+
---
|
|
7
|
+
# E2E Commerce Test Scenarios
|
|
8
|
+
|
|
9
|
+
Playwright-based E2E testing for commerce checkout flows.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
- After implementing checkout/payment features
|
|
14
|
+
- Before production deployment
|
|
15
|
+
- CI/CD pipeline quality gate
|
|
16
|
+
- Regression testing after changes
|
|
17
|
+
|
|
18
|
+
## Test Scenarios
|
|
19
|
+
|
|
20
|
+
### 1. Happy Path - Complete Checkout
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// tests/e2e/checkout.spec.ts
|
|
24
|
+
import { test, expect } from '@playwright/test';
|
|
25
|
+
|
|
26
|
+
test.describe('Checkout Flow', () => {
|
|
27
|
+
test('complete purchase - happy path', async ({ page }) => {
|
|
28
|
+
// 1. Add to cart
|
|
29
|
+
await page.goto('/products/test-product');
|
|
30
|
+
await page.click('[data-testid="add-to-cart"]');
|
|
31
|
+
await expect(page.locator('[data-testid="cart-count"]')).toHaveText('1');
|
|
32
|
+
|
|
33
|
+
// 2. Go to cart
|
|
34
|
+
await page.click('[data-testid="cart-icon"]');
|
|
35
|
+
await expect(page).toHaveURL('/cart');
|
|
36
|
+
|
|
37
|
+
// 3. Proceed to checkout
|
|
38
|
+
await page.click('[data-testid="checkout-button"]');
|
|
39
|
+
await expect(page).toHaveURL('/checkout');
|
|
40
|
+
|
|
41
|
+
// 4. Fill shipping info
|
|
42
|
+
await page.fill('[name="name"]', 'Test User');
|
|
43
|
+
await page.fill('[name="phone"]', '010-1234-5678');
|
|
44
|
+
await page.fill('[name="address"]', 'Test Address 123');
|
|
45
|
+
|
|
46
|
+
// 5. Select payment method
|
|
47
|
+
await page.click('[data-testid="payment-card"]');
|
|
48
|
+
|
|
49
|
+
// 6. Complete payment (sandbox/mock)
|
|
50
|
+
await page.click('[data-testid="pay-button"]');
|
|
51
|
+
|
|
52
|
+
// 7. Verify order complete
|
|
53
|
+
await expect(page).toHaveURL(/\/orders\/\w+/);
|
|
54
|
+
await expect(page.locator('[data-testid="order-status"]')).toHaveText('결제 완료');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Stock Validation
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
test('prevent checkout when out of stock', async ({ page }) => {
|
|
63
|
+
// Setup: Product with stock = 1, another user reserves it
|
|
64
|
+
await page.goto('/products/low-stock-product');
|
|
65
|
+
await page.click('[data-testid="add-to-cart"]');
|
|
66
|
+
await page.goto('/checkout');
|
|
67
|
+
|
|
68
|
+
// Simulate stock depletion during checkout
|
|
69
|
+
await page.evaluate(async () => {
|
|
70
|
+
await fetch('/api/test/deplete-stock', { method: 'POST' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Attempt payment
|
|
74
|
+
await page.click('[data-testid="pay-button"]');
|
|
75
|
+
|
|
76
|
+
// Should show out of stock error
|
|
77
|
+
await expect(page.locator('[data-testid="error-message"]'))
|
|
78
|
+
.toContainText('재고가 부족합니다');
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Payment Failure Handling
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
test('handle payment failure gracefully', async ({ page }) => {
|
|
86
|
+
await page.goto('/products/test-product');
|
|
87
|
+
await page.click('[data-testid="add-to-cart"]');
|
|
88
|
+
await page.goto('/checkout');
|
|
89
|
+
|
|
90
|
+
// Fill form
|
|
91
|
+
await page.fill('[name="name"]', 'Test User');
|
|
92
|
+
await page.fill('[name="address"]', 'Test Address');
|
|
93
|
+
|
|
94
|
+
// Use test card that triggers failure
|
|
95
|
+
await page.fill('[name="card-number"]', '4000000000000002'); // Decline card
|
|
96
|
+
await page.click('[data-testid="pay-button"]');
|
|
97
|
+
|
|
98
|
+
// Verify error handling
|
|
99
|
+
await expect(page.locator('[data-testid="payment-error"]'))
|
|
100
|
+
.toContainText('결제가 거절되었습니다');
|
|
101
|
+
|
|
102
|
+
// Stock should be released
|
|
103
|
+
await page.goto('/products/test-product');
|
|
104
|
+
await expect(page.locator('[data-testid="stock-status"]'))
|
|
105
|
+
.not.toContainText('품절');
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 4. Duplicate Payment Prevention
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
test('prevent duplicate payment on double click', async ({ page }) => {
|
|
113
|
+
await page.goto('/checkout');
|
|
114
|
+
// Fill checkout form...
|
|
115
|
+
|
|
116
|
+
// Double click pay button rapidly
|
|
117
|
+
const payButton = page.locator('[data-testid="pay-button"]');
|
|
118
|
+
await Promise.all([
|
|
119
|
+
payButton.click(),
|
|
120
|
+
payButton.click(),
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
// Wait for completion
|
|
124
|
+
await page.waitForURL(/\/orders\/\w+/);
|
|
125
|
+
|
|
126
|
+
// Verify only one order created
|
|
127
|
+
const orderId = page.url().split('/').pop();
|
|
128
|
+
const response = await page.request.get(`/api/orders?userId=test`);
|
|
129
|
+
const orders = await response.json();
|
|
130
|
+
|
|
131
|
+
expect(orders.filter(o => o.id === orderId)).toHaveLength(1);
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 5. Coupon Application
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
test('apply coupon and verify discount', async ({ page }) => {
|
|
139
|
+
await page.goto('/products/test-product'); // Price: 10,000
|
|
140
|
+
await page.click('[data-testid="add-to-cart"]');
|
|
141
|
+
await page.goto('/checkout');
|
|
142
|
+
|
|
143
|
+
// Original price
|
|
144
|
+
await expect(page.locator('[data-testid="total-price"]'))
|
|
145
|
+
.toHaveText('10,000원');
|
|
146
|
+
|
|
147
|
+
// Apply 10% coupon
|
|
148
|
+
await page.fill('[name="coupon"]', 'DISCOUNT10');
|
|
149
|
+
await page.click('[data-testid="apply-coupon"]');
|
|
150
|
+
|
|
151
|
+
// Verify discount applied
|
|
152
|
+
await expect(page.locator('[data-testid="discount-amount"]'))
|
|
153
|
+
.toHaveText('-1,000원');
|
|
154
|
+
await expect(page.locator('[data-testid="total-price"]'))
|
|
155
|
+
.toHaveText('9,000원');
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 6. Webhook Resilience
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
test('order completes even with webhook delay', async ({ page, request }) => {
|
|
163
|
+
// Configure webhook delay in test environment
|
|
164
|
+
await request.post('/api/test/configure-webhook', {
|
|
165
|
+
data: { delay: 5000 } // 5 second delay
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Complete checkout
|
|
169
|
+
await page.goto('/checkout');
|
|
170
|
+
// ... fill form
|
|
171
|
+
await page.click('[data-testid="pay-button"]');
|
|
172
|
+
|
|
173
|
+
// Should show processing state
|
|
174
|
+
await expect(page.locator('[data-testid="order-status"]'))
|
|
175
|
+
.toHaveText('처리 중');
|
|
176
|
+
|
|
177
|
+
// Wait for webhook
|
|
178
|
+
await page.waitForSelector('[data-testid="order-status"]:has-text("결제 완료")', {
|
|
179
|
+
timeout: 10000
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## CLI Usage
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Run all commerce e2e tests
|
|
188
|
+
/vibe.utils --e2e commerce
|
|
189
|
+
|
|
190
|
+
# Run specific scenario
|
|
191
|
+
/vibe.utils --e2e checkout-flow
|
|
192
|
+
|
|
193
|
+
# Run with visual recording
|
|
194
|
+
/vibe.utils --e2e commerce --record
|
|
195
|
+
|
|
196
|
+
# Run against staging
|
|
197
|
+
/vibe.utils --e2e commerce --env staging
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Test Environment Setup
|
|
201
|
+
|
|
202
|
+
### Mock PG Server
|
|
203
|
+
```typescript
|
|
204
|
+
// tests/mocks/pg-server.ts
|
|
205
|
+
import { setupServer } from 'msw/node';
|
|
206
|
+
import { http, HttpResponse } from 'msw';
|
|
207
|
+
|
|
208
|
+
export const pgMockServer = setupServer(
|
|
209
|
+
// Success response
|
|
210
|
+
http.post('/payments/authorize', () => {
|
|
211
|
+
return HttpResponse.json({
|
|
212
|
+
success: true,
|
|
213
|
+
transactionId: `txn_${Date.now()}`,
|
|
214
|
+
status: 'AUTHORIZED',
|
|
215
|
+
});
|
|
216
|
+
}),
|
|
217
|
+
|
|
218
|
+
// Failure simulation
|
|
219
|
+
http.post('/payments/authorize', ({ request }) => {
|
|
220
|
+
const body = request.json();
|
|
221
|
+
if (body.cardNumber === '4000000000000002') {
|
|
222
|
+
return HttpResponse.json({
|
|
223
|
+
success: false,
|
|
224
|
+
error: 'CARD_DECLINED',
|
|
225
|
+
}, { status: 400 });
|
|
226
|
+
}
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Database Seeding
|
|
232
|
+
```typescript
|
|
233
|
+
// tests/fixtures/commerce.ts
|
|
234
|
+
export async function seedCommerceData(db: Database) {
|
|
235
|
+
// Create test products
|
|
236
|
+
await db.products.createMany([
|
|
237
|
+
{ id: 'test-product', name: 'Test Product', price: 10000, stock: 100 },
|
|
238
|
+
{ id: 'low-stock', name: 'Low Stock', price: 5000, stock: 1 },
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
// Create test coupons
|
|
242
|
+
await db.coupons.create({
|
|
243
|
+
code: 'DISCOUNT10',
|
|
244
|
+
discountPercent: 10,
|
|
245
|
+
expiresAt: addDays(new Date(), 30),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## CI/CD Integration
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
# .github/workflows/e2e.yml
|
|
254
|
+
name: E2E Commerce Tests
|
|
255
|
+
|
|
256
|
+
on:
|
|
257
|
+
pull_request:
|
|
258
|
+
paths:
|
|
259
|
+
- 'src/checkout/**'
|
|
260
|
+
- 'src/payment/**'
|
|
261
|
+
- 'src/cart/**'
|
|
262
|
+
|
|
263
|
+
jobs:
|
|
264
|
+
e2e:
|
|
265
|
+
runs-on: ubuntu-latest
|
|
266
|
+
steps:
|
|
267
|
+
- uses: actions/checkout@v4
|
|
268
|
+
|
|
269
|
+
- name: Install dependencies
|
|
270
|
+
run: npm ci
|
|
271
|
+
|
|
272
|
+
- name: Start test server
|
|
273
|
+
run: npm run start:test &
|
|
274
|
+
|
|
275
|
+
- name: Run E2E tests
|
|
276
|
+
run: npx playwright test tests/e2e/commerce/
|
|
277
|
+
|
|
278
|
+
- name: Upload test results
|
|
279
|
+
if: failure()
|
|
280
|
+
uses: actions/upload-artifact@v4
|
|
281
|
+
with:
|
|
282
|
+
name: playwright-report
|
|
283
|
+
path: playwright-report/
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Quality Checklist
|
|
287
|
+
|
|
288
|
+
### Must Pass (P0)
|
|
289
|
+
- [ ] Happy path checkout completes
|
|
290
|
+
- [ ] Payment failure releases stock
|
|
291
|
+
- [ ] Duplicate payment prevented
|
|
292
|
+
- [ ] Out of stock blocks checkout
|
|
293
|
+
|
|
294
|
+
### Should Pass (P1)
|
|
295
|
+
- [ ] Coupon calculation correct
|
|
296
|
+
- [ ] Webhook retry handled
|
|
297
|
+
- [ ] Cart merge on login works
|
|
298
|
+
- [ ] Partial refund processed
|
|
299
|
+
|
|
300
|
+
### Nice to Have (P2)
|
|
301
|
+
- [ ] Multiple payment methods
|
|
302
|
+
- [ ] Guest checkout flow
|
|
303
|
+
- [ ] Order cancellation
|
|
304
|
+
- [ ] Subscription renewal
|