Skip to content

Specification: Phase 2 — Multi-Template Printing

This phase implements the core multi-template printing capability: grouping items by template size, rendering groups in parallel with batch limiting, and returning a composite response with one PDF per group.

Requirements: Phase 2 Requirements Verification: Phase 2 Verification Analysis: Gap Analysis — Gaps F-1, F-2, F-3, F-4, F-5 Depends on: Phase 1 (bug fixes) must be complete — the notes mapping fix and print status changes are prerequisites.

Architecture Decision: Responsibility Split

Section titled “Architecture Decision: Responsibility Split”

The parallelism, batch splitting, column packing, and result composition are PdfRenderService responsibilities, not calling-module responsibilities. This avoids duplicating orchestration logic in both the item and kanban modules.

ConcernOwner
Transform domain objects → List<JsonElement>Calling service (ItemPrintingService, PrintLifecycleImpl)
Group items by templateCalling service
Validate per-request limitCalling service
Pack JsonElements into Grid (columns)PdfRenderService
Split groups exceeding per-call limitPdfRenderService
Parallel Documint execution with concurrency limitPdfRenderService
Compose results into CompositeRenderResultPdfRenderService

Files:

  • operations/src/main/kotlin/cards/arda/operations/shopaccess/pdfrender/business/RenderResult.kt

Change: Add composite response types in the pdfRender module (shared by item and kanban):

@Serializable
data class CompositeRenderResult(
@Serializable(with = UUIDSerializer::class)
val job: UUID,
val asOF: TimeCoordinates,
val results: List<GroupRenderResult>
)
@Serializable
data class GroupRenderResult(
val templateId: String,
val description: String,
val itemCount: Int,
@Serializable(with = URLSerializer::class)
val url: URL? = null,
val error: String? = null
)

The existing RenderResult is preserved for internal use (single Documint call result). CompositeRenderResult is the API response shape for all print endpoints (single-item and bulk).

Tests: T-P2-006, T-P2-007.

Requirement: REQ-P2-003


Files:

  • operations/src/main/resources/shop-access/pdf-render/application.conf
  • operations/src/main/kotlin/cards/arda/operations/shopaccess/pdfrender/Module.kt

Change: Add batch and concurrency configuration to the PdfRenderService module config:

extras {
printing {
maxItemsPerDocumintRequest = 40
maxParallelRenders = 3
}
documint: {
useLive: false
apiKey: "from-build-system"
}
}

The per-request limit (maxItemsPerRequest) remains in the calling module configs (item, kanban) since it is a user-facing constraint that may differ per module:

# In reference/item/application.conf and resources/kanban/application.conf
extras {
printing {
maxItemsPerRequest = 200
}
}

Load these values in the respective Module.kt files and pass to services.

Tests: T-P2-013.

Requirement: REQ-P2-006


Task 3: Extend PdfRenderService with renderGroups()

Section titled “Task 3: Extend PdfRenderService with renderGroups()”

Files:

  • operations/src/main/kotlin/cards/arda/operations/shopaccess/pdfrender/service/PdfRenderService.kt

Change: Add a new method to the PdfRenderService interface for multi-group rendering:

interface PdfRenderService {
// Existing single-job method (unchanged)
suspend fun render(job: RenderJob, author: String, live: Boolean?): Result<RenderResult>
// New multi-group method
suspend fun renderGroups(
groups: List<Pair<PrintingTemplateConfiguration, List<JsonElement>>>,
author: String,
live: Boolean?
): Result<CompositeRenderResult>
}

Implementation responsibilities of renderGroups():

  1. For each group (templateConfig, elements):
    • If elements.size > maxItemsPerDocumintRequest, split into sub-batches
    • Pack each batch into a Grid using templateConfig.columns
    • Create a RenderJob per batch
  2. Execute all RenderJobs via DocumintProxy.render() in parallel using kotlinx.coroutines.sync.Semaphore(maxParallelRenders)
  3. Map each result to a GroupRenderResult (URL on success, error message on failure)
  4. Compose into CompositeRenderResult with a generated job UUID and current timestamp

The existing render(RenderJob) method remains for backward compatibility and is used internally by renderGroups().

Tests: T-P2-001 through T-P2-005, T-P2-008 through T-P2-012.

Requirements: REQ-P2-001, REQ-P2-002, REQ-P2-004, REQ-P2-005


Files:

  • operations/src/main/kotlin/cards/arda/operations/reference/item/service/ItemPrintingService.kt

