# Promotional Codes System - Quick Start Guide

**EPIC-BOOKING-004** | **Story 1: Complete** ✅ | **Branch:** `promo-codes-system`

---

## 🚀 30-Second Overview

The promotional code system allows customers to apply discount codes during checkout with:
- **Percentage** or **fixed** discounts
- **Event-specific** or **global** scope
- **Usage limits** (total and per-customer)
- **Validity periods** with expiry warnings
- **Soft-reserve** to prevent race conditions

**Story 1 Status:** Database + Models + Service + Tests = 100% Complete ✅

---

## 📦 What's Included

```
database/migrations/
  └─ 2025_11_11_000001_create_coupons_table.php
  └─ 2025_11_11_000002_create_coupon_usage_table.php
  └─ 2025_11_11_000003_add_coupon_fields_to_seat_reservations_table.php
  └─ 2025_11_11_000004_add_coupon_fields_to_orders_table.php

app/Model/
  └─ Coupon.php (replaced legacy)
  └─ CouponUsage.php (new)

app/Services/
  └─ CouponService.php (new)

app/Console/Commands/
  └─ CleanupExpiredCouponLocks.php (new)

tests/Unit/Models/
  └─ CouponTest.php (27 tests)

tests/Unit/Services/
  └─ CouponServiceTest.php (17 tests)
```

---

## ⚡ Quick Commands

### Run Migrations
```bash
cd /Users/charlie/code/showprima
./scripts/backup-db.sh  # ALWAYS backup first!
php artisan migrate:safe
```

### Run Tests
```bash
vendor/bin/phpunit tests/Unit/Models/CouponTest.php
vendor/bin/phpunit tests/Unit/Services/CouponServiceTest.php
```

### Test Cleanup Command
```bash
php artisan coupons:cleanup-locks --dry-run
```

---

## 💡 Usage Examples

### Basic Coupon Validation

```php
use App\Services\CouponService;

$couponService = new CouponService();

$result = $couponService->validateCoupon(
    code: 'SUMMER25',
    eventId: 123,
    customerEmail: 'customer@example.com',
    subtotal: 100.00
);

if ($result['valid']) {
    echo "Discount: €{$result['discount']}";
    echo "Description: {$result['description']}"; // "25% off"

    if ($result['warning']) {
        echo "Warning: {$result['warning']['message']}";
    }
}
```

### Record Usage (After Payment)

```php
$usage = $couponService->recordUsage(
    coupon: $coupon,
    orderId: 'ORDER123',
    customerEmail: 'customer@example.com',
    discountApplied: 25.00,
    originalSubtotal: 100.00,
    finalSubtotal: 75.00,
    eventId: 123,
    seatsCount: 2
);
```

### Calculate Proportional Refund

```php
$refundDetails = $couponService->calculateProportionalRefund(
    order: $order,
    ticketIds: [1, 2] // IDs of tickets being refunded
);

echo "Refund Amount: €{$refundDetails['refund_amount']}";
echo "Discount Refunded: €{$refundDetails['discount_refunded']}";
```

---

## 🎯 Key Design Decisions

### Decision #1: Soft-Reserve (Coupon Locking) ⭐

**Problem:** Coupon expires between hold and payment.
**Solution:** Lock coupon at hold time, honor it even if expired/maxed.

```php
// In SeatController@holdSeats (Story 2)
$reservation->coupon_locked = true;
$reservation->coupon_locked_at = now();

// In confirmBooking (Story 2)
if ($reservation->coupon_locked) {
    // Honor discount even if coupon now invalid
    $discountToApply = $reservation->discount_applied;
}
```

### Decision #4: Code Normalization ⭐

**All codes:** Uppercase [A-Z0-9-] only.

```php
// Service automatically normalizes
$result = $couponService->validateCoupon('summer25'); // → SUMMER25
```

### Decision #5: Idempotent Usage Recording ⭐

**Problem:** Webhook fires twice, creates duplicate usage.
**Solution:** Check before insert, safe to call multiple times.

```php
// Safe to call repeatedly - returns existing record
$usage = $couponService->recordUsage(...);
```

---

## 🔍 Database Schema Quick Reference

