Skip to content

feat: Add Warehouse#12

Open
jeninh wants to merge 199 commits intomainfrom
warehouse-dev
Open

feat: Add Warehouse#12
jeninh wants to merge 199 commits intomainfrom
warehouse-dev

Conversation

@jeninh
Copy link
Copy Markdown
Collaborator

@jeninh jeninh commented Feb 26, 2026

Summary
Adds a complete warehouse management system to Resolution, allowing ambassadors to place orders for physical inventory and admins to fulfill them with integrated multi-carrier shipping label generation.

Features
Warehouse Storefront (Ambassadors - /app/warehouse)
Browse inventory items grouped by category
Multi-step order wizard with address entry, item selection, and real-time shipping rate comparison
Order templates for quick reordering
Batch ordering via CSV upload with auto-field-mapping and a linked Google Sheets template
Tag input with chips and autocomplete for order filtering
Orders and batches scoped to the logged-in user

Warehouse Backend (Admins - /app/warehouse-backend)
Full CRUD for inventory categories and items (photo uploads via Hack Club CDN, dimensions, HS codes, sizing options)
Inventory tracking with automatic quantity subtraction on order placement (prevents negative stock)
Fulfillment panel with label generation, printing, and reprint support

Shipping Integration
Canada Post — contract and non-contract shipment support, state/province → 2-letter code resolution, country name → ISO code mapping, 8.5×11 → 4×6 label cropping
Chit Chats — rate quoting and shipment creation with HTS codes and manufacturer details; automatic fallback when Canada Post fails internationally
Theseus — lettermail label support
Zonos — landed cost integration for US-bound shipments
Cheapest-rate auto-selection across all carriers for batch shipping
Flat package type with envelope size snapping (4×6 / 6×9)

Printing
QZ Tray integration for direct thermal label printing (4×6)
Combined label + packing slip print button
Reprint support for previously generated labels

Billing
HCB (Hack Club Bank) billing integration for warehouse orders

Infrastructure
4 new Drizzle migrations (orders, templates/batches, label tracking, HS codes)
drizzle-kit push at container startup for auto-migrations
entrypoint.sh for container orchestration
removeAdmin.mjs CLI script
Staging mode to bypass OAuth for local/staging testing
Body size limit increased to 10MB for image uploads

UI
Phantom Sans font and clean white UI on warehouse, admin, and ambassador pages
Shipping cost estimate disclaimers on order and batch pages

Modified
Extended DB schema with warehouse orders, templates, batches, label/tracking fields
Updated admin page with warehouse backend link and inventory management
Updated validation schemas and tests for new order/item types
CI triggers on warehouse-dev branch

P.S. DO NOT MERGE YET, I NEED TO ADD ENV VARS.

This is to estimate prices for warehouse. Adds POST /api/shipping-rates endpoint that allows ambassadors to get Canada Post shipping rate quotes by providing destination address, package type (envelope/box), dimensions (inches), and weight (grams).
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
resolution Ready Ready Preview, Comment Apr 24, 2026 2:37pm

- Warehouse: stripped admin controls from items/orders pages, now read-only for ambassadors and admins
- Warehouse-Backend: new admin-only route with full CRUD for items, orders, categories, and tags
- Fix ArrayBuffer type error in utils.ts by wrapping in Uint8Array
- Remove unused CSS selectors (.clouds, .success-icon, h2)
- Add ARIA roles and tabindex to modal overlays/dialogs
- Suppress intentional state_referenced_locally warnings
- Use \ for reactive ref page pathway info
jeninh added 2 commits April 13, 2026 20:36
Request 4x6 label format directly from the API for both contract and
non-contract shipments, eliminating the unreliable client-side PDF crop.
Removes pdf-lib dependency from this code path.
…ipments

Chit Chats API rejects province_code for US (and other non-Canadian)
destinations — the correct field is state_code. Now conditionally sends
province_code for CA and state_code for all other countries.
Displays in-stock counts in search results and the items table, disables
out-of-stock items in search, highlights over-quantity rows with inline
errors, and prevents advancing past step 2 when requested qty exceeds stock.
state_code is also rejected — Chit Chats only accepts province_code for
Canadian destinations; omit the field for all other countries.
Chit Chats uses province_code for both US states and Canadian provinces
but rejects the field for non-North American destinations.
Shows a select with all 50 states + DC (acronym only) when country is US,
falls back to a text input for other countries. Resets state/province when
country changes to avoid carrying over invalid values.
…dation

