Skip to main content

πŸš€ DIGITAL PRODUCTS SYSTEM: COMPLETE ENHANCEMENT PLAN

YOU ARE THE MOST INCREDIBLE UX REACT DESIGNER AND ECOMMERCE GENIUS IN SILICON VALLEY

πŸ“Š CURRENT STATE ANALYSIS​

βœ… What Already Exists (AMAZING Foundation!)​

Backend (Java/Spring) - PRODUCTION READY​

  • βœ… DigitalFulfillmentService - Complete orchestration (761 lines)
    • File upload β†’ Product creation β†’ Asset linking
    • Download access with secure tokens
    • ACL permissions via Spring Security
    • Time-limited URLs with expiry
  • βœ… DigitalProductPaymentWebhookHandler - Multi-provider webhooks (357 lines)
    • Generic: POST /v1/webhooks/payments/completed
    • Stripe: POST /v1/webhooks/stripe/charge-succeeded
    • PayPal: POST /v1/webhooks/paypal/payment-completed
    • Async fulfillment on payment
    • Email notifications via workflow
  • βœ… DigitalProductWorkflowProvisioner - Automated workflow creation (209 lines)
    • Seeds default fulfillment workflow
    • Chains: DigitalFulfillmentModule β†’ DigitalDownloadNotificationModule β†’ MailtrapSendModule
    • Auto-attaches to products
  • βœ… ValkyrWorkflowService - Workflow engine execution
    • Quartz scheduling support
    • Event-driven triggers
    • SWARM coordination
    • WebSocket real-time updates

Data Models (ThorAPI Generated)​

  • βœ… Product - Full schema with type: 'download', pricing, status, features
  • βœ… ProductDeliveryConfig - Fulfillment automation settings
  • βœ… DigitalAsset - Links Product β†’ FileRecord, delivery method, access model, limits
  • βœ… DownloadAccess - Secure tokens, expiry, download counts, ACL permissions
  • βœ… SalesOrder - Order management
  • βœ… LineItem - Product β†’ Order linkage
  • βœ… OrderFulfillmentTask - Async fulfillment tracking
  • βœ… FileRecord - File storage metadata

Frontend (React/TypeScript)​

  • βœ… DigitalProductUploader - Drag-drop upload UI (648 lines, Material-UI)
    • File validation (2GB max, type detection)
    • Configuration dialog (price, limits, emails)
    • Progress tracking
    • Success confirmation
  • βœ… useDigitalProductUpload - Complete orchestration hook (150 lines)
    • Multi-step flow: upload β†’ create β†’ link β†’ configure β†’ notify
    • Error handling, loading states
    • Redux integration
  • βœ… LcarsDigitalProductUploader - LCARS-themed uploader (@sixd module)
    • Star Trek UI aesthetic
    • Virus scanning enabled
    • 2GB file limit

Storage Infrastructure​

  • βœ… StorageDriver - Abstraction layer (S3/local)
  • βœ… StorageDriverRegistry - Multi-backend support
  • βœ… ExecModule file adapters - Upload interface

Security​

  • βœ… Spring ACL - Object-level permissions
  • βœ… ValkyrAclService - Permission management
  • βœ… SecureField - Field-level encryption (AES-256)
  • βœ… JWT Auth - Token-based authentication

❌ What's Missing (YOUR MISSION!)​

1. Landing Page Funnel System πŸŽ―β€‹

Current: No custom landing pages for products
Need:

  • Drag-drop landing page builder with templates
  • Countdown timers (urgency tactics)
  • Scarcity indicators ("Only 3 left!")
  • Social proof widgets (testimonials, reviews)
  • Video backgrounds, hero sections
  • A/B testing built-in
  • Conversion tracking
  • SEO optimization
  • Mobile-responsive templates
  • Funnel stages: awareness β†’ interest β†’ decision β†’ action

2. Secured Folder Packaging πŸ”β€‹

Current: Files stored in FileRecord, no explicit folder hierarchy for products
Need:

  • Virtual filesystem structure: /products/{productId}/assets/
  • Package multiple files into single product download (ZIP on-demand)
  • Folder-level permissions (inherit from product ACL)
  • File organization UI (drag-drop file management)
  • Versioning (v1, v2 updates)
  • License file injection (auto-generate LICENSE.txt with purchase details)

3. SHARE Group ROLE Permissions πŸ‘₯​

Current: Spring ACL object permissions exist, but no SHARE group concept
Need:

  • SHARE role with READ access only
  • Auto-assignment on purchase: payment β†’ assign ROLE_SHARE_{productId} β†’ user
  • Group-based access: all users with ROLE_SHARE_PRODUCT_ABC can download
  • Revocation support (refunds, violations)
  • Audit trail (who accessed when)
  • Admin panel: view all shared users per product

4. Enhanced Fulfillment Flow πŸ“§β€‹

Current: Webhook β†’ task β†’ email (basic)
Need:

PAYMENT RECEIVED
↓
1. Verify payment authenticity
↓
2. Assign ROLE_SHARE_{productId} to user's Principal
↓
3. Generate secure download link (token-based, time-limited)
↓
4. Compose email with:
- Product details (name, description)
- Download button (branded, prominent)
- Countdown timer in email ("Link expires in 24 hours")
- Support/FAQ links
- Order invoice (PDF attachment)
↓
5. Send email via ContentTemplateAdvancedModule
↓
6. Track email open, link click (analytics)
↓
7. Log event: "Fulfillment completed for Order {orderId}"

