Building a Spreadsheet-Driven Buyback Platform

TL;DR: This is a device-buyback service whose commercial catalogue changes in a client-maintained XLSX workbook. The application keeps that workbook as the source of truth, turns it into a validated catalogue of products and conditions, presents each offer in an interface people can actually use, and creates orders from prices locked in PostgreSQL. The stack is Nuxt 3, NestJS, PostgreSQL, Prisma, pnpm workspaces, Docker Compose, and a deliberately strict spreadsheet-import pipeline.

The service is a price catalogue with operational consequences

The service buys whole phones, screens, components, and bulk lots. A customer chooses an item and its condition, receives an estimated buyback value, then sends the hardware for inspection.

That sounds like ordinary ecommerce until the catalogue is examined closely. A model can have several mutually exclusive conditions, each with a different price. Phone conditions, screen defects, iCloud states, components, and bulk units do not share the same vocabulary. The catalogue also changes as the operator updates commercial rates.

The hard requirement was simple: the customer-facing catalogue had to reflect the workbook the business already uses, without asking the team to maintain the same data in two places.

XLSX is the source of truth

The import flow treats a raw client workbook as an input contract rather than a file to massage manually:

XLSX upload
  -> sheet normalization
  -> price parsing
  -> catalogue validation
  -> impact preview
  -> atomic PostgreSQL update
  -> storefront catalogue

The NestJS sync module accepts both the canonical sheets used internally and the French-named sheets maintained by the client. Normalization produces four catalogue families: phones, screens, parts, and bulk buyback. The parser then emits one explicit price record per product-condition pair.

This is deliberately fail-closed. An unknown worksheet containing price-looking data stops the import instead of being silently ignored. Duplicate product identities, unsupported condition labels, contradictory prices, missing part categories, and invalid values are rejected before anything changes in the catalogue.

The admin interface exposes the same boundary to the operator. It previews product and condition counts, shows the products that would be deactivated, and requires the uploaded file to match the SHA-256 hash that was previewed before it can be applied.

Synchronization is an atomic replacement, not a best-effort patch

Each imported product is identified by a stable source key. The importer creates or updates active products and their variations inside a PostgreSQL transaction. It takes an advisory transaction lock so two synchronizations cannot interleave.

Products and conditions that disappeared from the workbook are deactivated in the same transaction. They are not hard-deleted, which preserves order history while ensuring the public catalogue matches the current workbook. The sync job records created, updated, skipped, deactivated-product, deactivated-variation, and error counts for the admin result screen.

That gives the business one controlled operation with a clear outcome instead of a series of partial writes and a catalogue that may or may not have caught up.

A catalogue UI built around choice, not variant clutter

The raw data model stores products separately from their priced variations. The UI should not expose that implementation detail as a stack of pills or a long list of nearly identical cards.

For desktop, the storefront groups compatible conditions into compact selection matrices. A product occupies one row, each condition has a labelled price cell, and the current estimate updates with quantity. Conditions are grouped by family, such as iCloud state, phone condition, screen condition, part condition, or bulk unit. Matrices are capped at four condition columns and split into chunks when necessary, which keeps dense catalogues readable.

On smaller screens, the same data is rendered as focused product cards. The customer still selects a condition and quantity, but without needing to scan a desktop-width table.

The catalogue API filters active products and active variations by category, brand, and product family. Categories, brands, product labels, and condition availability come from the import instead of an independently curated storefront taxonomy.

A quote is protected at checkout

Browser totals are useful for display and unsuitable as a source of truth. When an order is created, the API receives variation IDs and quantity, then resolves prices from PostgreSQL inside a transaction:

variation IDs
  -> sort IDs consistently
  -> SELECT ... FOR UPDATE
  -> read current database prices
  -> snapshot each order item
  -> create the order

The consistent lock order reduces deadlock risk. The resulting order items keep the product name, selected condition, price at order time, quantity, and later inspection fields. A sync that runs after checkout cannot rewrite the offer attached to an existing order.

Checkout requests use idempotency keys so a retried request cannot create duplicate orders. The order state machine follows the real workflow: pending, received, testing, graded, paid, or cancelled. This matters because a buyback offer can be adjusted after the physical device has been inspected.

Payout destinations are stored separately from the order. The sensitive value is encrypted with AES-256-GCM, an HMAC fingerprint prevents duplicate storage for the same customer, and the back-office response only exposes a masked value.

The stack

Area Choice Responsibility
Storefront Nuxt 3, Vue 3, TypeScript, Pinia SSR catalogue pages, responsive selection flows, cart, account, and admin UI
Experience layer Three.js and Lucide Immersive landing presentation and familiar UI controls without adding custom SVG plumbing
API NestJS Auth, catalogue, XLSX synchronization, orders, audit-oriented admin operations, validation, and rate limits
Data PostgreSQL and Prisma Relational catalogue, transaction boundaries, migrations, order snapshots, and concurrency control
Workbook handling SheetJS xlsx Raw workbook ingestion, normalization, parsing, and canonical export
Shared contracts pnpm workspace package Types shared between the Nuxt client and NestJS API
Operations Docker Compose Reproducible local, staging, and production service topology

The monorepo has three clear boundaries: apps/web for the Nuxt application, apps/api for NestJS and Prisma, and packages/shared for contracts that both sides consume. The repository also carries parser, synchronization, order-state, checkout, and Playwright coverage so catalogue changes can be verified against the workflows they affect.

The useful outcome

The business keeps the spreadsheet that fits its daily pricing work. The application turns that input into a reviewable, validated, and transactional catalogue update. Customers receive a clearer way to select the precise condition of their hardware, and operations retain an order history that is insulated from later price changes.

That is the service: operational data stays with the people who own it, while the platform makes it reliable enough to drive a public catalogue and a buyback workflow.