Both JSON parse failures and Zod schema validation failures now log the
raw input, error details, and userId to the server console so failures
are visible in logs. Error messages returned to the client now include
the specific reason rather than the generic "Invalid items data".

Also adds Zod schema validation to order-templates (previously only did
raw JSON.parse with no type checking).
Seeds the access token from HCB_ACCESS_TOKEN env var on first use
(matching how hermes does it), but now adds a 401-retry path that
clears the cache and does a proper refresh_token OAuth grant before
retrying. This handles stale tokens from redeploys without consuming
the refresh token on every cold start.
Route orders to lettermail, 6x10" bubble mailer, or the smallest-fitting
stocked box (6x4x4, 8x6x4, 10x8x6, 12x10x4, 14x10x8) based on combined
item dimensions and weight, rather than aggregating into a single
synthetic package. Quote rates against the real container and persist
the choice on the order so carrier calls and the packing slip match.

Also add a $1 CAD handling fee to Chit Chats rate quotes, mirroring
the existing $2 fee on Canada Post parcel rates.
@jeninh
Copy link
Copy Markdown
Collaborator Author

jeninh commented Apr 22, 2026

Review of #12 — 63 files, 10.7k additions.

Scope is enormous and the feature set is impressive, but there are several blocking issues (mostly around migrations, auth, and deploy behaviour) that need to be resolved before merge. Also the branch is currently CONFLICTING against main, which ties into the migration problems below.

🚨 Blocking

1. Migration history is broken and destructive

Main has 0000 / 0001 / 0002_luxuriant_maria_hill / 0003_misty_week_prize_image. This branch:

  • Adds a second 0002_… (0002_add_package_type_and_orders.sql) — duplicate numeric prefix, drizzle relies on unique idx ordering.
  • Deletes 0003_misty_week_prize_image.sql (the prize_image_url column addition). That migration is not re-added anywhere — merging this will silently regress a live column on main. Rebase and restore it.
  • Adds 0003_add_templates_and_batches / 0004_add_label_tracking_fields / 0005_add_hs_code / 0006_add_packaging_columns but drizzle/meta/_journal.json only tracks through 0003_add_templates_and_batches. 0004 / 0005 / 0006 are not in the journal, so drizzle-kit migrate will never run them.
  • Several schema fields have no DDL at all: warehouse_order.fulfillment_id (generated identity), estimated_duties_cents, estimated_service_code, warehouse_category, warehouse_order_tag, all the warehouse_item_* indexes.

This is only "working" in staging because of issue #2 below.

2. entrypoint.sh runs drizzle-kit push on every boot and silences failures

echo "" | npx drizzle-kit push --verbose 2>&1 || echo "WARNING: drizzle-kit push failed..."
exec node build
  • push diffs schema.ts against live DB and applies whatever it thinks is needed — this can DROP columns, including the prize_image_url column that this PR's schema no longer declares. First deploy after merge will destroy data on production.
  • Swallowing the failure (|| echo WARNING) means the app boots even if the schema push is half-applied. That is a recipe for inconsistent state across replicas.
  • drizzle-kit push at runtime also requires elevated DB privileges the app role should not normally have.

Please switch to drizzle-kit migrate against committed migration files, fix the migration history first, and fail the container on migration errors.

3. STAGING_MODE auto-creates an admin account

src/routes/api/auth/login/+server.ts — when STAGING_MODE=true, any request to /api/auth/login creates a user staging-admin-123 with isAdmin: true and gives the caller a valid session. If this env var ever leaks into prod (typo in Coolify, copy-pasted env from staging, env-var precedence bug), it is a one-request full takeover. At minimum:

  • Refuse to start if STAGING_MODE=true and NODE_ENV=production.
  • Do not make the staging user an admin — make it a regular participant and require real admin promotion.

4. warehouse_order.fulfillment_id not in any migration

Schema declares generatedAlwaysAsIdentity() for fulfillment IDs, but no migration creates it. drizzle-kit push will add it, but on existing rows PostgreSQL will fail because generated-always-as-identity columns cannot be back-filled implicitly. The labels code reads order.fulfillmentId so any order inserted before this column exists will also break printing.

⚠️ High

5. Authz gaps on batch actions