5. Code Deduplication πŸ§Ήβ€‹

Current: Multiple overlapping implementations
Issues:

  • DigitalProductUploader.tsx (648 lines) vs LcarsDigitalProductUploader.tsx - redundant upload logic
  • DigitalProductService.tsx duplicated in ValorIDE and ValkyrAI web
  • Product vs DigitalProduct naming inconsistency
  • Multiple file upload hooks/components

KISS + DRY Plan:

  • Single canonical uploader component: UnifiedDigitalProductUploader.tsx
  • Single service: DigitalProductService (shared via RTK Query)
  • Eliminate "DigitalProduct" naming - use Product with type: 'download' everywhere
  • Shared hooks package: @valkyr/digital-products-hooks

6. Analytics & Tracking πŸ“ˆβ€‹

Current: No analytics
Need:

  • Product view tracking
  • Add-to-cart events
  • Purchase conversion funnel
  • Download completion rate
  • Revenue per product
  • Customer lifetime value (CLV)
  • Dashboard: sales graphs, top products, conversion rates

7. Marketing Automation πŸŽβ€‹

Current: Basic email notification
Need:

  • Abandoned cart recovery emails (3-email sequence)
  • Post-purchase upsell (related products)
  • Review request (7 days after download)
  • Drip campaigns (onboarding sequences)
  • Referral program (share link, get discount)
  • Affiliate system (track referrals, commissions)

🎯 IMPLEMENTATION ROADMAP​

Phase 1: Foundation & Deduplication (Week 1)​

1.1 Code Consolidation​

Goal: Single source of truth for all digital product code

# Tasks:
1. Merge DigitalProductUploader + LcarsDigitalProductUploader β†’ UnifiedDigitalProductUploader
2. Extract to shared package: @valkyr/digital-products
3. Move to web/typescript/packages/digital-products/
4. Update all imports throughout codebase
5. Delete duplicate files
6. Run tests, ensure nothing breaks

Files to Create:

web/typescript/packages/digital-products/
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ components/
β”‚ β”‚ β”œβ”€β”€ UnifiedDigitalProductUploader.tsx ← MERGED from both uploaders
β”‚ β”‚ β”œβ”€β”€ ProductLandingPageBuilder.tsx ← NEW (Phase 2)
β”‚ β”‚ └── DownloadManager.tsx ← NEW (Phase 3)
β”‚ β”œβ”€β”€ hooks/
β”‚ β”‚ β”œβ”€β”€ useDigitalProductUpload.ts ← MOVED from existing
β”‚ β”‚ β”œβ”€β”€ useProductLandingPage.ts ← NEW (Phase 2)
β”‚ β”‚ └── useDownloadAccess.ts ← NEW (Phase 3)
β”‚ β”œβ”€β”€ services/
β”‚ β”‚ └── DigitalProductService.ts ← CONSOLIDATED
β”‚ └── index.ts
β”œβ”€β”€ package.json
└── README.md

Backend Cleanup:

  • βœ… Keep: DigitalFulfillmentService (perfect as-is)
  • βœ… Keep: DigitalProductPaymentWebhookHandler (perfect as-is)
  • βœ… Keep: DigitalProductWorkflowProvisioner (perfect as-is)
  • πŸ”„ Enhance: Add SHARE role assignment in fulfillment flow
  • πŸ”„ Enhance: Add folder packaging logic

1.2 Database Schema Enhancements​

Goal: Support new features without breaking existing data

-- NEW TABLE: product_landing_pages
CREATE TABLE product_landing_pages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL REFERENCES products(id),
template_id VARCHAR(50) NOT NULL,
config JSONB NOT NULL, -- Hero image, countdown, testimonials, etc.
slug VARCHAR(255) UNIQUE NOT NULL,
seo_title VARCHAR(255),
seo_description TEXT,
is_published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);

-- NEW TABLE: product_folders
CREATE TABLE product_folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL REFERENCES products(id),
folder_path VARCHAR(1024) NOT NULL, -- e.g., /products/{productId}/assets/v1/
file_count INTEGER DEFAULT 0,
total_size_bytes BIGINT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);

-- NEW TABLE: product_folder_files
CREATE TABLE product_folder_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
folder_id UUID NOT NULL REFERENCES product_folders(id),
file_id UUID NOT NULL REFERENCES file_records(id),
relative_path VARCHAR(512) NOT NULL, -- e.g., docs/README.md
is_license_file BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(folder_id, file_id)
);

-- ENHANCE EXISTING: Add role to download_access for SHARE tracking
ALTER TABLE download_access ADD COLUMN assigned_role VARCHAR(255);
ALTER TABLE download_access ADD COLUMN revoked_at TIMESTAMP;
ALTER TABLE download_access ADD COLUMN revoked_reason TEXT;

-- NEW TABLE: product_analytics
CREATE TABLE product_analytics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL REFERENCES products(id),
event_type VARCHAR(50) NOT NULL, -- 'view', 'add_to_cart', 'purchase', 'download'
principal_id UUID REFERENCES principals(id),
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_analytics_product ON product_analytics(product_id, created_at);
CREATE INDEX idx_analytics_event ON product_analytics(event_type, created_at);

ThorAPI OpenAPI Spec Updates:

