diff --git a/TEST_GUIDE.md b/TEST_GUIDE.md new file mode 100644 index 0000000..023cfa6 --- /dev/null +++ b/TEST_GUIDE.md @@ -0,0 +1,197 @@ +# Test Guide - Digital Access and Serving System + +## Quick Start + +1. **Start the system** (if not already running): +```bash +# Terminal 1: Database +docker-compose up -d + +# Terminal 2: Backend (will auto-seed database) +dotnet run --project src/Hospitality.Backend + +# Terminal 3: Frontend +cd src/hospitality-web +npm run dev +``` + +2. **Access the application**: http://localhost:5173 + +## Sample Data Overview + +The database is pre-loaded with: + +### Events +1. **Summer Festival 2025** (July 1-3, Oslo) +2. **Winter Gala 2025** (December 15, Bergen) + +### Groups (Summer Festival) +1. **VIP Sponsors** - 3 people +2. **Event Staff** - 2 people +3. **General Admission** - 2 people + +### Products (Summer Festival) +- Beer (Drink) +- Wine (Drink) +- Lunch (Meal) +- Dinner (Meal) +- VIP Lounge Access (Access) + +### Test Guests with QR Codes + +**Note**: QR codes are GUIDs that change each time you seed. To get the actual QR codes: +1. Go to Admin → Summer Festival 2025 → VIP Sponsors +2. Click on a person to see their QR code + +## Test Scenarios + +### Scenario 1: Admin - View Existing Data + +1. Go to http://localhost:5173 +2. You should see 2 events: "Summer Festival 2025" and "Winter Gala 2025" +3. Click on "Summer Festival 2025" +4. You should see: + - 3 groups (VIP Sponsors, Event Staff, General Admission) + - 5 products (Beer, Wine, Lunch, Dinner, VIP Lounge Access) + +### Scenario 2: Admin - View Group Details + +1. From Event Detail page, click on "VIP Sponsors" +2. You should see 3 people: + - **John Doe** (john.doe@example.com) + - Beer: 8/10 remaining + - Wine: 4/5 remaining + - Lunch: 2/3 remaining + - Dinner: 3/3 remaining + - VIP Access: 1/1 remaining + + - **Jane Smith** (jane.smith@example.com) + - All quotas unused (full amounts) + + - **Bob Wilson** (bob.wilson@example.com) + - Beer: 9/12 remaining + - Wine: 2/4 remaining + - Lunch: 1/3 remaining + - Dinner: 2/3 remaining + - VIP Access: 0/1 (used) + +### Scenario 3: Staff Scanner - Lookup Guest + +1. Go to http://localhost:5173/staff +2. From Admin, get John Doe's QR code: + - Go to Summer Festival → VIP Sponsors → John Doe + - Copy the QR code (GUID under "QR: ...") +3. Paste the QR code in Staff Scanner +4. Click "Lookup Person" +5. You should see: + - Name: John Doe + - Email: john.doe@example.com + - All quotas with remaining amounts +6. Click "Use 1" on Beer +7. Verify quota decreases from 8 to 7 + +### Scenario 4: Guest View - Check Quotas + +1. Go to http://localhost:5173/guest +2. Enter John Doe's QR code (same as above) +3. Click "Show My QR Code" +4. You should see: + - Large QR code for scanning + - All quotas with progress bars + - Updated Beer quota (7/10 if you used 1 in Scenario 3) + +### Scenario 5: Admin - Add New Person + +1. Go to Summer Festival → VIP Sponsors +2. Click "Add Person" +3. Fill in: + - Name: "Test User" + - Email: "test@example.com" + - Phone: "+47 999 88 777" +4. Click "Add Person" +5. Person appears in the list with a new QR code +6. Click "Assign Quota" on the new person +7. Select "Beer" and amount "5" +8. Click "Assign Quota" +9. Verify quota appears under the person + +### Scenario 6: Test Different User Types + +**VIP Sponsor (High Quotas)** +- Use Jane Smith's QR code +- Has full quotas: Beer (8), Wine (6), Lunch (3), Dinner (3), VIP Access (1) + +**Staff Member (Limited Quotas)** +- Use Alice Staff's QR code +- Has: Beer (3/4), Lunch (1/2) + +**General Guest (Basic Quotas)** +- Use Emily Guest's QR code +- Has: Beer (2/3), Lunch (1/1) + +**Heavy User (Mostly Used)** +- Use Bob Wilson's QR code +- Has used most quotas, VIP Access is depleted + +## Testing Checklist + +- [ ] View events list +- [ ] View event details with groups and products +- [ ] View group details with people +- [ ] View person's QR code and quotas +- [ ] Add new person to group +- [ ] Assign quota to person +- [ ] Use Staff Scanner to lookup person +- [ ] Record transaction (Use 1 or Use 2) +- [ ] Verify quota decreases after transaction +- [ ] Use Guest View to see QR code +- [ ] Verify progress bars show correct status +- [ ] Test with different user types (VIP, Staff, Guest) +- [ ] Try to use more than remaining quota (should fail) + +## Expected Behaviors + +### Success Cases +- ✅ Quotas display correctly with remaining amounts +- ✅ Transactions record successfully when quota available +- ✅ Quota decrements after transaction +- ✅ Progress bars update in real-time +- ✅ QR codes are unique per person + +### Error Cases +- ❌ Cannot use more than remaining quota +- ❌ Invalid QR code shows "Not Found" +- ❌ Empty fields in forms show validation errors + +## Quick Reference - Sample QR Codes + +**To get actual QR codes:** +1. Go to Admin → Summer Festival 2025 +2. Click on a group (e.g., VIP Sponsors) +3. Each person's card shows their QR code (starts with "QR: ...") +4. Copy the full GUID for testing + +**Sample People:** +- John Doe - Has some usage, good for testing transactions +- Jane Smith - Fresh quotas, good for first-time testing +- Bob Wilson - Heavy usage, good for testing limits +- Alice Staff - Staff member with limited quotas +- Emily Guest - Basic guest with minimal quotas + +## Troubleshooting + +**No data showing?** +- Restart backend: `dotnet run --project src/Hospitality.Backend` +- Database will auto-seed on startup + +**QR code not working?** +- Make sure you copied the full GUID +- QR codes are case-sensitive +- Get fresh QR code from Admin interface + +**Transaction fails?** +- Check remaining quota is > 0 +- Verify you're using correct QR code +- Check browser console for errors + +Enjoy testing! 🎉 diff --git a/src/Hospitality.Backend/Data/DbSeeder.cs b/src/Hospitality.Backend/Data/DbSeeder.cs new file mode 100644 index 0000000..a6ab5c1 --- /dev/null +++ b/src/Hospitality.Backend/Data/DbSeeder.cs @@ -0,0 +1,364 @@ +using Hospitality.Domain.Entities; +using Hospitality.Domain.Enums; +using Hospitality.Infrastructure.Data; + +namespace Hospitality.Backend.Data; + +public static class DbSeeder +{ + public static async Task SeedAsync(HospitalityDbContext context) + { + // Check if already seeded + if (context.Events.Any()) + { + return; + } + + // Create Events + var summerFestival = new Event + { + Id = Guid.NewGuid(), + Name = "Summer Festival 2025", + StartDate = DateTime.SpecifyKind(new DateTime(2025, 7, 1, 10, 0, 0), DateTimeKind.Utc), + EndDate = DateTime.SpecifyKind(new DateTime(2025, 7, 3, 22, 0, 0), DateTimeKind.Utc), + Location = "Oslo" + }; + + var winterGala = new Event + { + Id = Guid.NewGuid(), + Name = "Winter Gala 2025", + StartDate = DateTime.SpecifyKind(new DateTime(2025, 12, 15, 18, 0, 0), DateTimeKind.Utc), + EndDate = DateTime.SpecifyKind(new DateTime(2025, 12, 15, 23, 0, 0), DateTimeKind.Utc), + Location = "Bergen" + }; + + context.Events.AddRange(summerFestival, winterGala); + + // Create Products for Summer Festival + var beer = new Product + { + Id = Guid.NewGuid(), + EventId = summerFestival.Id, + Name = "Beer", + Type = ProductType.Drink + }; + + var wine = new Product + { + Id = Guid.NewGuid(), + EventId = summerFestival.Id, + Name = "Wine", + Type = ProductType.Drink + }; + + var lunch = new Product + { + Id = Guid.NewGuid(), + EventId = summerFestival.Id, + Name = "Lunch", + Type = ProductType.Meal + }; + + var dinner = new Product + { + Id = Guid.NewGuid(), + EventId = summerFestival.Id, + Name = "Dinner", + Type = ProductType.Meal + }; + + var vipAccess = new Product + { + Id = Guid.NewGuid(), + EventId = summerFestival.Id, + Name = "VIP Lounge Access", + Type = ProductType.Access + }; + + context.Products.AddRange(beer, wine, lunch, dinner, vipAccess); + + // Create Products for Winter Gala + var champagne = new Product + { + Id = Guid.NewGuid(), + EventId = winterGala.Id, + Name = "Champagne", + Type = ProductType.Drink + }; + + var galaDinner = new Product + { + Id = Guid.NewGuid(), + EventId = winterGala.Id, + Name = "Gala Dinner", + Type = ProductType.Meal + }; + + context.Products.AddRange(champagne, galaDinner); + + // Create Groups for Summer Festival + var vipSponsors = new Group + { + Id = Guid.NewGuid(), + EventId = summerFestival.Id, + Name = "VIP Sponsors", + ContactPersonName = "Sarah Johnson", + ContactEmail = "sarah@sponsors.com" + }; + + var staff = new Group + { + Id = Guid.NewGuid(), + EventId = summerFestival.Id, + Name = "Event Staff", + ContactPersonName = "Mike Anderson", + ContactEmail = "mike@eventstaff.com" + }; + + var generalAdmission = new Group + { + Id = Guid.NewGuid(), + EventId = summerFestival.Id, + Name = "General Admission", + ContactPersonName = "Lisa Chen", + ContactEmail = "lisa@tickets.com" + }; + + context.Groups.AddRange(vipSponsors, staff, generalAdmission); + + // Create People for VIP Sponsors + var johnDoe = new Person + { + Id = Guid.NewGuid(), + GroupId = vipSponsors.Id, + QrCode = Guid.NewGuid(), + Name = "John Doe", + Email = "john.doe@example.com", + PhoneNumber = "+47 123 45 678" + }; + + var janeSmith = new Person + { + Id = Guid.NewGuid(), + GroupId = vipSponsors.Id, + QrCode = Guid.NewGuid(), + Name = "Jane Smith", + Email = "jane.smith@example.com", + PhoneNumber = "+47 234 56 789" + }; + + var bobWilson = new Person + { + Id = Guid.NewGuid(), + GroupId = vipSponsors.Id, + QrCode = Guid.NewGuid(), + Name = "Bob Wilson", + Email = "bob.wilson@example.com", + PhoneNumber = "+47 345 67 890" + }; + + // Create People for Staff + var aliceStaff = new Person + { + Id = Guid.NewGuid(), + GroupId = staff.Id, + QrCode = Guid.NewGuid(), + Name = "Alice Staff", + Email = "alice@staff.com", + PhoneNumber = "+47 456 78 901" + }; + + var charlieStaff = new Person + { + Id = Guid.NewGuid(), + GroupId = staff.Id, + QrCode = Guid.NewGuid(), + Name = "Charlie Staff", + Email = "charlie@staff.com", + PhoneNumber = "+47 567 89 012" + }; + + // Create People for General Admission + var emilyGuest = new Person + { + Id = Guid.NewGuid(), + GroupId = generalAdmission.Id, + QrCode = Guid.NewGuid(), + Name = "Emily Guest", + Email = "emily@guest.com", + PhoneNumber = "+47 678 90 123" + }; + + var davidGuest = new Person + { + Id = Guid.NewGuid(), + GroupId = generalAdmission.Id, + QrCode = Guid.NewGuid(), + Name = "David Guest", + Email = "david@guest.com", + PhoneNumber = "+47 789 01 234" + }; + + context.People.AddRange(johnDoe, janeSmith, bobWilson, aliceStaff, charlieStaff, emilyGuest, davidGuest); + + // Assign Quotas to VIP Sponsors + var johnQuotas = new[] + { + new PersonQuota { Id = Guid.NewGuid(), PersonId = johnDoe.Id, ProductId = beer.Id, InitialAmount = 10, UsedAmount = 2 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = johnDoe.Id, ProductId = wine.Id, InitialAmount = 5, UsedAmount = 1 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = johnDoe.Id, ProductId = lunch.Id, InitialAmount = 3, UsedAmount = 1 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = johnDoe.Id, ProductId = dinner.Id, InitialAmount = 3, UsedAmount = 0 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = johnDoe.Id, ProductId = vipAccess.Id, InitialAmount = 1, UsedAmount = 0 } + }; + + var janeQuotas = new[] + { + new PersonQuota { Id = Guid.NewGuid(), PersonId = janeSmith.Id, ProductId = beer.Id, InitialAmount = 8, UsedAmount = 0 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = janeSmith.Id, ProductId = wine.Id, InitialAmount = 6, UsedAmount = 0 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = janeSmith.Id, ProductId = lunch.Id, InitialAmount = 3, UsedAmount = 0 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = janeSmith.Id, ProductId = dinner.Id, InitialAmount = 3, UsedAmount = 0 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = janeSmith.Id, ProductId = vipAccess.Id, InitialAmount = 1, UsedAmount = 0 } + }; + + var bobQuotas = new[] + { + new PersonQuota { Id = Guid.NewGuid(), PersonId = bobWilson.Id, ProductId = beer.Id, InitialAmount = 12, UsedAmount = 3 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = bobWilson.Id, ProductId = wine.Id, InitialAmount = 4, UsedAmount = 2 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = bobWilson.Id, ProductId = lunch.Id, InitialAmount = 3, UsedAmount = 2 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = bobWilson.Id, ProductId = dinner.Id, InitialAmount = 3, UsedAmount = 1 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = bobWilson.Id, ProductId = vipAccess.Id, InitialAmount = 1, UsedAmount = 1 } + }; + + // Assign Quotas to Staff + var aliceQuotas = new[] + { + new PersonQuota { Id = Guid.NewGuid(), PersonId = aliceStaff.Id, ProductId = beer.Id, InitialAmount = 4, UsedAmount = 1 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = aliceStaff.Id, ProductId = lunch.Id, InitialAmount = 2, UsedAmount = 1 } + }; + + var charlieQuotas = new[] + { + new PersonQuota { Id = Guid.NewGuid(), PersonId = charlieStaff.Id, ProductId = beer.Id, InitialAmount = 4, UsedAmount = 0 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = charlieStaff.Id, ProductId = lunch.Id, InitialAmount = 2, UsedAmount = 0 } + }; + + // Assign Quotas to General Admission + var emilyQuotas = new[] + { + new PersonQuota { Id = Guid.NewGuid(), PersonId = emilyGuest.Id, ProductId = beer.Id, InitialAmount = 3, UsedAmount = 1 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = emilyGuest.Id, ProductId = lunch.Id, InitialAmount = 1, UsedAmount = 0 } + }; + + var davidQuotas = new[] + { + new PersonQuota { Id = Guid.NewGuid(), PersonId = davidGuest.Id, ProductId = beer.Id, InitialAmount = 3, UsedAmount = 0 }, + new PersonQuota { Id = Guid.NewGuid(), PersonId = davidGuest.Id, ProductId = lunch.Id, InitialAmount = 1, UsedAmount = 0 } + }; + + context.PersonQuotas.AddRange(johnQuotas); + context.PersonQuotas.AddRange(janeQuotas); + context.PersonQuotas.AddRange(bobQuotas); + context.PersonQuotas.AddRange(aliceQuotas); + context.PersonQuotas.AddRange(charlieQuotas); + context.PersonQuotas.AddRange(emilyQuotas); + context.PersonQuotas.AddRange(davidQuotas); + + // Create some sample transactions + var transactions = new[] + { + new Transaction + { + Id = Guid.NewGuid(), + PersonId = johnDoe.Id, + ProductId = beer.Id, + Amount = 2, + Timestamp = DateTime.UtcNow.AddHours(-5) + }, + new Transaction + { + Id = Guid.NewGuid(), + PersonId = johnDoe.Id, + ProductId = wine.Id, + Amount = 1, + Timestamp = DateTime.UtcNow.AddHours(-3) + }, + new Transaction + { + Id = Guid.NewGuid(), + PersonId = johnDoe.Id, + ProductId = lunch.Id, + Amount = 1, + Timestamp = DateTime.UtcNow.AddHours(-2) + }, + new Transaction + { + Id = Guid.NewGuid(), + PersonId = bobWilson.Id, + ProductId = beer.Id, + Amount = 3, + Timestamp = DateTime.UtcNow.AddHours(-4) + }, + new Transaction + { + Id = Guid.NewGuid(), + PersonId = bobWilson.Id, + ProductId = wine.Id, + Amount = 2, + Timestamp = DateTime.UtcNow.AddHours(-3) + }, + new Transaction + { + Id = Guid.NewGuid(), + PersonId = bobWilson.Id, + ProductId = lunch.Id, + Amount = 2, + Timestamp = DateTime.UtcNow.AddHours(-2) + }, + new Transaction + { + Id = Guid.NewGuid(), + PersonId = bobWilson.Id, + ProductId = dinner.Id, + Amount = 1, + Timestamp = DateTime.UtcNow.AddHours(-1) + }, + new Transaction + { + Id = Guid.NewGuid(), + PersonId = bobWilson.Id, + ProductId = vipAccess.Id, + Amount = 1, + Timestamp = DateTime.UtcNow.AddHours(-6) + }, + new Transaction + { + Id = Guid.NewGuid(), + PersonId = aliceStaff.Id, + ProductId = beer.Id, + Amount = 1, + Timestamp = DateTime.UtcNow.AddHours(-1) + }, + new Transaction + { + Id = Guid.NewGuid(), + PersonId = aliceStaff.Id, + ProductId = lunch.Id, + Amount = 1, + Timestamp = DateTime.UtcNow.AddHours(-2) + }, + new Transaction + { + Id = Guid.NewGuid(), + PersonId = emilyGuest.Id, + ProductId = beer.Id, + Amount = 1, + Timestamp = DateTime.UtcNow.AddMinutes(-30) + } + }; + + context.Transactions.AddRange(transactions); + + await context.SaveChangesAsync(); + } +} diff --git a/src/Hospitality.Backend/Program.cs b/src/Hospitality.Backend/Program.cs index ea1f9ca..699111b 100644 --- a/src/Hospitality.Backend/Program.cs +++ b/src/Hospitality.Backend/Program.cs @@ -22,6 +22,14 @@ builder.Services.AddScoped(); var app = builder.Build(); +// Seed database with sample data +using (var scope = app.Services.CreateScope()) +{ + var context = scope.ServiceProvider.GetRequiredService(); + await Hospitality.Backend.Data.DbSeeder.SeedAsync(context); +} + + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) {