Skip to main content

ADR-009: Digital Product Fulfillment System Architecture

Date: 2025-10-18 Status: ACCEPTED Stakeholders: John McMahon (CTO/CEO), ValkyrAI Core Team Relates To: PR #35 (feat(core): rc wip), ValkyrAI v0.6.0 BETA

Context

ValkyrAI required a complete end-to-end digital product delivery system to enable:

  • Uploading digital assets (e-books, PDFs, software)
  • Creating downloadable products tied to file storage
  • Automated fulfillment workflows on order completion
  • Row-level access control via ACL enforcement
  • Token-based secure downloads with usage tracking

The challenge: integrate cleanly with existing Product, SalesOrder, Invoice, FileRecord, Workflow, and ACL models without duplicating auto-generated fields or violating THORAPI's "minimal duplication" golden rule.

Design Goals

  1. Modularity: Minimal new models; composition via UUID references
  2. THORAPI Compliance: No re-creation of auto-generated fields (id, createdDate, owner_id, etc.)
  3. E2E Cohesion: Link files → products → orders → fulfillment → ACL → downloads
  4. Security: Token validation, download limits, revocation support
  5. Workflow Integration: ValkyrAI ExecModules for async fulfillment automation

Solution: Four-Model Extension Architecture

1. DigitalAsset (New)

Purpose: Immutable record linking FileRecord to Product for delivery metadata.

ProductId (UUID ref) ──→ Product (enum type="download")
FileId (UUID ref) ────→ FileRecord (checksumSha256, virusScanStatus=CLEAN)
deliveryMethod ─────→ enum: direct_download, email_delivery, portal_access, streaming, api_key
accessModel ────────→ enum: perpetual, subscription, trial, license_key, one_time
maxDownloads ───────→ int (-1 = unlimited)
expiresAfterDays ───→ int (-1 = never)
notifyCustomerOnExpiry → boolean

Rationale: Separates fulfillment metadata from Product (keeps Product generic). Single responsibility: define how a file is deliverable.


2. DownloadAccess (New)

Purpose: Row-level ACL permission grant for Principal to download DigitalAsset.

DigitalAssetId (UUID ref) ──→ DigitalAsset
PrincipalId (UUID ref) ─────→ Principal (customer/user)
SalesOrderLineItemId (UUID ref) ─→ LineItem (audit: which purchase unlocked this?)
downloadToken (UUID, x-thorapi-secureField) ─ secure, regenerable
downloadCount ──────────── int (incremented on each download)
maxDownloadsRemaining ───── int (-1 = unlimited, enforced on download)
grantedAt ──────────────── timestamp (auto-filled on creation)
expiresAt ──────────────── timestamp | null (null = never expires)
lastDownloadedAt ───────── timestamp | null
revokedAt ──────────────── timestamp | null (soft-delete)
revokedReason ──────────── string | null (e.g., "refund", "chargeback", "policy_violation")

Rationale: Represents the fulfillment outcome—customer's earned right to download. Backed by Spring ACL (ValkyrAclService grants READ permission). Token is cryptographically secure and regenerable per request.


3. OrderFulfillmentTask (New)

Purpose: Tracks fulfillment action state on order/line item; drives workflow execution.

SalesOrderId (UUID ref) ────→ SalesOrder (which order to fulfill?)
FulfillmentType (enum) ─────→ digital_delivery, physical_shipment, service_activation, etc.
Status (enum) ──────────────→ pending, in_progress, completed, failed, canceled
WorkflowId (UUID ref, nullable) → Workflow (which ValkyrAI workflow executes this?)
AssignedTo (UUID, nullable) ─→ Principal (for manual fulfillment routing)
Attempts ──────────────────→ int (retry counter)
LastError ─────────────────→ string | null (error from failed attempt)
CompletedAt ───────────────→ timestamp | null (when successfully finished)
Metadata ──────────────────→ object (flexible: shipping address, tracking ID, email status, etc.)

Rationale: Separates fulfillment lifecycle from order state. Enables:

  • Multi-fulfillment-type support (digital vs. physical in same order)
  • Workflow orchestration (async vs. sync)
  • Audit trail (attempts, errors, completion time)
  • Manual intervention flow (assignedTo)

4. ProductDeliveryConfig (New)

Purpose: Per-product rules for fulfillment automation and delivery strategy.

