AI Prompt: Part 3 — Print Service + Shipping Service + Event Flow Rewiring¶
Series: Forma3D.Connect Microservice Decomposition + GridFlock STL Pipeline (Part 3 of 6) Purpose: Extract the Print Service and Shipping Service from the monolith and rewire all event flows to work across service boundaries via BullMQ Estimated Effort: 20–26 hours Prerequisites: Parts 1–2 completed (shared libs, Gateway, Order Service) Output:
apps/print-service+apps/shipping-servicefully extracted, all existing event flows working across services via BullMQ + HTTP Status: 🚧 TODO Previous Part: Part 2 — API Gateway + Order Service Next Part: Part 4 — GridFlock Service + Slicer Container
🎯 Mission¶
Extract the Print Service and Shipping Service from the monolith, and rewire all existing event flows to work across service boundaries. After this part, the full order lifecycle works end-to-end through microservices: order creation → print jobs → fulfillment → shipping.
What this part delivers:
- Print Service (
apps/print-service) — Extracted from monolith: - Print job management (CRUD, lifecycle, stuck job monitor)
- SimplyPrint integration (API client, polling, webhooks, reconciliation)
- SimplyPrint API Files upload endpoint (for GridFlock gcode — used in Part 4)
- BullMQ event publishing (print-job.completed, print-job.failed, etc.)
- Internal API endpoints for service-to-service calls
- Shipping Service (
apps/shipping-service) — Extracted from monolith: - Shipment management (CRUD, lifecycle)
- Sendcloud integration (API client, webhooks, reconciliation)
- BullMQ event publishing (shipment.created, shipment.status-changed)
- Internal API endpoints
- OrderServiceClient for fetching order details
- Event Flow Rewiring — All existing workflows work across services:
- Order created → print jobs created (via HTTP to Print Service)
- Print job completed → order status updated (via BullMQ event)
- Order ready → shipment created (via HTTP to Shipping Service)
- Shipment created → fulfillment completed (via BullMQ event)
- Cancellation flows across services
📌 Prerequisites (Parts 1–2 Completed)¶
Verify these before starting:
-
libs/service-commonavailable (event bus, internal auth, service clients) - Redis running via Docker Compose
-
apps/gatewayrunning on port 3000, proxying requests -
apps/order-servicerunning on port 3001, all existing endpoints working - BullMQ event bus integration in Order Service (publishing + subscribing)
- PrintServiceClient and ShippingServiceClient stubs in Order Service
📁 Files to Create¶
apps/print-service¶
apps/print-service/
├── src/
│ ├── main.ts # HTTP server + BullMQ event workers
│ ├── app/
│ │ └── app.module.ts
│ ├── print-jobs/ # From apps/api/src/print-jobs/
│ ├── simplyprint/ # From apps/api/src/simplyprint/
│ ├── internal/
│ │ ├── internal.module.ts
│ │ ├── internal.controller.ts # /internal/print-jobs/* + /internal/simplyprint/*
│ │ └── internal-auth.guard.ts
│ ├── events/
│ │ ├── events.module.ts
│ │ ├── event-publisher.service.ts # Publishes print-job.* events
│ │ └── event-subscriber.service.ts # Subscribes to order.created
│ ├── config/
│ ├── database/
│ ├── tenancy/
│ ├── common/
│ └── observability/
├── Dockerfile
├── project.json
└── jest.config.ts
apps/shipping-service¶
apps/shipping-service/
├── src/
│ ├── main.ts # HTTP server + BullMQ event workers
│ ├── app/
│ │ └── app.module.ts
│ ├── shipments/ # From apps/api/src/shipments/
│ ├── sendcloud/ # From apps/api/src/sendcloud/
│ ├── internal/
│ │ ├── internal.module.ts
│ │ ├── internal.controller.ts # /internal/shipments/* endpoints
│ │ └── internal-auth.guard.ts
│ ├── events/
│ │ ├── events.module.ts
│ │ ├── event-publisher.service.ts # Publishes shipment.* events
│ │ └── event-subscriber.service.ts # Subscribes to order.ready-for-fulfillment
│ ├── service-clients/
│ │ └── order-service.client.ts # HTTP client to Order Service
│ ├── config/
│ ├── database/
│ ├── tenancy/
│ ├── common/
│ └── observability/
├── Dockerfile
├── project.json
└── jest.config.ts
🔧 Implementation¶
Phase 4: Print Service (8–10 hours)¶
Priority: Critical | Impact: High | Dependencies: Parts 1–2
4.1 Create Print Service App¶
pnpm nx generate @nx/nest:application print-service --directory=apps/print-service
4.2 Move Modules¶
Move from apps/api/src/:
| Module | Notes |
|---|---|
print-jobs/ |
Print job CRUD, lifecycle, stuck job monitor |
simplyprint/ |
SimplyPrint API client, polling, webhooks, reconciliation |
4.3 Internal API Endpoints¶
// apps/print-service/src/internal/internal.controller.ts
@Controller('internal')
@UseGuards(InternalAuthGuard)
export class InternalController {
@Post('print-jobs')
async createPrintJobs(@Body() data: CreatePrintJobsInternalDto) { ... }
@Get('print-jobs/order/:orderId')
async getJobsByOrderId(@Param('orderId') orderId: string, @Query('tenantId') tenantId: string) { ... }
@Post('print-jobs/order/:orderId/cancel')
async cancelJobsForOrder(@Param('orderId') orderId: string, @Body('tenantId') tenantId: string) { ... }
@Get('print-jobs/order/:orderId/status-summary')
async getJobStatusSummary(@Param('orderId') orderId: string, @Query('tenantId') tenantId: string) { ... }
/**
* Upload a gcode file to SimplyPrint via their API Files endpoint.
* Called by GridFlock Service during the buffer-based pipeline (Part 4).
*
* Flow:
* 1. Receive base64-encoded gcode buffer + filename
* 2. Decode base64 → Buffer
* 3. Upload to SimplyPrint API Files: POST https://files.simplyprint.io/{companyId}/files/Upload
* 4. Return SimplyPrint API File ID
*
* Note: SimplyPrint API Files require the Print Farm plan.
*/
@Post('simplyprint/upload')
async uploadFileToSimplyPrint(@Body() data: UploadFileToSimplyPrintDto): Promise<{ simplyPrintFileId: string; filename: string }> {
const buffer = Buffer.from(data.fileBase64, 'base64');
return this.simplyPrintService.uploadApiFile(data.tenantId, data.filename, buffer);
}
}
4.4 SimplyPrint API Files Integration¶
The Print Service is the only service that interacts with the SimplyPrint API:
// apps/print-service/src/simplyprint/simplyprint-api-files.service.ts
@Injectable()
export class SimplyPrintApiFilesService {
/**
* Upload a file to SimplyPrint via their API Files endpoint.
* Endpoint: POST https://files.simplyprint.io/{companyId}/files/Upload
* Auth: X-API-KEY header
* Body: multipart/form-data with `file` field
* Response: { status: true, file: { id: string, name: string, size: number } }
*/
async uploadApiFile(tenantId: string, filename: string, buffer: Buffer): Promise<{ simplyPrintFileId: string; filename: string }> {
const tenantConfig = await this.getTenantSimplyPrintConfig(tenantId);
const formData = new FormData();
formData.append('file', new Blob([buffer]), filename);
const response = await this.httpService.axiosRef.post(
`https://files.simplyprint.io/${tenantConfig.companyId}/files/Upload`,
formData,
{
headers: {
'X-API-KEY': tenantConfig.apiKey,
...formData.getHeaders?.() ?? {},
},
},
);
if (!response.data.status) {
throw new Error(`SimplyPrint upload failed: ${response.data.message}`);
}
return {
simplyPrintFileId: response.data.file.id,
filename: response.data.file.name,
};
}
}
Important: SimplyPrint API Files require the Print Farm plan.
4.5 Event Publishing¶
When a print job changes status, publish to BullMQ:
await this.eventBus.publish({
eventType: SERVICE_EVENTS.PRINT_JOB_COMPLETED,
tenantId,
printJobId: job.id,
orderId: job.orderId,
lineItemId: job.lineItemId,
timestamp: new Date().toISOString(),
});
Phase 5: Shipping Service (6–8 hours)¶
Priority: Critical | Impact: High | Dependencies: Parts 1–2
5.1 Create Shipping Service App¶
pnpm nx generate @nx/nest:application shipping-service --directory=apps/shipping-service
5.2 Move Modules¶
Move from apps/api/src/:
| Module | Notes |
|---|---|
shipments/ |
Shipment CRUD and lifecycle |
sendcloud/ |
Sendcloud API client, webhooks, reconciliation |
5.3 Internal API Endpoints¶
@Controller('internal')
@UseGuards(InternalAuthGuard)
export class InternalController {
@Post('shipments')
async createShipment(@Body() data: CreateShipmentInternalDto) { ... }
@Get('shipments/order/:orderId')
async getShipmentsByOrderId(...) { ... }
@Post('shipments/:id/cancel')
async cancelShipment(...) { ... }
}
5.4 Event Publishing¶
await this.eventBus.publish({
eventType: SERVICE_EVENTS.SHIPMENT_CREATED,
tenantId,
shipmentId: shipment.id,
orderId: shipment.orderId,
trackingNumber: shipment.trackingNumber,
trackingUrl: shipment.trackingUrl,
carrier: shipment.carrier,
timestamp: new Date().toISOString(),
});
5.5 Order Service Client¶
The Shipping Service needs order data for creating Sendcloud parcels:
const order = await this.orderServiceClient.getOrderById(tenantId, orderId);
Event Flow Rewiring (6–8 hours)¶
Priority: Critical | Impact: Very High | Dependencies: Phases 4–5 above
This phase ensures all existing event-driven workflows work across service boundaries.
Flow 1: Order Created → Print Job Creation¶
Current flow (monolith):
Shopify webhook → OrdersService.create() → EventEmitter.emit(ORDER_CREATED)
→ OrchestrationService.handleOrderCreated()
→ PrintJobsService.createJobsForOrder()
New flow (microservices):
Shopify webhook → Order Service: OrdersService.create()
→ Order Service: OrchestrationService.handleOrderCreated()
→ HTTP call: PrintServiceClient.createPrintJobs()
→ Print Service: creates print jobs in DB
Order creation + orchestration stays in Order Service. Print job creation happens via HTTP call to Print Service.
Flow 2: Print Job Completed → Order Status Update¶
Current flow: EventEmitter.emit(PRINT_JOB_COMPLETED) → OrchestrationService.handlePrintJobCompleted()
New flow:
Print Service: job completes → EventBus.publish(PRINT_JOB_COMPLETED)
→ BullMQ queue "print-job.completed"
→ Order Service worker claims job → OrchestrationEventSubscriber receives event
→ OrchestrationService.handlePrintJobCompleted()
→ recalculates order status
Flow 3: Order Ready → Shipment + Fulfillment¶
Current flow: EventEmitter.emit(ORDER_READY_FOR_FULFILLMENT) → FulfillmentService → SendcloudService.createParcel()
New flow:
Order Service: order completes
→ FulfillmentService checks if shipping enabled
→ If YES: HTTP call to ShippingServiceClient.createShipment()
→ Shipping Service creates shipment + Sendcloud parcel
→ EventBus.publish(SHIPMENT_CREATED)
→ Order Service: FulfillmentService.handleShipmentCreated()
→ Creates Shopify fulfillment with tracking
→ If NO: Creates Shopify fulfillment directly
Event Reliability¶
BullMQ event queues provide built-in reliability:
- Persistence — events stored in Redis until processed
- Automatic retry — failed handlers retried up to 3 times with exponential backoff
- Dead letter retention — events that fail all retries kept for debugging
- Idempotent handlers — handlers MUST check if already processed
- Startup recovery — workers pick up unprocessed events on restart
- Exactly-once per service — only one worker instance claims each event
🧪 Testing Requirements¶
Print Service Tests¶
- Controllers — correct HTTP responses, input validation
- Services — business logic, error handling
- Repositories — tenant isolation
- Event publishing — correct events published on status changes
- Internal API — create jobs, query by order, cancel, upload file
- SimplyPrint API Files — upload integration tests (mock SimplyPrint API)
- All existing print-job tests still pass
Shipping Service Tests¶
- Controllers — correct HTTP responses
- Services — business logic
- Repositories — tenant isolation
- Event publishing — shipment events published correctly
- Internal API — create shipment, query by order, cancel
- OrderServiceClient — correctly fetches order details
- All existing shipment/sendcloud tests still pass
Event Flow Integration Tests¶
- Order created → print jobs created in Print Service via HTTP
- Print job completed → order status updated in Order Service via BullMQ event
- Order ready for fulfillment → shipment created in Shipping Service via HTTP
- Shipment created → fulfillment completed in Order Service via BullMQ event
- Cancellation flows work across services
- Idempotent handlers: duplicate events don't cause duplicate processing
✅ Validation Checklist¶
Build & Lint¶
-
pnpm nx build print-servicesucceeds -
pnpm nx build shipping-servicesucceeds -
pnpm nx run-many -t lint --allpasses
Print Service¶
- Runs on port 3002
- Print job endpoints accessible through gateway
- SimplyPrint webhooks routed correctly
- Internal API protected by InternalAuthGuard
- SimplyPrint API Files upload endpoint works
- BullMQ events published on job status changes
Shipping Service¶
- Runs on port 3003
- Shipment endpoints accessible through gateway
- Sendcloud webhooks routed correctly
- Internal API protected by InternalAuthGuard
- OrderServiceClient correctly fetches order data
- BullMQ events published on shipment changes
Event Flows (Full Order Lifecycle)¶
- Order created → print jobs created in Print Service via HTTP
- Print job completed → order status updated via BullMQ event
- Order ready → shipment created in Shipping Service via HTTP
- Shipment created → fulfillment completed via BullMQ event
- Cancellation flows work across services
- All event handlers are idempotent
API Parity¶
- All existing print-job endpoints return same responses through gateway
- All existing shipment endpoints return same responses through gateway
- SimplyPrint webhooks still work
- Sendcloud webhooks still work
🚫 Constraints¶
- All existing API endpoints must continue working identically
- Internal endpoints NOT exposed through gateway
- All event handlers MUST be idempotent
- SimplyPrint is the ONLY service that talks to the SimplyPrint API (via Print Service)
- Sendcloud is the ONLY service that talks to the Sendcloud API (via Shipping Service)
- No
any,ts-ignore, oreslint-disable - Set
connection_limit=3in Prisma datasource URL
📚 Key References¶
- SimplyPrint API: https://apidocs.simplyprint.io/
- SimplyPrint API Files: https://apidocs.simplyprint.io/#api-files
- Current print-jobs module:
apps/api/src/print-jobs/ - Current simplyprint module:
apps/api/src/simplyprint/ - Current shipments module:
apps/api/src/shipments/ - Current sendcloud module:
apps/api/src/sendcloud/ - Current orchestration events:
apps/api/src/orchestration/
END OF PART 3
Previous: Part 2 — API Gateway + Order Service Next: Part 4 — GridFlock Service + Slicer Container