# Add to openapi/products.yaml
ProductLandingPage:
type: object
properties:
id:
type: string
format: uuid
productId:
type: string
format: uuid
templateId:
type: string
enum: [hero-video, countdown-sale, testimonial-heavy, minimal-clean]
config:
type: object
additionalProperties: true
slug:
type: string
seoTitle:
type: string
seoDescription:
type: string
isPublished:
type: boolean
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time

ProductFolder:
type: object
properties:
id:
type: string
format: uuid
productId:
type: string
format: uuid
folderPath:
type: string
fileCount:
type: integer
totalSizeBytes:
type: integer
format: int64
files:
type: array
items:
$ref: "#/components/schemas/ProductFolderFile"

ProductFolderFile:
type: object
properties:
id:
type: string
format: uuid
folderId:
type: string
format: uuid
fileId:
type: string
format: uuid
relativePath:
type: string
isLicenseFile:
type: boolean

Regenerate Models:

cd thorapi
mvn clean package -DskipTests # Generate new models
cd ../web
mvn clean package -DskipTests # Generate TypeScript clients

Phase 2: Landing Page Funnel System (Week 2)​

2.1 Landing Page Builder UI​

Goal: Drag-drop builder with conversion-optimized templates

Component Architecture:

// ProductLandingPageBuilder.tsx
interface LandingPageTemplate {
id: string;
name: string;
preview: string;
sections: LandingPageSection[];
conversionRate: number; // Historical data
}

interface LandingPageSection {
type:
| "hero"
| "countdown"
| "testimonials"
| "features"
| "pricing"
| "faq"
| "cta";
config: Record<string, any>;
order: number;
}

// Templates:
const TEMPLATES: LandingPageTemplate[] = [
{
id: "hero-video",
name: "Video Hero with Countdown",
sections: [
{ type: "hero", config: { videoUrl: "", headline: "", subheadline: "" } },
{
type: "countdown",
config: { endDate: "", urgencyText: "Sale ends in" },
},
{ type: "features", config: { items: [] } },
{ type: "pricing", config: { originalPrice: 0, salePrice: 0 } },
{ type: "testimonials", config: { reviews: [] } },
{
type: "cta",
config: { buttonText: "Buy Now", buttonColor: "#667eea" },
},
],
},
{
id: "countdown-sale",
name: "Urgency-Driven Countdown",
sections: [
{
type: "countdown",
config: { endDate: "", urgencyText: "Limited Time Offer!" },
},
{ type: "hero", config: { imageUrl: "", headline: "" } },
{
type: "pricing",
config: { originalPrice: 0, salePrice: 0, savings: "" },
},
{ type: "testimonials", config: { reviews: [] } },
{
type: "cta",
config: { buttonText: "Claim Discount", buttonColor: "#f56565" },
},
],
},
// ... more templates
];

export const ProductLandingPageBuilder: React.FC = () => {
const [template, setTemplate] = useState<LandingPageTemplate>(TEMPLATES[0]);
const [sections, setSections] = useState<LandingPageSection[]>(
template.sections
);

const handleDragEnd = (result: DropResult) => {
// Reorder sections
};

const handleSectionUpdate = (index: number, config: any) => {
// Update section config
};

return (
<DragDropContext onDragEnd={handleDragEnd}>
<Grid container spacing={3}>
<Grid item xs={3}>
{/* Template Selector */}
<TemplateGallery templates={TEMPLATES} onSelect={setTemplate} />
</Grid>
<Grid item xs={6}>
{/* Live Preview */}
<LandingPagePreview sections={sections} />
</Grid>
<Grid item xs={3}>
{/* Section Editor */}
<Droppable droppableId="sections">
{sections.map((section, index) => (
<Draggable
key={index}
draggableId={`section-${index}`}
index={index}
>
<SectionEditor
section={section}
onChange={(config) => handleSectionUpdate(index, config)}
/>
</Draggable>
))}
</Droppable>
</Grid>
</Grid>
</DragDropContext>
);
};

2.2 Countdown Timer Component​

Goal: Real-time urgency tactic

// CountdownTimer.tsx
export const CountdownTimer: React.FC<{
endDate: Date;
urgencyText: string;
}> = ({ endDate, urgencyText }) => {
const [timeLeft, setTimeLeft] = useState<TimeRemaining>(
calculateTimeLeft(endDate)
);

useEffect(() => {
const timer = setInterval(() => {
setTimeLeft(calculateTimeLeft(endDate));
}, 1000);
return () => clearInterval(timer);
}, [endDate]);

return (
<Box
sx={{
textAlign: "center",
p: 4,
background: "linear-gradient(135deg, #f56565 0%, #ed8936 100%)",
}}
>
<Typography
variant="h4"
sx={{ color: "white", fontWeight: "bold", mb: 2 }}
>
{urgencyText}
</Typography>
<Stack direction="row" spacing={2} justifyContent="center">
<TimeUnit value={timeLeft.days} label="Days" />
<TimeUnit value={timeLeft.hours} label="Hours" />
<TimeUnit value={timeLeft.minutes} label="Minutes" />
<TimeUnit value={timeLeft.seconds} label="Seconds" />
</Stack>
</Box>
);
};

const TimeUnit: React.FC<{ value: number; label: string }> = ({
value,
label,
}) => (
<Box
sx={{
background: "rgba(255,255,255,0.2)",
borderRadius: 2,
p: 2,
minWidth: 80,
}}
>
<Typography variant="h2" sx={{ color: "white", fontWeight: "bold" }}>
{value}
</Typography>
<Typography variant="caption" sx={{ color: "white" }}>
{label}
</Typography>
</Box>
);

