Skip to content

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-service fully 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:

  1. Print Service (apps/print-service) — Extracted from monolith:
  2. Print job management (CRUD, lifecycle, stuck job monitor)
  3. SimplyPrint integration (API client, polling, webhooks, reconciliation)
  4. SimplyPrint API Files upload endpoint (for GridFlock gcode — used in Part 4)
  5. BullMQ event publishing (print-job.completed, print-job.failed, etc.)
  6. Internal API endpoints for service-to-service calls
  7. Shipping Service (apps/shipping-service) — Extracted from monolith:
  8. Shipment management (CRUD, lifecycle)
  9. Sendcloud integration (API client, webhooks, reconciliation)
  10. BullMQ event publishing (shipment.created, shipment.status-changed)
  11. Internal API endpoints
  12. OrderServiceClient for fetching order details
  13. Event Flow Rewiring — All existing workflows work across services:
  14. Order created → print jobs created (via HTTP to Print Service)
  15. Print job completed → order status updated (via BullMQ event)
  16. Order ready → shipment created (via HTTP to Shipping Service)
  17. Shipment created → fulfillment completed (via BullMQ event)
  18. Cancellation flows across services

📌 Prerequisites (Parts 1–2 Completed)

Verify these before starting:

  • libs/service-common available (event bus, internal auth, service clients)
  • Redis running via Docker Compose
  • apps/gateway running on port 3000, proxying requests
  • apps/order-service running 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)FulfillmentServiceSendcloudService.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:

  1. Persistence — events stored in Redis until processed
  2. Automatic retry — failed handlers retried up to 3 times with exponential backoff
  3. Dead letter retention — events that fail all retries kept for debugging
  4. Idempotent handlers — handlers MUST check if already processed
  5. Startup recovery — workers pick up unprocessed events on restart
  6. Exactly-once per service — only one worker instance claims each event

🧪 Testing Requirements

  • 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-service succeeds
  • pnpm nx build shipping-service succeeds
  • pnpm nx run-many -t lint --all passes
  • 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, or eslint-disable
  • Set connection_limit=3 in 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