ProductId (UUID ref) ──────────→ Product (this config applies to which product?)
DeliveryType (enum) ───────────→ instant_digital, scheduled_digital, manual_review, physical_shipment, hybrid
AutoFulfill (boolean, default=true) → auto-trigger workflow on payment?
FulfillmentWorkflowId (UUID ref, nullable) ─→ Workflow (if null, use default for deliveryType)
NotificationTemplate (string, nullable) ─→ ContentData slug for email/SMS (rendered with download link)
MaxConcurrentFulfillments (int, default=10) → throttle parallel task execution
RetryPolicy (object) ────────→ flexible: maxAttempts, backoffMultiplier, etc.

Rationale: Centralizes product-level fulfillment rules. Enables:

  • Different workflows per product (custom logic, integrations)
  • Email template selection (per-product branding)
  • Throttling & retry strategy per product type
  • Easy future extensions (e.g., payment gateway routing)

Integration Points

Product ↔ DigitalAsset

  • Product.type enum already includes "download" ✅
  • DigitalAsset.productId creates 1:1 link
  • Product can have zero or one DigitalAsset

FileRecord ↔ DigitalAsset

  • DigitalAsset.fileId references FileRecord
  • FileRecord already provides: checksumSha256, virusScanStatus, storageKey, mimeType
  • DigitalAsset assumes FileRecord.virusScanStatus = "CLEAN" before grant

SalesOrder ↔ OrderFulfillmentTask

  • One OrderFulfillmentTask per fulfillable LineItem
  • Triggered when SalesOrder.status moves to "pending" (payment confirmed)
  • Created by fulfillment service: fulfillmentService.createFulfillmentTask(order, lineItem, "digital_delivery")

OrderFulfillmentTask ↔ DownloadAccess

  • On OrderFulfillmentTask.complete() with status="completed":
    • Loop through LineItems with Product.type="download"
    • Create DownloadAccess for each (Principal=order.customerId)
    • Grant ACL READ permission via ValkyrAclService.grantPermission()
  • Implemented in DigitalFulfillmentModule.execute()

DownloadAccess ↔ Spring ACL

  • Each DownloadAccess is an ACL-protected object
  • Customer (Principal) has READ permission on DownloadAccess
  • Enforced at REST endpoint level via @Secured("ROLE_CUSTOMER") + ACL check
  • Download endpoint validates: token match + expiry + limit + revocation status

ProductDeliveryConfig ↔ Workflow

  • Links Product to Workflow for fulfillment automation
  • If auto-fulfil enabled: OrderFulfillmentTask created in-progress, workflow triggered immediately
  • Workflow contains tasks: DigitalFulfillmentModule → SendDownloadEmailModule → etc.

REST Endpoint Architecture

Non-CRUD Endpoints (Hand-Written)

POST /Product/{productId}/createDigitalAsset

  • Body: { fileId, deliveryMethod, accessModel, maxDownloads, expiresAfterDays }
  • Returns: DigitalAsset (201 Created)
  • Validates: file is CLEAN, product exists, principal has ADMIN role
  • Service call: fulfillmentService.createDigitalAsset(...)

POST /Product/{productId}/generateDownloadLink

  • Body: { validityMinutes: int (optional, default 60) }
  • Returns: {"downloadLink": "https://.../DownloadAccess/{id}/file?token=<token>", "expiresAt": ..., "accessId": ...}
  • Creates ephemeral DownloadAccess record with expiry
  • For use in email/UI distribution

GET /DownloadAccess/{accessId}/file?token=<token>

  • Binary download endpoint
  • Validates: token matches, expiry not reached, limit not exceeded, not revoked
  • Side effects: increments downloadCount, updates lastDownloadedAt, decrements maxDownloadsRemaining
  • Returns: 200 with octet-stream OR 403 if access denied
  • Service call: fulfillmentService.downloadFile(accessId, token, auth)

POST /OrderFulfillmentTask/{taskId}/complete

  • Body: { status: "completed|failed|canceled", metadata: object }
  • Returns: { task: OrderFulfillmentTask, downloadAccess: DownloadAccess|null }
  • If digital_delivery + completed: creates DownloadAccess, grants ACL
  • Service call: fulfillmentService.completeFulfillmentTask(...)

GET /OrderFulfillmentTask/byLineItem/{lineItemId}

  • Convenience query: fetch fulfillment task for a specific line item
  • Returns: OrderFulfillmentTask

Codegen Strategy (THORAPI)

Step 1: OpenAPI Schema Addition

✅ Four new schemas appended to api.hbs.yaml (lines 7525+):

  • DigitalAsset, DownloadAccess, OrderFulfillmentTask, ProductDeliveryConfig
  • Non-CRUD endpoints in paths/ section