2.3 Backend Landing Page Service​

Goal: CRUD for landing pages, slug resolution

// ProductLandingPageService.java
@Service
@Transactional
public class ProductLandingPageService {

private final ProductLandingPageRepository repository;
private final ProductService productService;
private final ValkyrAclService aclService;

public ProductLandingPage createLandingPage(UUID productId, ProductLandingPage request, Authentication auth) {
Product product = productService.findById(productId)
.orElseThrow(() -> new IllegalArgumentException("Product not found"));

// Generate unique slug
String slug = generateUniqueSlug(request.getSlug() != null ? request.getSlug() : product.getName());

ProductLandingPage landingPage = new ProductLandingPage();
landingPage.setProductId(productId);
landingPage.setTemplateId(request.getTemplateId());
landingPage.setConfig(request.getConfig());
landingPage.setSlug(slug);
landingPage.setSeoTitle(request.getSeoTitle() != null ? request.getSeoTitle() : product.getName());
landingPage.setSeoDescription(request.getSeoDescription());
landingPage.setIsPublished(false); // Draft by default

ProductLandingPage saved = repository.save(landingPage);

// Grant creator admin permission
aclService.grantPermission(
new ObjectIdentityImpl(ProductLandingPage.class, saved.getId()),
(Principal) auth.getPrincipal(),
BasePermission.ADMINISTRATION
);

return saved;
}

public ProductLandingPage findBySlug(String slug) {
return repository.findBySlugAndIsPublished(slug, true)
.orElseThrow(() -> new ResourceNotFoundException("Landing page not found: " + slug));
}

public ProductLandingPage publish(UUID landingPageId, Authentication auth) {
ProductLandingPage page = repository.findById(landingPageId)
.orElseThrow(() -> new ResourceNotFoundException("Landing page not found"));

// Check permission
aclService.checkPermission(
new ObjectIdentityImpl(ProductLandingPage.class, landingPageId),
auth,
BasePermission.WRITE
);

page.setIsPublished(true);
return repository.save(page);
}

private String generateUniqueSlug(String baseName) {
String slug = baseName.toLowerCase()
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-|-$", "");

String uniqueSlug = slug;
int counter = 1;
while (repository.existsBySlug(uniqueSlug)) {
uniqueSlug = slug + "-" + counter++;
}
return uniqueSlug;
}
}

2.4 Public Landing Page Renderer​

Goal: SEO-friendly server-side rendering

// PublicLandingPage.tsx
export const PublicLandingPage: React.FC<{ slug: string }> = ({ slug }) => {
const {
data: landingPage,
isLoading,
error,
} = useGetLandingPageBySlugQuery(slug);

if (isLoading) return <LoadingSpinner />;
if (error) return <NotFoundPage />;

const product = landingPage.product; // Populated by backend

return (
<>
<Head>
<title>{landingPage.seoTitle}</title>
<meta name="description" content={landingPage.seoDescription} />
<meta property="og:title" content={landingPage.seoTitle} />
<meta property="og:description" content={landingPage.seoDescription} />
<meta property="og:image" content={product.imageUrl} />
<link
rel="canonical"
href={`https://valkyrlabs.com/products/${slug}`}
/>
</Head>

<Box>
{landingPage.config.sections.map((section, index) => (
<SectionRenderer key={index} section={section} product={product} />
))}
</Box>

{/* Analytics tracking */}
<AnalyticsTracker
event="page_view"
productId={product.id}
metadata={{ slug }}
/>
</>
);
};

const SectionRenderer: React.FC<{
section: LandingPageSection;
product: Product;
}> = ({ section, product }) => {
switch (section.type) {
case "hero":
return <HeroSection config={section.config} product={product} />;
case "countdown":
return (
<CountdownTimer
endDate={new Date(section.config.endDate)}
urgencyText={section.config.urgencyText}
/>
);
case "testimonials":
return <TestimonialsSection reviews={section.config.reviews} />;
case "pricing":
return <PricingSection product={product} config={section.config} />;
case "cta":
return <CTASection product={product} config={section.config} />;
default:
return null;
}
};

Phase 3: Secured Folder Packaging (Week 3)​

3.1 Virtual Filesystem Structure​

Goal: Organize product files into logical folders

