Project

On-Demand 3D Printer Platform

API and web application for intake, quoting, queue management, and fulfillment across custom print requests.

active

Started January 12, 2026

Updated June 12, 2026

TypeScriptAPIAngular3D Printing

Overview

This project is meant to turn a pile of manual print-request coordination into a usable system. The API handles request intake and print-job state. The web app handles quoting, operations visibility, and customer communication.

Current milestone

Build a dependable order-to-queue path that keeps pricing, material selection, and production status aligned.

Roadmap

  • Define the request and quote lifecycle clearly
  • Add authenticated operator workflows for reviewing jobs
  • Track printer capacity and queue contention
  • Generate customer-visible status updates
  • Add webhook-driven notifications for state changes

Recent progress

2026-06-12 · Use Slant3D V2 draft orders for cart shipping estimates (#154)

/cart/shipping now estimates shipping via Slant3D V2 draft orders instead of the V1 order/estimate flow. The route builds a V2 order payload from cart items plus the authenticated user’s shipping profile, while keeping the existing caller-facing response shape.

  • Integration change

    • Replace V1 order/estimate with V2 POST /orders
    • Stop short of any follow-up POST /orders/{publicOrderId} call
    • Normalize V2 estimate responses back to { shippingCost }
  • Order payload construction

    • Source print items from cart rows joined to product metadata
    • Require products.publicFileServiceId per item
    • Use cart filamentId when present; otherwise fall back to PLA Black
    • Carry shipping/billing address data from the authenticated user profile
{
  shippingAddress: { ...profileAddress },
  billingAddress: { ...profileAddress },
  orderItems: [
    {
      publicFileServiceId,
      filamentId,
      quantity,
    },
  ],
}

Impact: - API behavior

  • Existing /cart/shipping callers still receive { shippingCost }

  • Missing publicFileServiceId now returns a clear 400-style response

  • Slant3D draft-order failures now return a clear upstream failure response with status/details

  • Operationally

    • Shipping estimation now depends on V2 draft-order semantics and publicFileServiceId being populated on products

Context: paths src, test

2026-06-12 · Add cart filamentId support for Slant3D V2 fulfillment (#153)

Cart items now carry an optional Slant3D V2 filamentId while preserving existing color and filamentType behavior. The cart API also normalizes missing filament IDs to the default PLA Black filament so legacy and new callers behave consistently.

  • Schema + persistence

    • Added cart-level filamentId storage.
    • Added a D1 migration for the new filament_id column.
    • Reused a shared PLA Black default filament ID constant.
  • API behavior

    • POST /cart/add accepts optional filamentId and validates it as a UUID.
    • Missing filamentId is defaulted to PLA Black.
    • Cart item reads include filamentId.
    • Existing cart matching/backcompat still respects color and filamentType; legacy rows without filamentId are treated as default PLA Black.
  • Coverage

    • Added focused cart tests for explicit filament preservation, UUID validation, and default filament behavior.
{
  cartId,
  skuNumber,
  quantity,
  color,
  filamentType,
  filamentId: '76fe1f79-3f1e-43e4-b8f4-61159de5b93c' // defaulted when omitted
}

Impact: Cart callers can now send and retrieve Slant3D filament public IDs for V2 fulfillment. Callers that only use color and filamentType continue to work unchanged, and carts without a supplied filament ID resolve to PLA Black.

Context: paths drizzle, src, test

2026-06-12 · Stop /v2/upload from self-fetching protected V2 routes (#152)

/v2/upload was routing through protected local Worker endpoints for presign, confirm, and estimate, which broke the authenticated upload flow unless those internal self-fetches succeeded. This change moves the Slant3D V2 file operations into shared helpers so /v2/upload calls Slant directly while the public helper endpoints keep their existing response contracts.

  • Shared Slant3D V2 file helpers

    • Extract direct-upload, confirm-upload, and estimate calls into src/lib/slant3d-v2-files.ts
    • Centralize request construction and error parsing for the V2 file workflow
    • parseResponseDetails tries response.json() first, then falls back to text() + JSON.parse to handle varied Slant3D error response shapes
  • /v2/upload flow

    • Replace internal fetches to /v2/presigned-upload, /v2/confirm, and /v2/estimate
    • Preserve current STL validation behavior and /v2/upload response shape
    • Keep the same default estimate behavior for PLA BLACK / quantity 1
  • Protected route compatibility

    • Rewire /v2/presigned-upload, /v2/confirm, and /v2/estimate to use the same shared helpers
    • Add malformed-response validation in each route handler (missing total, presignedUrl/key, or publicFileServiceId/name/fileURL) to preserve 500 error contracts
    • Adopt estimate response normalization from main, filling in fallback values for publicFileServiceId, quantity, filamentId, and estimatedCost
    • Preserve their public JSON contracts instead of introducing a second code path
const presigned = await createSlant3DDirectUpload(c.env, {
  name: fileName.replace(/\.stl$/i, ''),
  ownerId: userId?.toString() || 'anonymous',
});

const confirmed = await confirmSlant3DUpload(c.env, presigned.filePlaceholder);

const estimate = await estimateSlant3DFile(c.env, confirmed.publicFileServiceId, {
  filamentId: defaultFilamentId,
  quantity: 1,
});

Impact: - /v2/upload no longer depends on unauthenticated self-fetches to protected local routes

  • Authenticated uploads continue returning the same payload, but now complete through direct Slant3D helper calls
  • /v2/estimate response now includes normalized fields (estimatedCost, fallback quantity, filamentId, publicFileServiceId) for more consistent downstream consumption
  • Coverage now explicitly includes invalid type, empty file, oversized file, Slant failures, success, and the absence of local protected endpoint self-fetching

Context: paths src, test

2026-06-12 · Align Slant3D V2 file routes with live file API contract (#151)

Aligned the Slant3D V2 file flow with the live contract used by the printer routes. /v2/presigned-upload, /v2/confirm, and /v2/estimate now follow the live file endpoints and treat estimate totals as data.total.

  • Route contract alignment

    • kept direct upload on POST /files/direct-upload
    • kept confirm on POST /files/confirm-upload
    • kept estimate on POST /files/{publicFileId}/estimate
  • Response shape normalization

    • validated successful Slant responses before using them
    • normalized local estimate responses from Slant data.total
    • preserved local estimatedCost as a compatibility alias for existing callers
  • Failure handling

    • return explicit errors when Slant returns malformed successful payloads
    • tightened add-product estimate parsing to require the live estimate shape instead of falling back across legacy fields
const total = estimateResponse.data.total;

return {
  success: true,
  data: {
    total,
    estimatedCost: total, // compatibility
  },
};

Impact: Users and operators get more predictable failures when Slant returns malformed payloads instead of partial/implicit parsing. Estimate-backed flows now consistently price from the live Slant V2 total field while keeping existing local response consumers compatible.

Context: paths src, test

2026-06-12 · Remove duplicate mounted Stripe webhook route (#156)

Removed the stale duplicate Stripe webhook route so the mounted app has a single authoritative /webhook/stripe implementation in src/routes/payments.ts. This cleanup also removes the obsolete TODO-only handler path and adds a regression check around the mounted webhook behavior.

  • Route ownership

    • Removed the standalone src/routes/webhooks.ts implementation.
    • Stopped mounting the duplicate webhook router from src/index.ts.
    • Kept src/routes/payments.ts as the sole mounted owner of /webhook/stripe.
  • Behavior guardrail

    • Added a focused payments-route test covering an unhandled Stripe event to assert the app resolves through the payments webhook handler, not the deleted stale route.
.route('/', paymentsRouter)
.route('/', shoppingCart);

Impact: - Stripe webhooks now resolve through one mounted handler only, eliminating ambiguity in request handling and OpenAPI ownership.

  • Operators should configure Stripe against the existing /webhook/stripe endpoint owned by payments.

Context: paths src, test

2026-06-11 · Remove legacy PayPal payment surface (#150)

  • API surface

    • Removed POST /paypal from payments routing.
    • Deleted the PayPal access helper and removed PayPal response types.
  • Dependencies / test scaffolding

    • Removed @paypal/checkout-server-sdk from package.json and pnpm-lock.yaml.
    • Removed PayPal mocks and skipped PayPal route tests.
    • Added a focused assertion that the legacy route is no longer exposed.
const res = await app.fetch(new Request('http://localhost/paypal', {
  method: 'POST',
}), env);

expect(res.status).toBe(404);
  • Package management cleanup
    • Removed the tracked package-lock.json file so the repo only keeps pnpm lock metadata.

Impact: - Stripe remains unchanged.

  • POST /paypal is no longer available; callers now receive 404.
  • PayPal env/config is no longer required by app code.
  • Maintainers should use pnpm lockfiles only; package-lock.json is no longer tracked.

Context: paths package-lock.json, package.json, pnpm-lock.yaml, src, test

2026-06-11 · Bump vite from 6.3.4 to 6.4.2 (#141)

Bumps vite from 6.3.4 to 6.4.2.

Impact: See the linked pull request for implementation details.

Context: labels dependencies, javascript | paths package.json, pnpm-lock.yaml

2026-06-11 · new workflow for blog setup (#128)

Adds the project-notes automation workflow set for this repository (PR-note validation, markdown generation, and blog-sync flow), plus follow-up fixes from review feedback. Also reconciles merge conflicts with main so the Node CI workflow keeps running both the standard test suite and test:project-notes with a frozen lockfile install.

Impact: No direct end-user product behavior changes. Maintainers get updated CI/workflow behavior for project-notes automation and a conflict-free branch aligned with current main test infrastructure.

Context: paths .github, README.md, package.json, project-notes.config.json, test, tools

2026-06-11 · fixed tests (#129)

fixed tests

Impact: See the linked pull request for implementation details.

Context: paths .github, package.json, pnpm-lock.yaml, test, worker-configuration.d.ts

2026-03-18 · Revert "Bump wrangler from 4.35.0 to 4.75.0" (#127)

Reverts benhalverson/3dprinter-farm#126

Impact: See the linked pull request for implementation details.

Context: paths pnpm-lock.yaml

Notes

  • 2026-06-12: - Compatibility - The route intentionally preserves the old outward response contract while adapting to a different upstream API shape - Fallback behavior - Filament defaults to PLA Black until cart-level filamentId is reliably available everywhere
  • 2026-06-12: Legacy cart rows may not have filament_id; the cart layer treats those as PLA Black and backfills that default during add/merge flows to keep behavior stable across pre- and post-migration data.
  • 2026-06-12: - The V2 file workflow was duplicated across route handlers; extracting shared helpers removes the internal routing dependency without changing the external helper-route contracts - estimate cost fallback handling remains in place because the current Slant response shape is not fully uniform across callers - Spying on File.size does not survive Cloudflare Workers multipart serialisation in the vitest pool — the oversized-file test requires an actual Uint8Array allocation to cross the 100 MB threshold reliably - parseResponseDetails must attempt json() before text() because different Slant3D error responses (and test mocks) surface the body through different methods; falling back through both with a JSON.parse attempt handles all cases
  • 2026-06-12: The live Slant V2 estimate contract should be treated as data.total, not a mix of legacy top-level or alternate cost fields. The local /v2/estimate route intentionally continues to expose estimatedCost alongside total to avoid breaking downstream callers during the transition.
  • 2026-06-12: - The deleted webhook route had diverged into a TODO-only implementation with different response semantics; keeping a single mounted handler avoids silent drift between duplicate webhook paths.
  • 2026-06-11: - This repository uses pnpm, so npm lockfiles should not be committed or updated here.