Step 2: ThorAPI Code Generation

  • Run: mvn clean install or ./vai
  • Generated outputs:
    • Models: DigitalAssetRepository, DownloadAccessRepository, OrderFulfillmentTaskRepository, ProductDeliveryConfigRepository (Spring Data)
    • Services: Auto-generated CRUD services + controllers
    • TypeScript Clients: RTK Query hooks (useGetDownloadAccessesQuery, useCompleteOrderFulfillmentTaskMutation, etc.)

Step 3: Hand-Written Business Logic

  • DigitalFulfillmentService.java: core fulfillment orchestration (not auto-generated)
  • DigitalFulfillmentModule.java: VModule for workflow execution
  • SendDownloadEmailModule.java: email delivery workflow step
  • Custom endpoints called by auto-generated controllers or custom routes

Workflow Module Implementation

DigitalFulfillmentModule (ExecModule)

Input:

{
"orderFulfillmentTaskId": "uuid",
"status": "completed|failed|canceled",
"metadata": { "fulfilledBy": "system", ... }
}

Output:

{
"success": true,
"message": "DownloadAccess granted successfully",
"downloadAccessId": "uuid"
}

Logic:

  1. Fetch OrderFulfillmentTask by ID
  2. Call fulfillmentService.completeFulfillmentTask(taskId, status, metadata)
  3. If digital_delivery + completed: returns DownloadAccess object
  4. Emit success + downloadAccessId for downstream modules

Registration: In workflow factory (e.g., OrderFulfillmentWorkflowFactory.java):

ExecModule DIGITAL_FULFILLMENT = module(
UUID.randomUUID(),
DigitalFulfillmentModule.class,
ExecModule.ModuleTypeEnum.WRITER,
40f, // order in palette
"Digital Fulfillment",
"Grant download access for completed digital product orders"
);

SendDownloadEmailModule (Future)

Input:

{
"downloadAccessId": "uuid",
"customerEmail": "user@example.com",
"templateSlug": "digital-delivery-email"
}

Logic:

  1. Fetch DownloadAccess + DigitalAsset + Product metadata
  2. Render email from ContentData template with download link
  3. Send via mail service
  4. Log event

ACL & Security Model

Object-Level Permissions

  • DownloadAccess is ACL-protected
  • When granted: ValkyrAclService.grantPermission(accessId, DownloadAccess.class, customerId, BasePermission.READ)
  • Check before download: if (!aclService.hasPermission(accessId, DownloadAccess.class, principal, BasePermission.READ)) throw AccessDeniedException

Field-Level Encryption

  • DownloadAccess.downloadToken marked with x-thorapi-secureField: true
  • Automatically encrypted at rest, decrypted in memory via AspectJ

Authentication & Authorization

  • REST endpoints annotated: @Secured("ROLE_CUSTOMER") (downloads), @Secured("ROLE_ADMIN") (create assets, configs)
  • Customer can only download if they have ACL READ on that DownloadAccess
  • Admin can create, revoke, query all accesses

Audit Trail

  • OrderFulfillmentTask.metadata stores context (fulfilling user, timestamp, etc.)
  • DownloadAccess.lastDownloadedAt records each access
  • AclEntry audit logs who granted/revoked permissions

Data Flow Diagram

┌─────────────────────────────────────────────────────────────────┐
│ DIGITAL E-BOOK FULFILLMENT │
└─────────────────────────────────────────────────────────────────┘

1. FILE UPLOAD & ASSET CREATION
┌──────────────┐ ┌──────────────┐ ┌─────────────┐
│ FileRecord │────────→│DigitalAsset │────────→│ Product │
│ (checksums, │ │(metadata) │ │(type=dl) │
│ virusScan) │ │ │ │ │
└──────────────┘ └──────────────┘ └─────────────┘

2. PRODUCT CONFIGURATION
┌──────────────────────┐
│ProductDeliveryConfig │
│(autoFulfill, wf, etc)│
└────────────┬─────────┘

v
┌──────────────────────┐
│ Workflow Engine │
│(ValkyrAI) │
└──────────────────────┘

3. ORDER PLACEMENT & FULFILLMENT
┌──────────────┐ ┌──────────────────┐ ┌────────────┐
│ SalesOrder │────────→│OrderFulfillment │────────→│DownloadAccess
│(customer) │ │Task(pending) │ │(token) │
└──────────────┘ └──────────────────┘ └────┬───────┘
│ │
v v
┌──────────────────┐ ┌──────────────┐
│Workflow Execute: │ │ ACL Perm │
│DigitalFulfill │ │ (READ grant)│
│Module │ └──────────────┘
└──────────────────┘

4. DOWNLOAD
Customer: `GET /DownloadAccess/{id}/file?token=<token>`