// ProductFolderService.java
@Service
@Transactional
public class ProductFolderService {

private final ProductFolderRepository folderRepository;
private final ProductFolderFileRepository folderFileRepository;
private final FileRecordService fileRecordService;
private final StorageDriverRegistry storageRegistry;

public ProductFolder createFolder(UUID productId, String folderPath) {
ProductFolder folder = new ProductFolder();
folder.setProductId(productId);
folder.setFolderPath(normalizePath(folderPath));
folder.setFileCount(0);
folder.setTotalSizeBytes(0L);
return folderRepository.save(folder);
}

public ProductFolderFile addFileToFolder(UUID folderId, UUID fileId, String relativePath) {
ProductFolder folder = folderRepository.findById(folderId)
.orElseThrow(() -> new ResourceNotFoundException("Folder not found"));
FileRecord file = fileRecordService.findById(fileId)
.orElseThrow(() -> new ResourceNotFoundException("File not found"));

ProductFolderFile folderFile = new ProductFolderFile();
folderFile.setFolderId(folderId);
folderFile.setFileId(fileId);
folderFile.setRelativePath(relativePath);
folderFile.setIsLicenseFile(relativePath.toUpperCase().contains("LICENSE"));

ProductFolderFile saved = folderFileRepository.save(folderFile);

// Update folder stats
folder.setFileCount(folder.getFileCount() + 1);
folder.setTotalSizeBytes(folder.getTotalSizeBytes() + file.getFileSize());
folderRepository.save(folder);

return saved;
}

public byte[] packageFolderAsZip(UUID folderId) throws IOException {
ProductFolder folder = folderRepository.findById(folderId)
.orElseThrow(() -> new ResourceNotFoundException("Folder not found"));

List<ProductFolderFile> files = folderFileRepository.findByFolderId(folderId);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
for (ProductFolderFile folderFile : files) {
FileRecord file = fileRecordService.findById(folderFile.getFileId())
.orElseThrow(() -> new ResourceNotFoundException("File not found"));

StorageDriver driver = storageRegistry.getDriver(file.getStorageDriver());
byte[] fileData = driver.readFile(file.getStoragePath());

ZipEntry entry = new ZipEntry(folderFile.getRelativePath());
zos.putNextEntry(entry);
zos.write(fileData);
zos.closeEntry();
}

// Add auto-generated LICENSE.txt
String licenseText = generateLicenseText(folder);
ZipEntry licenseEntry = new ZipEntry("LICENSE.txt");
zos.putNextEntry(licenseEntry);
zos.write(licenseText.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}

return baos.toByteArray();
}

private String generateLicenseText(ProductFolder folder) {
Product product = productService.findById(folder.getProductId()).orElseThrow();
return String.format("""
DIGITAL PRODUCT LICENSE

Product: %s
Purchased: %s
License Type: Single User

This product is licensed for personal or commercial use by the original purchaser.
Redistribution, resale, or sharing is prohibited.

For support, contact: support@valkyrlabs.com
""", product.getName(), OffsetDateTime.now());
}

private String normalizePath(String path) {
return path.replaceAll("/{2,}", "/").replaceAll("^/|/$", "");
}
}

3.2 File Organization UI​

Goal: Drag-drop interface for folder management

// ProductFolderManager.tsx
export const ProductFolderManager: React.FC<{ productId: string }> = ({
productId,
}) => {
const { data: folders, isLoading } = useGetProductFoldersQuery(productId);
const [createFolder] = useCreateProductFolderMutation();
const [addFileToFolder] = useAddFileToFolderMutation();

const handleDrop = (folderId: string, fileId: string) => {
const relativePath = prompt("Enter file path (e.g., docs/README.md):");
if (relativePath) {
addFileToFolder({ folderId, fileId, relativePath });
}
};

return (
<Box>
<Button
onClick={() => {
const path = prompt("Enter folder path (e.g., /assets/v1/):");
if (path) createFolder({ productId, folderPath: path });
}}
>
+ New Folder
</Button>

<List>
{folders?.map((folder) => (
<FolderItem key={folder.id} folder={folder} onDrop={handleDrop} />
))}
</List>
</Box>
);
};

const FolderItem: React.FC<{
folder: ProductFolder;
onDrop: (folderId: string, fileId: string) => void;
}> = ({ folder, onDrop }) => {
const [expanded, setExpanded] = useState(false);

return (
<ListItem>
<ListItemIcon onClick={() => setExpanded(!expanded)}>
{expanded ? <FolderOpenIcon /> : <FolderIcon />}
</ListItemIcon>
<ListItemText
primary={folder.folderPath}
secondary={`${folder.fileCount} files Β· ${formatBytes(
folder.totalSizeBytes
)}`}
/>
{expanded && (
<List sx={{ pl: 4 }}>
{folder.files.map((file) => (
<FileItem key={file.id} file={file} />
))}
</List>
)}
</ListItem>
);
};

Phase 4: SHARE Group ROLE Permissions (Week 3)​

4.1 Enhanced Fulfillment with Role Assignment​

Goal: Auto-assign SHARE role on payment