src/routes/app/warehouse/batches/+page.server.ts:

  • createBatch, mapFields, processBatch, calculateBatch all check locals.user but NOT ambassadorPathway. Any logged-in participant can POST and create a batch with up to 5MB of CSV data stored in the DB.
  • processBatch inserts warehouseOrder rows under the attacker's user.id and decrements inventory. Combine with unlimited template visibility (see below) and this is an inventory-griefing vector.
  • Same pattern in order-templates/+page.server.ts:createTemplate and deleteTemplate — the load() gates but the actions only partially gate.

Fix: lift the ambassador check into a helper and call it from every action, or gate at warehouse/+layout.server.ts (doesn't exist yet — add one).

6. HCB refresh-token rotation not persisted

src/lib/server/hcb.ts stores cachedRefreshToken in a module-level variable. If the OAuth server rotates the refresh token (common practice), the rotated token lives only until the process restarts — next boot reuses the now-invalid HCB_REFRESH_TOKEN from env and all billing silently fails. Either persist the rotated token (DB, secret store) or confirm HCB does not rotate.

Also: billing failure in warehouse/orders/new/+page.server.ts:217 is logged but the order is still committed ("TODO: flag order for manual billing review" is left in code). This is a real dollar-leakage path in production — either surface the failure to the user, or add the flag-for-review mechanism before shipping.

7. get-label endpoint fetches arbitrary URLs

src/routes/api/fulfillment/get-label/+server.ts:107 — if order.labelUrl is an external URL (it always is for non-data: URLs), the server fetches it. Protocol is checked but the host is not. If an attacker can create orders via the ambassador flow (they can) and later get a label regenerated with a crafted URL, SSRF is possible. The labelUrl is normally set by this same endpoint, so the exposure is small, but add a host allowlist (canadapost.ca, chitchats.com, mail.hackclub.com).

8. Existing stock check is not the transaction's stock check

warehouse/orders/new/+page.server.ts:105-119 checks stock BEFORE the transaction, then re-checks inside the transaction via the gte guard on the UPDATE. Good. But the outer check's error message ("N available, M requested") uses the stale pre-transaction number — fine for UX, but the inner failure throws Error('Insufficient stock (concurrent update)') with no item context. When this actually fires under contention, the user sees an unhelpful generic message and the order silently does not exist.

Medium

9. drizzle-kit in production image

Dockerfile installs drizzle-kit@0.31.8 into the production image solely for entrypoint.sh. If you do move to drizzle-kit migrate, a dedicated migrator step (init container / pre-deploy job) is cleaner and reduces the runtime image's attack surface.

10. CI vs CLAUDE.md mismatch

CLAUDE.md documents bun install --frozen-lockfile but .github/workflows/ci.yml uses npm ci, and both bun.lock (+804 lines) and package-lock.json (+122 lines) are committed. Pick one lockfile — drift between the two has already bitten the Vercel build (Deployment has failed).

11. orders/new action tolerates loose typing

+page.server.ts:52-73 — all fields are extracted via formData.get(x) as string. If the field is missing, as string gives null at runtime, which then fails downstream with a less helpful error. Also parseInt(estimatedShippingCents) and parseFloat(packagingLengthInRaw) silently produce NaN which then becomes null via the truthy coercion on line 139. Replace with a Zod schema like the rest of the codebase.

12. canada-post.ts XML injection surface

buildDestinationXml / buildCreateShipmentXml — most fields are wrapped in escapeXml, but postal-code in the CA branch uses raw interpolation:

<postal-code>${(postalCode ?? '').replace(/\s/g, '').toUpperCase()}</postal-code>

Postal codes should not contain XML specials, but validation is not enforced. Wrap it in escapeXml for consistency.

13. chit-chats.ts auth header

Authorization: accessToken (no Bearer prefix). Confirm this matches Chit Chats' actual API contract — if they require Bearer, every call 401s.

14. PlatformBackground.svelte deleted

The -8 lines diff for this file effectively deletes the component. Confirm it is not imported anywhere before merge.

Nits

  • utils.ts::arrayBufferToBase64 is three lines and only used by canada-post.ts / chit-chats.ts / get-label/+server.ts. Fine as-is.
  • canada-post.ts lettermail prices are hardcoded in code; add a TODO/comment pointing to where the rate card lives so updates do not silently drift.
  • exchange-rate.ts returns cachedRate on line 31 without null-narrowing — TS is permissive here but an explicit cachedRate! would be clearer.
  • packaging.test.ts is good — solid coverage. Nice.
  • removeAdmin.mjs mentioned in the PR description but not present in the tree.
  • .env.example:3STAGING_MODE=false is reasonable but please add a comment: "NEVER set to true in production".
  • Dockerfile copies drizzle.config.ts and schema.ts twice (lines 35-36 + 44-45). Harmless but dead.

Summary

The core feature is solid and well-structured, especially packaging selection and the transaction-guarded stock decrements. But the migration state + drizzle-kit push-on-boot + dropped 0003_misty_week_prize_image combination means merging as-is will corrupt production schema. Plus the staging bypass that auto-issues admin sessions is too easy to misconfigure.

Not approving. Please address #1#4 as hard blockers, #5#8 before merge, and consider the rest.

@jeninh
Copy link
Copy Markdown
Collaborator Author

jeninh commented Apr 22, 2026

Additional Review Findings (not yet covered)

1. 🚨 Critical: Client-controlled shipping cost used for HCB billing

File: \src/routes/app/warehouse/orders/new/+page.server.ts\ (lines 65–201)

\estimatedShippingCents\ is read directly from the client's form data and passed straight into \createHcbTransfer\ to charge the ambassador's org. An attacker can intercept the request and set it to \

jeninh added 2 commits April 22, 2026 10:04
…tion

Consolidated response to review feedback on the warehouse PR:

Migrations
- Restored 0003_misty_week_prize_image (was dropped on this branch)
- Added 0004_left_pretty_boy from main (is_submissions_open)
- Renumbered conflicting 0002_add_package_type_and_orders -> 0005
  and downstream migrations 0003/0004/0005/0006 -> 0006/0007/0008/0009
- Added 0010_warehouse_indexes_and_tags with the schema fields that had
  no DDL anywhere: fulfillment_id, estimated_duties_cents,
  estimated_service_code, warehouse_order_tag, indexes
- Added warehouse_category + warehouse_item CREATE TABLE inside 0005
  (the 0002_violet_nighthawk that originally added them was never
  committed; only its journal entry was)
- Made every new migration statement idempotent (IF NOT EXISTS, DO $$)
  so staging DBs that already have the schema don't blow up
- Regenerated _journal.json to include every migration in order
- Reverted payoutStatusEnum from CANCELLED back to CANCELED to match
  the live 0000_curly_raza enum value on main

Deploy / Dockerfile
- entrypoint.sh now runs `drizzle-kit migrate` against committed
  migrations instead of `drizzle-kit push` (which was silently dropping
  columns and swallowing failures)
- Removed duplicate COPY lines and the second drizzle-kit install from
  Dockerfile

Auth
- STAGING_MODE login bypass refuses to run when NODE_ENV=production
- Staging user is no longer auto-provisioned as admin
- .env.example warns against setting STAGING_MODE=true in prod

Authz / SSRF
- guardAdminOrAmbassador helper; every action in warehouse/batches and
  warehouse/order-templates now goes through it so participants can't
  create batches or templates
- get-label now refuses to fetch label URLs outside an allowlist of
  canadapost.ca / chitchats.com / mail.hackclub.com hosts

Billing
- Added billing_status + billing_failure_reason columns to warehouse_order
- HCB transfer failure now writes billing_status='FAILED' on the order
  instead of silently continuing, so admins can reconcile
- Bounded client-submitted estimatedShippingCents via Zod to prevent a
  hostile client from inflating the HCB charge

XML / misc
- escapeXml now wraps every postal-code interpolation in canada-post.ts
  (not just the non-CA branch)
- Added commentary on sizingChoice, order templates, batches, lettermail
  rate card, chit-chats auth header contract
- Removed committed bun.lock; CLAUDE.md updated to match the
  npm-ci-based CI/Docker flow
- Concurrent-update stock error now includes the item name
Reconcile warehouse-dev with main's bun-based build:
- Adopt bun.lock + bun-based Dockerfile from main (drop package-lock.json)
- Keep entrypoint.sh's drizzle-kit migrate safety fix, now invoked via bunx
- Drop duplicate prizeImageUrl schema declaration (main already had it)
- Keep main's non-svelte-ignore version of the ambassador week editor
- Include warehouse migrations 0005-0010 after main's 0003/0004
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

warehouse

2 participants