dad798a904
Deploy to TrueNAS / deploy (push) Failing after 33s
- Landing page with hero, feature grid, trust signals - Split-layout login redesign (admin + portal) - Pricing page with storage tiers (5GB/50GB/unlimited) - StorageQuotaService backend (V36 migration, 402 on exceeded) - Frontend storage integration + 402 error handling - StorageController uses TenantContext for tenant isolation - onTierChange() hook for subscription tier updates
125 lines
7.5 KiB
Markdown
125 lines
7.5 KiB
Markdown
# Code Review: Sprint 14 — Marketing & Monetization
|
||
|
||
**Datum:** 2026-06-18
|
||
**Reviewer:** Roo (Reviewer)
|
||
**Plan:** cannamanage-sprint14-plan.md v2
|
||
**Testplan:** cannamanage-sprint14-testplan.md v2
|
||
**Status:** ⚠️ Approved with comments
|
||
|
||
---
|
||
|
||
## Zusammenfassung
|
||
|
||
Implementation is solid and complete for all 5 phases. All plan components are present, i18n is complete with full DE/EN parity, the backend quota enforcement chain is correctly wired end-to-end, and the frontend properly handles 402 responses. Two warnings identified — one security-relevant deviation from plan (StorageController auth pattern) and one missing planned component (SubscriptionService tier-change hook).
|
||
|
||
## Geprüfte Dateien
|
||
|
||
| Datei | Änderung | Bewertung |
|
||
|-------|---------|-----------|
|
||
| `cannamanage-frontend/src/app/(marketing)/page.tsx` | Neu | ✅ |
|
||
| `cannamanage-frontend/src/app/(marketing)/marketing-layout-client.tsx` | Neu | ✅ |
|
||
| `cannamanage-frontend/src/app/(marketing)/layout.tsx` | Geändert | ✅ |
|
||
| `cannamanage-frontend/src/app/(auth)/layout.tsx` | Geändert | ✅ |
|
||
| `cannamanage-frontend/src/app/(auth)/login/page.tsx` | Geändert | ✅ |
|
||
| `cannamanage-frontend/src/app/(portal)/portal-login/page.tsx` | Geändert | ✅ |
|
||
| `cannamanage-frontend/src/app/(marketing)/pricing/page.tsx` | Geändert | ✅ (not reviewed in full — storage keys verified) |
|
||
| `cannamanage-frontend/messages/de.json` | Geändert | ✅ |
|
||
| `cannamanage-frontend/messages/en.json` | Geändert | ✅ |
|
||
| `cannamanage-frontend/src/services/storage.ts` | Neu | ✅ |
|
||
| `cannamanage-frontend/src/services/documents.ts` | Geändert | ✅ |
|
||
| `cannamanage-service/src/main/java/.../StorageQuotaService.java` | Neu | ✅ |
|
||
| `cannamanage-service/src/main/java/.../StorageQuotaExceededException.java` | Neu | ✅ |
|
||
| `cannamanage-api/src/main/java/.../StorageController.java` | Neu | ⚠️ |
|
||
| `cannamanage-api/src/main/java/.../GlobalExceptionHandler.java` | Geändert | ✅ |
|
||
| `cannamanage-api/src/main/resources/db/migration/V36__storage_quota.sql` | Neu | ✅ |
|
||
| `cannamanage-domain/src/main/java/.../Club.java` | Geändert | ✅ |
|
||
| `cannamanage-service/src/main/java/.../DocumentService.java` | Geändert | ✅ |
|
||
|
||
## Checkliste
|
||
|
||
| # | Prüfpunkt | Ergebnis | Anmerkung |
|
||
|---|-----------|----------|-----------|
|
||
| 1 | Plan-Konformität | ⚠️ | StorageController uses @RequestParam instead of JWT extraction (Step 4.5). SubscriptionService hook (Step 4.9) not implemented. |
|
||
| 2 | Kein Extra-Scope | ✅ | No scope creep detected |
|
||
| 3 | Bestehende Patterns korrekt | ✅ | Follows existing codebase conventions (constructor injection, @Slf4j, RFC 9457 ProblemDetail) |
|
||
| 4 | i18n vollständig (de + en) | ✅ | 26 `marketing.home` keys, 10 `marketing.nav` keys, 16 `marketing.pricing.storage` keys, 10 `comparison` keys, FAQ storage — all present in both DE and EN with zero mismatches |
|
||
| 5 | StorageQuotaExceededException separat | ✅ | Separate class with incompatible constructor (long, long, long). QuotaExceededException maps to 409, StorageQuotaExceededException maps to 402 — correct separation. |
|
||
| 6 | V36 Migration korrekt | ✅ | Non-destructive (`ADD COLUMN IF NOT EXISTS`), correct default (5 GB = 5368709120), backfill via SUM(documents.file_size) |
|
||
| 7 | Frontend responsive + dark/light | ✅ | Landing page uses responsive grid (`grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`), auth layouts use `hidden md:flex`, proper use of Tailwind dark mode utilities |
|
||
| 8 | Error handling (402 on quota exceeded) | ✅ | Full chain: StorageQuotaExceededException → GlobalExceptionHandler 402 → documents.ts catches status 402 → throws typed error with problemDetail |
|
||
|
||
## Befunde
|
||
|
||
### ⚠️ WARNING-1: StorageController auth deviation from plan
|
||
|
||
**Datei:** [`StorageController.java`](cannamanage-api/src/main/java/de/cannamanage/api/controller/StorageController.java:29)
|
||
|
||
**Plan (Step 4.5)** specified:
|
||
```java
|
||
@GetMapping("/usage")
|
||
public ResponseEntity<StorageUsageDTO> getUsage(@AuthenticationPrincipal UserDetails user) {
|
||
UUID clubId = extractClubId(user);
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**Actual implementation:**
|
||
```java
|
||
@GetMapping("/usage")
|
||
public ResponseEntity<StorageUsageDTO> getUsage(@RequestParam UUID clubId) {
|
||
return ResponseEntity.ok(storageQuotaService.getUsage(clubId));
|
||
}
|
||
```
|
||
|
||
- **Risiko:** Any authenticated ADMIN/STAFF user can query any club's storage usage by passing an arbitrary `clubId`. The plan's approach enforced tenant isolation by deriving the club from the JWT token.
|
||
- **Empfehlung:** Replace `@RequestParam UUID clubId` with `@AuthenticationPrincipal` extraction pattern used elsewhere (e.g., DocumentController). This ensures users can only see their own club's data. If cross-club access is intentional (admin panel use case), add a separate admin-only endpoint.
|
||
|
||
---
|
||
|
||
### ⚠️ WARNING-2: SubscriptionService tier-change hook not implemented
|
||
|
||
**Plan (Step 4.9)** specified a `SubscriptionService.onTierChange(UUID clubId, PlanTier newTier)` method to update `storage_limit_bytes` when a club changes tier.
|
||
|
||
- **Not found** in the implementation.
|
||
- **Impact:** Low — the plan's own note says "Full subscription management (Stripe webhooks, billing cycle, trial-to-paid) is deferred to a future sprint." The hook would currently be dead code.
|
||
- **Empfehlung:** Acceptable to defer. When subscription management is implemented, this hook must be added. The `StorageQuotaService.getLimitForTier()` already exists to support it.
|
||
|
||
---
|
||
|
||
### ℹ️ INFO-1: getLimitForTier takes String instead of PlanTier enum
|
||
|
||
**Datei:** [`StorageQuotaService.java`](cannamanage-service/src/main/java/de/cannamanage/service/StorageQuotaService.java:86)
|
||
|
||
Plan specified `getLimitForTier(PlanTier tier)` but implementation uses `getLimitForTier(String tier)` with a switch on lowercase string matching. This is pragmatic since no `PlanTier` enum exists yet in the domain module. When the enum is created, update the method signature.
|
||
|
||
---
|
||
|
||
### ℹ️ INFO-2: Marketing layout extracted to client component
|
||
|
||
**Datei:** [`marketing-layout-client.tsx`](cannamanage-frontend/src/app/(marketing)/marketing-layout-client.tsx:1)
|
||
|
||
The plan didn't explicitly specify this separation, but it's a correct architectural choice: the server-side `layout.tsx` handles `getMessages()` and wraps with `NextIntlClientProvider`, while the client component handles the actual rendering with `useTranslations`. This follows Next.js 14 best practices for server/client component boundaries.
|
||
|
||
---
|
||
|
||
### ℹ️ INFO-3: Frontend storage service uses apiClient with clubId param
|
||
|
||
**Datei:** [`storage.ts`](cannamanage-frontend/src/services/storage.ts:12)
|
||
|
||
The frontend `getStorageUsage(clubId: string)` matches the backend's `@RequestParam UUID clubId`. This is consistent — but both should be updated together if WARNING-1 is addressed (the frontend would then not need to pass clubId explicitly).
|
||
|
||
## Tests
|
||
|
||
- **Backend-Tests ausgeführt:** Nicht im Scope dieses Reviews (kein Build-Ausführung angefordert)
|
||
- **Testplan T-14 bis T-32:** Backend unit/integration tests not yet verified as passing
|
||
- **E2E T-01 bis T-13:** Frontend E2E tests not executed in this review
|
||
|
||
## Empfehlung
|
||
|
||
**⚠️ Approved with comments** — merge is acceptable, but WARNING-1 (StorageController tenant isolation) should be addressed before production deployment. WARNING-2 (SubscriptionService hook) is acceptable to defer.
|
||
|
||
### Prioritized actions:
|
||
1. **Before production:** Fix StorageController to use JWT-based club extraction instead of `@RequestParam`
|
||
2. **Next sprint:** Add SubscriptionService tier-change hook when billing features are implemented
|
||
3. **Housekeeping:** Introduce `PlanTier` enum and update `getLimitForTier` signature
|