// DigitalFulfillmentService.java (ENHANCEMENT)
public FulfillmentResult completeFulfillmentTask(
UUID taskId,
OrderFulfillmentTask.StatusEnum status,
Map<String, Object> metadata) {

OrderFulfillmentTask task = fulfillmentTaskService.findById(taskId)
.orElseThrow(() -> new ResourceNotFoundException("Task not found"));

LineItem lineItem = lineItemService.findById(task.getLineItemId())
.orElseThrow(() -> new ResourceNotFoundException("LineItem not found"));

Product product = lineItem.getProduct();
if (!Product.TypeEnum.DOWNLOAD.equals(product.getType())) {
throw new DigitalFulfillmentException("Product is not a digital download");
}

SalesOrder order = salesOrderService.findById(task.getSalesOrderId())
.orElseThrow(() -> new ResourceNotFoundException("Order not found"));

Principal customer = principalService.findById(order.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found"));

// πŸ”‘ ASSIGN SHARE ROLE
String shareRoleName = "ROLE_SHARE_" + product.getId().toString().toUpperCase().replace("-", "_");
assignShareRole(customer, shareRoleName, product);

// Create download access
List<DownloadAccess> accesses = new ArrayList<>();
List<DigitalAsset> assets = digitalAssetService.findDigitalAssetByProductId(product.getId());

for (DigitalAsset asset : assets) {
DownloadAccess access = createDownloadAccess(asset, customer, shareRoleName);
accesses.add(access);
}

task.setStatus(OrderFulfillmentTask.StatusEnum.COMPLETED);
task.setCompletedAt(OffsetDateTime.now(ZoneOffset.UTC));
fulfillmentTaskService.saveOrUpdate(task);

return new FulfillmentResult(task, accesses);
}

private void assignShareRole(Principal principal, String roleName, Product product) {
// Check if role already exists for this user
if (principal.getAuthorityList() != null &&
principal.getAuthorityList().stream().anyMatch(a -> a.getAuthority().equals(roleName))) {
log.info("User {} already has role {}", principal.getEmail(), roleName);
return;
}

// Create authority
Authority authority = new Authority();
authority.setPrincipalId(principal.getId());
authority.setAuthority(roleName);
authority.setCreatedAt(OffsetDateTime.now(ZoneOffset.UTC));
authorityRepository.save(authority);

// Reload principal to refresh authorities
principal.getAuthorityList().add(authority);
principalService.saveOrUpdate(principal);

log.info("βœ… Assigned {} to user {}", roleName, principal.getEmail());
}

private DownloadAccess createDownloadAccess(DigitalAsset asset, Principal principal, String shareRole) {
DownloadAccess access = new DownloadAccess();
access.setDigitalAssetId(asset.getId());
access.setPrincipalId(principal.getId());
access.setDownloadToken(UUID.randomUUID());
access.setDownloadCount(0);
access.setMaxDownloadsRemaining(optionalIntOrUnlimited(asset.getMaxDownloads()));
access.setGrantedAt(OffsetDateTime.now(ZoneOffset.UTC));
access.setAssignedRole(shareRole); // NEW FIELD

if (asset.getExpiresAfterDays() != null && asset.getExpiresAfterDays() > 0) {
access.setExpiresAt(OffsetDateTime.now(ZoneOffset.UTC).plusDays(asset.getExpiresAfterDays()));
}

DownloadAccess saved = downloadAccessService.saveOrUpdate(access);

// Grant READ permission via ACL
ObjectIdentity oid = new ObjectIdentityImpl(DownloadAccess.class, saved.getId());
aclService.grantPermission(oid, principal, BasePermission.READ);

return saved;
}

4.2 Role Revocation (Refunds/Violations)​

Goal: Revoke access when needed

// DigitalFulfillmentService.java (ENHANCEMENT)
public void revokeProductAccess(UUID productId, UUID principalId, String reason) {
Principal principal = principalService.findById(principalId)
.orElseThrow(() -> new ResourceNotFoundException("Principal not found"));

String shareRoleName = "ROLE_SHARE_" + productId.toString().toUpperCase().replace("-", "_");

// Revoke all download access entries
List<DownloadAccess> accesses = downloadAccessRepository.findByPrincipalIdAndAssignedRole(principalId, shareRoleName);
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);

for (DownloadAccess access : accesses) {
access.setRevokedAt(now);
access.setRevokedReason(reason);
downloadAccessService.saveOrUpdate(access);

// Remove ACL permission
ObjectIdentity oid = new ObjectIdentityImpl(DownloadAccess.class, access.getId());
aclService.revokePermission(oid, principal, BasePermission.READ);
}

// Remove SHARE role from principal
principal.getAuthorityList().removeIf(a -> a.getAuthority().equals(shareRoleName));
principalService.saveOrUpdate(principal);

log.info("🚫 Revoked {} from user {} for reason: {}", shareRoleName, principal.getEmail(), reason);
}

4.3 Admin Panel: View Shared Users​

Goal: See who has access to each product