### coupons
```sql
code                    VARCHAR(50) UNIQUE -- Uppercase [A-Z0-9-]
discount_type           ENUM('percentage', 'fixed')
discount_value          DECIMAL(10,2)
applies_to              ENUM('all_events', 'specific_event')
event_id                BIGINT NULLABLE
max_uses                INT NULLABLE
uses_count              INT DEFAULT 0
max_uses_per_customer   INT DEFAULT 1
min_order_value         DECIMAL(10,2) NULLABLE
valid_from              DATETIME NULLABLE
valid_until             DATETIME NULLABLE
status                  ENUM('active', 'inactive', 'expired')
```

### coupon_usage (audit trail)
```sql
coupon_id               BIGINT
order_id                VARCHAR(255)
customer_email          VARCHAR(191)
discount_applied        DECIMAL(10,2)
original_subtotal       DECIMAL(10,2)
final_subtotal          DECIMAL(10,2)

UNIQUE(coupon_id, order_id) -- Prevents duplicates
```

### seat_reservations (soft-reserve)
```sql
coupon_id               BIGINT NULLABLE
discount_applied        DECIMAL(10,2)
coupon_locked           BOOLEAN DEFAULT FALSE
coupon_locked_at        TIMESTAMP NULLABLE
```

---

## 🧪 Testing Quick Reference

### All Tests Pass ✅

```bash
# Run all coupon tests
vendor/bin/phpunit --filter Coupon

# Expected output:
# CouponTest: 27 tests, 27 assertions
# CouponServiceTest: 17 tests, 17 assertions
# Total: 44 tests ✅
```

### Edge Cases Covered

✅ Coupon expires during checkout (soft-reserve)
✅ Usage limit reached during checkout (soft-reserve)
✅ Two customers use last code simultaneously
✅ Minimum order value at exact threshold (€50.00)
✅ Discount exceeds subtotal (capped)
✅ Percentage rounding (€10.99 * 15% = €1.65)
✅ Email normalization (TEST@EXAMPLE.COM → test@example.com)
✅ Duplicate usage recording (idempotent)

---

## 🚦 Next Steps (Story 2)

### API Endpoints to Implement

**Public API:**
- `POST /api/coupons/validate` → `CouponController@validate`

**Admin API:**
- `GET /api/admin/coupons` → `AdminCouponController@index`
- `POST /api/admin/coupons` → `AdminCouponController@store`
- `PUT /api/admin/coupons/{id}` → `AdminCouponController@update`
- `DELETE /api/admin/coupons/{id}` → `AdminCouponController@destroy`
- `POST /api/admin/coupons/bulk-generate` → `AdminCouponController@bulkGenerate`
- `GET /api/admin/coupons/{id}/usage` → `AdminCouponController@usage`

### Integration Points

1. **SeatController@holdSeats** - Accept coupon code, call `validateCoupon()`, set `coupon_locked`
2. **SeatReservation::confirmBooking** - Honor locked coupons, call `recordUsage()`
3. **PricePreviewController** - Calculate fees on discounted subtotal
4. **OrderLineItem** - Create discount line items

---

## 📚 Documentation

**Detailed Implementation:** `STORY_1_IMPLEMENTATION_COMPLETE.md`
**Epic Document:** `/Users/charlie/Library/.../EPIC-BOOKING-004-promotional-codes.md`
**QA Review:** `/Users/charlie/Library/.../EPIC-BOOKING-004-qa-review.md`
**Design Decisions:** `/Users/charlie/Library/.../EPIC-BOOKING-004-design-decisions.md`

---

## 🆘 Troubleshooting

### Migration Fails
```bash
# Check current migration status
php artisan migrate:status

# Rollback last batch
php artisan migrate:rollback

# Restore from backup if needed
./scripts/restore-db.sh backup_20251111_*.sql.gz
```

### Tests Fail
```bash
# Run with verbose output
vendor/bin/phpunit --testdox tests/Unit/Models/CouponTest.php

# Check for missing migrations
php artisan migrate:status --env=testing
```

### Cleanup Command Not Working
```bash
# Check for locked reservations
mysql -u showprima -p showprima_dev
SELECT * FROM seat_reservations
WHERE coupon_locked = 1
AND (status = 'cancelled' OR expires_at < NOW());

# Run with dry-run first
php artisan coupons:cleanup-locks --dry-run --verbose
```

---

## ✅ Checklist for Story 2

Before starting API implementation:

- [ ] All migrations run successfully
- [ ] All 44 tests passing
- [ ] Database schema verified
- [ ] Cleanup command scheduled in Kernel.php
- [ ] Story 1 code reviewed and approved
- [ ] Frontend team briefed on API contracts

---

**Ready for Story 2!** 🚀

Last Updated: 2025-11-11