Change: The calling service’s responsibility is now limited to:

  1. Validate input IDs (dedup, check non-empty)
  2. Fetch items from ItemUniverse
  3. Check total item count against maxItemsPerRequest — reject early if exceeded
  4. For each item: resolve template via templateSelector, render to ItemPrintInfo JSON via ItemPrinter
  5. Group (PrintingTemplateConfiguration, JsonElement) pairs by template
  6. Call pdfRenderService.renderGroups(groups, author, live)
  7. Return the CompositeRenderResult

Removed: Single-template enforcement, RenderJob construction, Grid.fromElements() — all now in PdfRenderService.

Tests: T-P2-001, T-P2-002, T-P2-011, T-P2-012.

Requirements: REQ-P2-001, REQ-P2-005


Task 5: Simplify PrintLifecycleImpl for cards

Section titled “Task 5: Simplify PrintLifecycleImpl for cards”

Files:

  • operations/src/main/kotlin/cards/arda/operations/resources/kanban/service/PrintLifecycleImpl.kt

Change: Apply the same simplification as Task 4:

  1. Fetch card details
  2. Check total count against maxItemsPerRequest
  3. For each card: resolve template via cardSizeTemplate(), render to KanbanCardPrintInfo JSON via KanbanCardPrinter
  4. Group by template
  5. Call pdfRenderService.renderGroups(groups, author, live)
  6. After response: update print status only for cards in successful groups (match cards to groups by template)

Key difference from items: Cards have print status side effects. The implementation must track which cards belong to which group so status updates are correctly scoped to successful groups.

Tests: T-P2-003, T-P2-016.

Requirements: REQ-P2-001, REQ-P2-008


Task 6: Update API endpoint response types

Section titled “Task 6: Update API endpoint response types”

Files:

  • operations/src/main/kotlin/cards/arda/operations/reference/item/api/rest/ItemEndpoint.kt
  • operations/src/main/kotlin/cards/arda/operations/resources/kanban/api/rest/KanbanCardEndpoint.kt

Change: Update the print endpoint response types from RenderResult to CompositeRenderResult. All three print endpoints (labels, breadcrumbs, cards) return the composite shape — including single-item requests.

Tests: T-P2-006, T-P2-007, T-P2-008.

Requirement: REQ-P2-003


Task 7: Frontend composite response handling

Section titled “Task 7: Frontend composite response handling”

Files:

  • arda-frontend-app/src/app/items/page.tsxhandlePrintSelectedCards, handlePrintSelectedLabels, handlePrintSelectedBreadcrumbs

Change: Update all three handlers to process the new composite response:

  1. Parse data.data.results as an array of GroupRenderResult
  2. For each result with a non-null url, call window.open(url, '_blank', 'noopener,noreferrer')
  3. For each result with a non-null error, show toast.error() with the template description and error message
  4. Remove the frontend card grouping logic (cardsByTemplate map) — the backend now handles this
  5. Remove the label/breadcrumb same-template error check — the backend now handles mixed sizes

Tests: T-P2-014, T-P2-015.

Requirement: REQ-P2-007


Update the following documents to reflect the multi-template architecture:

  1. PDF Render Module (current-system/functional/shop-access/pdf-render-module.md) — Add renderGroups() method to the PdfRenderService interface. Add CompositeRenderResult and GroupRenderResult to the data model. Add a sequence diagram for multi-group rendering. Update the block diagram.
  2. Item Module (current-system/functional/reference-data/item/) — Update the printing sequence diagram to show the simplified calling flow (group → pass to PdfRenderService). Update the ItemPrintingService interface.

Tests: T-P2-019, T-P2-020.

Requirement: REQ-P2-010


Run full test suites for both repositories:

  • operations: make build
  • arda-frontend-app: Jest + E2E

Tests: T-P2-017, T-P2-018.

Requirement: REQ-P2-009


#QuestionOptionsRecommendationDecision
1Where should CompositeRenderResult be defined?A) In pdfRender module (shared type) B) In item/kanban modules (local types)A — it’s a response type used by both item and kanban printingAgreed
2Should single-item prints also use the composite response, or keep RenderResult?A) Composite for all B) Composite for bulk only, RenderResult for singleA — consistency; frontend handles one shapeAgreed
3Should the frontend card grouping logic be removed entirely or kept as a fallback?A) Remove — backend handles grouping B) Keep as fallback for backward compatibilityA — clean cut; backend is the authorityAgreed
4How should sub-batch PDFs be presented to the user?A) One tab per sub-batch (may produce many tabs) B) Concatenate sub-batch PDFs server-sideA — simpler; concatenation adds complexity and Documint dependencyAgreed, more sophisticated rendering is possible in the future.

Before proceeding to implementation, confirm:

  • All open questions resolved
  • Requirements approved
  • Specification reviewed
  • Phase 1 complete and verified

Copyright: (c) Arda Systems 2025-2026, All rights reserved