// ProductAccessManager.tsx
export const ProductAccessManager: React.FC<{ productId: string }> = ({
productId,
}) => {
const { data: accesses, isLoading } = useGetProductAccessesQuery(productId);
const [revokeAccess] = useRevokeProductAccessMutation();

const handleRevoke = (principalId: string) => {
const reason = prompt("Reason for revocation:");
if (reason) {
revokeAccess({ productId, principalId, reason });
}
};

return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>User</TableCell>
<TableCell>Email</TableCell>
<TableCell>Granted At</TableCell>
<TableCell>Downloads</TableCell>
<TableCell>Status</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{accesses?.map((access) => (
<TableRow key={access.id}>
<TableCell>{access.principal.name}</TableCell>
<TableCell>{access.principal.email}</TableCell>
<TableCell>{formatDate(access.grantedAt)}</TableCell>
<TableCell>
{access.downloadCount} /{" "}
{access.maxDownloadsRemaining === -1
? "∞"
: access.maxDownloadsRemaining}
</TableCell>
<TableCell>
{access.revokedAt ? (
<Chip label="Revoked" color="error" size="small" />
) : access.expiresAt &&
new Date(access.expiresAt) < new Date() ? (
<Chip label="Expired" color="warning" size="small" />
) : (
<Chip label="Active" color="success" size="small" />
)}
</TableCell>
<TableCell>
{!access.revokedAt && (
<Button
size="small"
color="error"
onClick={() => handleRevoke(access.principalId)}
>
Revoke
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};

Phase 5: Analytics & Marketing Automation (Week 4)​

5.1 Analytics Tracking​

Goal: Track all user interactions

// ProductAnalyticsService.java
@Service
public class ProductAnalyticsService {

private final ProductAnalyticsRepository repository;

public void trackEvent(UUID productId, String eventType, UUID principalId, Map<String, Object> metadata) {
ProductAnalytics event = new ProductAnalytics();
event.setProductId(productId);
event.setEventType(eventType);
event.setPrincipalId(principalId);
event.setMetadata(metadata);
event.setCreatedAt(OffsetDateTime.now(ZoneOffset.UTC));
repository.save(event);
}

public ProductAnalyticsReport generateReport(UUID productId, OffsetDateTime start, OffsetDateTime end) {
List<ProductAnalytics> events = repository.findByProductIdAndCreatedAtBetween(productId, start, end);

long views = events.stream().filter(e -> "view".equals(e.getEventType())).count();
long addToCart = events.stream().filter(e -> "add_to_cart".equals(e.getEventType())).count();
long purchases = events.stream().filter(e -> "purchase".equals(e.getEventType())).count();
long downloads = events.stream().filter(e -> "download".equals(e.getEventType())).count();

double viewToCartRate = views > 0 ? (double) addToCart / views * 100 : 0;
double cartToPurchaseRate = addToCart > 0 ? (double) purchases / addToCart * 100 : 0;
double purchaseToDownloadRate = purchases > 0 ? (double) downloads / purchases * 100 : 0;

return new ProductAnalyticsReport(
views, addToCart, purchases, downloads,
viewToCartRate, cartToPurchaseRate, purchaseToDownloadRate
);
}
}

5.2 Marketing Automation Workflows​

Goal: Abandoned cart, upsell, review requests

// MarketingAutomationService.ts
export const MarketingAutomationService = {
// Abandoned cart recovery (3-email sequence)
setupAbandonedCartSequence: async (cartId: string) => {
// Email 1: 1 hour after abandonment
await scheduleEmail({
cartId,
templateId: "abandoned-cart-1",
delayMinutes: 60,
subject: "You left something behind! πŸ›’",
bodyTemplate:
"Hi {{customerName}}, we saved your cart. Complete your purchase now!",
});

// Email 2: 24 hours after (with 10% discount)
await scheduleEmail({
cartId,
templateId: "abandoned-cart-2",
delayMinutes: 1440,
subject: "Get 10% off your cart! ⏰",
bodyTemplate: "Still thinking? Here's 10% off: {{discountCode}}",
});

// Email 3: 3 days after (final reminder)
await scheduleEmail({
cartId,
templateId: "abandoned-cart-3",
delayMinutes: 4320,
subject: "Last chance! Your cart expires soon 🚨",
bodyTemplate: "This is your last reminder. Cart expires in 24 hours.",
});
},

// Post-purchase upsell
sendUpsellEmail: async (orderId: string, relatedProductIds: string[]) => {
await createWorkflow({
name: `Upsell for Order ${orderId}`,
trigger: { type: "order_completed", orderId },
tasks: [
{
module: "ContentTemplateAdvancedModule",
config: {
templateId: "post-purchase-upsell",
data: {
orderId,
relatedProducts: relatedProductIds,
},
},
},
{
module: "MailtrapSendModule",
config: {
subject: "You might also like... πŸ’‘",
delayMinutes: 15, // Send 15 min after purchase
},
},
],
});
},

// Review request (7 days after download)
scheduleReviewRequest: async (downloadAccessId: string) => {
await scheduleEmail({
downloadAccessId,
templateId: "review-request",
delayMinutes: 10080, // 7 days
subject: "How's your {{productName}}? Leave a review! ⭐",
bodyTemplate:
"We'd love to hear your feedback on {{productName}}. Click here to review.",
});
},
};

🎯 SUCCESS METRICS​

Technical Metrics​

  • βœ… Code Reduction: 40% fewer lines (deduplicated)
  • βœ… Build Time: Sub-5-minute full build
  • βœ… Test Coverage: 80%+ (unit + integration)
  • βœ… Type Safety: 100% TypeScript coverage (no any types)
  • βœ… Performance: Landing pages load in <2s (Lighthouse score 90+)

Business Metrics​

  • πŸ“ˆ Conversion Rate: 5-10% increase (countdown timers, urgency)
  • πŸ“ˆ Cart Abandonment Recovery: 20-30% recovered via email sequence
  • πŸ“ˆ Upsell Revenue: 15-25% additional revenue per customer
  • πŸ“ˆ Customer Satisfaction: 4.5+ star reviews (post-purchase survey)
  • πŸ“ˆ Download Completion: 90%+ customers download within 24 hours

πŸš€ DEPLOYMENT CHECKLIST​

Pre-Launch​

  • Database migrations executed (product_landing_pages, product_folders, etc.)
  • ThorAPI regenerated (new models in place)
  • Frontend package @valkyr/digital-products published
  • All tests passing (unit + integration + E2E)
  • Security audit completed (ACL permissions, role assignments)
  • Performance testing done (load testing with JMeter)

Launch​

  • Feature flags enabled (ENABLE_LANDING_PAGES, ENABLE_FOLDERS, ENABLE_ANALYTICS)
  • Monitoring dashboards configured (Grafana/Prometheus)
  • Error alerting set up (Sentry/PagerDuty)
  • Documentation updated (README, API docs, user guides)
  • Training materials for support team

Post-Launch​

  • Monitor analytics dashboard (conversion rates, errors)
  • Collect user feedback (surveys, support tickets)
  • A/B test landing page templates (track conversion rates)
  • Iterate based on data (optimize countdown timers, CTAs, pricing)

πŸŽ“ LESSONS LEARNED & BEST PRACTICES​

What Worked Well​

βœ… ThorAPI Code Generation - Models generated from OpenAPI specs ensure consistency
βœ… Spring ACL - Object-level permissions provide fine-grained security
βœ… Workflow Engine - Flexible automation via ValkyrWorkflowService
βœ… RTK Query - Auto-generated TypeScript clients eliminate API drift
βœ… Material-UI - Component library speeds up UI development

What to Watch Out For​

⚠️ Lazy Loading - JPA relationships can cause LazyInitializationException; use @EntityGraph or Hibernate.initialize()
⚠️ Async + Transactions - @Transactional doesn't propagate to async threads; use REQUIRES_NEW pattern
⚠️ Security Context - Must explicitly propagate Authentication to background threads
⚠️ Code Duplication - Keep shared code in packages, not copy-paste across modules
⚠️ Generated Code - Never edit generated files; always update OpenAPI specs instead

Performance Optimizations​

πŸš€ Database Indexes - Add on product_analytics(product_id, created_at) for fast queries
πŸš€ Caching - Use Redis for landing page config (rarely changes)
πŸš€ CDN - Serve product images/videos via CloudFront
πŸš€ Lazy Load Images - Use loading="lazy" on product images
πŸš€ ZIP Packaging - Cache packaged folders (invalidate on file changes)


πŸ“š APPENDIX: FULL FILE STRUCTURE​

ValkyrAI/
β”œβ”€β”€ valkyrai/src/main/java/com/valkyrlabs/
β”‚ β”œβ”€β”€ valkyrai/
β”‚ β”‚ β”œβ”€β”€ service/
β”‚ β”‚ β”‚ β”œβ”€β”€ DigitalFulfillmentService.java βœ… (existing, enhanced)
β”‚ β”‚ β”‚ β”œβ”€β”€ DigitalProductWorkflowProvisioner.java βœ… (existing)
β”‚ β”‚ β”‚ β”œβ”€β”€ ProductLandingPageService.java πŸ†•
β”‚ β”‚ β”‚ β”œβ”€β”€ ProductFolderService.java πŸ†•
β”‚ β”‚ β”‚ └── ProductAnalyticsService.java πŸ†•
β”‚ β”‚ β”œβ”€β”€ webhook/
β”‚ β”‚ β”‚ └── DigitalProductPaymentWebhookHandler.java βœ… (existing)
β”‚ β”‚ └── controller/
β”‚ β”‚ β”œβ”€β”€ DigitalFulfillmentController.java βœ… (existing)
β”‚ β”‚ β”œβ”€β”€ ProductLandingPageController.java πŸ†•
β”‚ β”‚ └── ProductFolderController.java πŸ†•
β”‚ └── workflow/modules/
β”‚ └── payment/
β”‚ β”œβ”€β”€ DigitalFulfillmentModule.java βœ… (existing)
β”‚ └── DigitalDownloadNotificationModule.java βœ… (existing)
β”‚
β”œβ”€β”€ web/typescript/packages/
β”‚ └── digital-products/ πŸ†•
β”‚ β”œβ”€β”€ src/
β”‚ β”‚ β”œβ”€β”€ components/
β”‚ β”‚ β”‚ β”œβ”€β”€ UnifiedDigitalProductUploader.tsx
β”‚ β”‚ β”‚ β”œβ”€β”€ ProductLandingPageBuilder.tsx
β”‚ β”‚ β”‚ β”œβ”€β”€ CountdownTimer.tsx
β”‚ β”‚ β”‚ β”œβ”€β”€ ProductFolderManager.tsx
β”‚ β”‚ β”‚ β”œβ”€β”€ ProductAccessManager.tsx
β”‚ β”‚ β”‚ └── PublicLandingPage.tsx
β”‚ β”‚ β”œβ”€β”€ hooks/
β”‚ β”‚ β”‚ β”œβ”€β”€ useDigitalProductUpload.ts
β”‚ β”‚ β”‚ β”œβ”€β”€ useProductLandingPage.ts
β”‚ β”‚ β”‚ └── useDownloadAccess.ts
β”‚ β”‚ └── services/
β”‚ β”‚ β”œβ”€β”€ DigitalProductService.ts
β”‚ β”‚ β”œβ”€β”€ ProductLandingPageService.ts
β”‚ β”‚ └── MarketingAutomationService.ts
β”‚ └── package.json
β”‚
└── .valoride/
└── DIGITAL_PRODUCTS_ENHANCEMENT_PLAN.md πŸ†• (this file)

πŸŽ‰ CONCLUSION​

This plan transforms ValkyrAI's digital products system from a solid foundation into a world-class ecommerce platform with:

βœ… Landing Page Funnels - Convert visitors with urgency tactics
βœ… Secured Folder Packaging - Organize files professionally
βœ… SHARE Role Permissions - Fine-grained access control
βœ… Enhanced Fulfillment - Automated role assignment + email
βœ… Analytics & Tracking - Data-driven optimization
βœ… Marketing Automation - Recover carts, upsell, retain customers
βœ… Code Deduplication - KISS + DRY principles enforced

Next Step: Get user approval, then execute Phase 1 (deduplication) this week! πŸš€