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.
| Concern | Owner |
|---|---|
Transform domain objects → List<JsonElement> | Calling service (ItemPrintingService, PrintLifecycleImpl) |
| Group items by template | Calling service |
| Validate per-request limit | Calling service |
| Pack JsonElements into Grid (columns) | PdfRenderService |
| Split groups exceeding per-call limit | PdfRenderService |
| Parallel Documint execution with concurrency limit | PdfRenderService |
Compose results into CompositeRenderResult | PdfRenderService |
Task 1: Define composite response type
Section titled “Task 1: Define composite response type”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):
@Serializabledata class CompositeRenderResult( @Serializable(with = UUIDSerializer::class) val job: UUID, val asOF: TimeCoordinates, val results: List<GroupRenderResult>)
@Serializabledata 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
Task 2: Add printing configuration
Section titled “Task 2: Add printing configuration”Files:
operations/src/main/resources/shop-access/pdf-render/application.confoperations/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.confextras { 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():
- For each group
(templateConfig, elements):- If
elements.size > maxItemsPerDocumintRequest, split into sub-batches - Pack each batch into a
GridusingtemplateConfig.columns - Create a
RenderJobper batch
- If
- Execute all
RenderJobs viaDocumintProxy.render()in parallel usingkotlinx.coroutines.sync.Semaphore(maxParallelRenders) - Map each result to a
GroupRenderResult(URL on success, error message on failure) - Compose into
CompositeRenderResultwith 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
Task 4: Simplify ItemPrintingService
Section titled “Task 4: Simplify ItemPrintingService”Files:
operations/src/main/kotlin/cards/arda/operations/reference/item/service/ItemPrintingService.kt
Change: The calling service’s responsibility is now limited to:
- Validate input IDs (dedup, check non-empty)
- Fetch items from
ItemUniverse - Check total item count against
maxItemsPerRequest— reject early if exceeded - For each item: resolve template via
templateSelector, render toItemPrintInfoJSON viaItemPrinter - Group
(PrintingTemplateConfiguration, JsonElement)pairs by template - Call
pdfRenderService.renderGroups(groups, author, live) - 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:
- Fetch card details
- Check total count against
maxItemsPerRequest - For each card: resolve template via
cardSizeTemplate(), render toKanbanCardPrintInfoJSON viaKanbanCardPrinter - Group by template
- Call
pdfRenderService.renderGroups(groups, author, live) - 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.ktoperations/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.tsx—handlePrintSelectedCards,handlePrintSelectedLabels,handlePrintSelectedBreadcrumbs
Change: Update all three handlers to process the new composite response:
- Parse
data.data.resultsas an array ofGroupRenderResult - For each result with a non-null
url, callwindow.open(url, '_blank', 'noopener,noreferrer') - For each result with a non-null
error, showtoast.error()with the template description and error message - Remove the frontend card grouping logic (
cardsByTemplatemap) — the backend now handles this - 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
Task 8: Update design documentation
Section titled “Task 8: Update design documentation”Update the following documents to reflect the multi-template architecture:
- PDF Render Module (
current-system/functional/shop-access/pdf-render-module.md) — AddrenderGroups()method to the PdfRenderService interface. AddCompositeRenderResultandGroupRenderResultto the data model. Add a sequence diagram for multi-group rendering. Update the block diagram. - Item Module (
current-system/functional/reference-data/item/) — Update the printing sequence diagram to show the simplified calling flow (group → pass to PdfRenderService). Update theItemPrintingServiceinterface.
Tests: T-P2-019, T-P2-020.
Requirement: REQ-P2-010
Task 9: Regression tests
Section titled “Task 9: Regression tests”Run full test suites for both repositories:
operations:make buildarda-frontend-app: Jest + E2E
Tests: T-P2-017, T-P2-018.
Requirement: REQ-P2-009
Open Questions and Decisions
Section titled “Open Questions and Decisions”| # | Question | Options | Recommendation | Decision |
|---|---|---|---|---|
| 1 | Where 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 printing | Agreed |
| 2 | Should single-item prints also use the composite response, or keep RenderResult? | A) Composite for all B) Composite for bulk only, RenderResult for single | A — consistency; frontend handles one shape | Agreed |
| 3 | Should the frontend card grouping logic be removed entirely or kept as a fallback? | A) Remove — backend handles grouping B) Keep as fallback for backward compatibility | A — clean cut; backend is the authority | Agreed |
| 4 | How 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-side | A — simpler; concatenation adds complexity and Documint dependency | Agreed, more sophisticated rendering is possible in the future. |
STOP: Review Gate
Section titled “STOP: Review Gate”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
Copyright: © Arda Systems 2025-2026, All rights reserved