Backend: Validate token + expiry + limit + revocation status

Backend: Increment downloadCount, update lastDownloadedAt

Backend: Return file (200) or deny (403)

Error Handling & Edge Cases

Scenario: File Not Clean After Scan

  • createDigitalAsset() throws DigitalFulfillmentException: "File has not passed virus scan or is infected: INFECTED"
  • Response: 400 Bad Request with error message

Scenario: Download Limit Exceeded

  • downloadFile() checks: if (maxDownloadsRemaining <= 0) throw...
  • Response: 403 Forbidden with "Download limit exceeded"
  • Access remains accessible (limit not auto-revoked, manual action required)

Scenario: Access Expired

  • downloadFile() checks: if (Instant.now().isAfter(expiresAt)) throw...
  • Response: 403 Forbidden with "Access expired"

Scenario: Access Revoked (Refund)

  • Admin calls: fulfillmentService.revokeDownloadAccess(accessId, "refund")
  • Sets: revokedAt = now(), revokedReason = "refund"
  • Subsequent downloads: 403 "Access revoked: refund"

Scenario: Token Mismatch

  • Attacker tries: GET /DownloadAccess/{id}/file?token=WRONG_TOKEN
  • Response: 400 "Invalid download token"

Scenario: Multiple Concurrent Downloads

  • Same DownloadAccess token can be used concurrently (token not single-use by default)
  • Each download independently decrements limit, records timestamp
  • Race condition safe: database transaction ensures accurate counters

Testing Strategy

Unit Tests (Service Layer)

  • DigitalFulfillmentServiceTest.testCreateDigitalAsset(): validate file clean, product exists
  • DigitalFulfillmentServiceTest.testDownloadLimitEnforcement(): counter decrement, revocation
  • DigitalFulfillmentServiceTest.testGrantDownloadAccess(): ACL grant correctness

Integration Tests (E2E)

  • DigitalEbookFulfillmentE2ETest: 10-step flow from upload → download
    • Step 1: Upload file → FileRecord
    • Step 2: Create product (type=download)
    • Step 3: Create DigitalAsset
    • Step 4: ProductDeliveryConfig
    • Step 5: Place order (line item with e-book)
    • Step 6: Payment confirmed → OrderFulfillmentTask created
    • Step 7: Complete task → DownloadAccess granted
    • Step 8: Generate download link
    • Step 9: Customer downloads (token validation, count incremented)
    • Step 10: Verify limit enforcement & revocation

Workflow Tests

  • Mock ValkyrAI workflow execution
  • Verify DigitalFulfillmentModule.execute() creates DownloadAccess

Future Enhancements

Phase 2: Advanced Features

  • Scheduled Delivery: ProductDeliveryConfig.deliveryType="scheduled_digital" → deliver at specific time
  • License Key Generation: Auto-generate per-download unique license keys (for software)
  • Bandwidth Throttling: Limit concurrent downloads per customer
  • Regional Restrictions: GeoIP-based access denial
  • Download Analytics: Dashboard showing sales → fulfillment → download metrics

Phase 3: Payment Integration

  • Link to Stripe/Paddle for subscription renewal triggers
  • Auto-revoke on chargeback detection
  • Tiered access (free tier: 1 download/month, premium: unlimited)

Phase 4: Multi-file Bundles

  • Product containing multiple DigitalAssets
  • Selective download (user picks which files to access)

Deployment Checklist

  • Append schemas to api.hbs.yaml
  • Append endpoints to api.hbs.yaml
  • Run mvn clean install to trigger codegen
  • Create DigitalFulfillmentService.java ✅
  • Create DigitalFulfillmentModule.java ✅
  • Create integration tests ✅
  • Register module in workflow factory
  • Create initial ProductDeliveryConfigs for existing digital products
  • Configure email template in ContentData (digital-product-delivery-email)
  • Deploy to staging, run E2E tests
  • Deploy to production, monitor fulfillment success rate

References

  • ValkyrAI Instructions: .github/copilot-instructions.md (THORAPI rules, ExecModule pattern, ACL enforcement)
  • OpenAPI Spec: valkyrai/src/main/resources/openapi/api.hbs.yaml (schemas + endpoints)
  • Related Models: Product, FileRecord, SalesOrder, LineItem, Principal, Workflow, AclEntry
  • PR: #35 (feat(core): rc wip)

Sign-Off

  • Proposed By: GitHub Copilot (Agent)
  • Reviewed By: (Pending)
  • Approved By: (Pending)
  • Implementation Start: 2025-10-18
  • Target Completion: 2025-10-25