Founder & Lead Engineer, RAITHub
RAITHub ships and tests production software. See QA as a Service or talk to us.
Migrate an e-commerce store as a money-correctness job: map the old schema to the new one field by field, keep every price and total as integer minor units, preserve the links between customers, orders and products, and reconcile counts and totals before and after. Run the whole migration against a copy first (a dry run), fix every mismatch, then cut over. Never migrate money from a rounded display value.
If you want the migration mapped, dry-run and reconciled for you, see how RAITHub would build this below. For the moment a migration breaks a live database, see a database migration broke production; this post is about moving store data without loss in the first place.
What goes wrong in an e-commerce data migration?
Usually one of four things, and each is quiet until a customer or your accounts notice.
| Failure | How it happens | What it costs |
|---|---|---|
| Rounded or wrong money | Prices read from a display string, or floats used for currency | Totals that no longer match orders; refunds and reconciliation pain |
| Broken relationships | Orders imported before customers; IDs remapped inconsistently | Orphan orders, orders attached to the wrong customer |
| Lost order history | Only open orders migrated; statuses or timestamps dropped | No record for support, returns, tax or repeat-customer insight |
| Duplicate or missing rows | A re-run with no idempotency, or a partial failure left half-done | Double customers, missing products, counts that do not add up |
The money failures are the worst, because an order total that no longer matches its lines is wrong forever, and you may not notice until a return or an audit. That is why money is handled as exact integers from the first step.
How do you keep prices and totals exact?
Store and move every monetary value as an integer number of minor units (cents, pence, poisha), never as a float and never from a rounded display string. A float cannot represent many decimal amounts exactly, so arithmetic drifts; an integer of minor units is exact. Read the amount from the source's own stored value, not from what the storefront showed.
// Right: prices as integer minor units, read from the source's stored value.
type MigratedLine = {
orderId: number
variantId: number
qty: number
priceMinor: number // e.g. 1299 for 12.99, an integer
}
// Convert a source decimal string to minor units without floating-point error.
function toMinor(decimal: string): number {
const [whole, frac = ''] = decimal.split('.')
const cents = (frac + '00').slice(0, 2)
return Number(whole) * 100 + Number(cents)
}
// Reconcile each migrated order: the sum of its lines must equal the stored total.
function orderBalances(lines: MigratedLine[], storedTotalMinor: number): boolean {
const sum = lines.reduce((t, l) => t + l.priceMinor * l.qty, 0)
return sum === storedTotalMinor
}
Run that balance check on every order during the dry run. An order whose lines do not sum to its stored total is a migration bug to fix before cutover, not after.
How do you keep the relationships intact?
Migrate in dependency order and remap IDs through a stable lookup, so nothing points at a row that does not exist yet. The order is almost always: customers, then products and variants, then orders, then order lines, then payments and refunds. Keep a mapping from each old ID to its new one, and resolve every foreign key through that mapping.
- Make the import idempotent. Key each imported row on a natural or source ID so a re-run updates rather than duplicates. A migration that cannot be safely re-run is a migration you cannot fix halfway.
- Preserve timestamps and statuses. Order date, paid date, fulfilment and refund states are history your support, returns and tax records depend on. Do not stamp everything with the migration date.
- Migrate closed and cancelled orders too, not just open ones. Order history is the record customers and your accounts rely on.
- Handle deleted or merged entities deliberately. Decide what happens to orders for a discontinued product or a merged customer before the run, not during it.
How do you run it safely?
As a dry run against a copy, reconciled, then a real cutover you can verify in minutes.
- Dry run against a copy. Run the full migration into a staging database with real data volume. Nothing is live, so you can run it as many times as it takes to get the counts right.
- Reconcile counts and totals. Compare source and destination: number of customers, products, orders and lines, and the sum of all order totals. Every number must match or be explained.
- Spot-check by hand. Open a sample of migrated orders and confirm the customer, the lines, the money and the status are all correct against the source.
- Freeze writes at cutover. Put the old store into a read-only window, run the final migration, then reconcile again before opening the new store.
- Keep the source read-only for a while. Do not delete the old data until the new store has run cleanly and the numbers have held.
-- Reconciliation: these pairs must match (or every difference must be explained).
SELECT 'orders' AS entity, count(*) FROM source.orders
UNION ALL
SELECT 'orders', count(*) FROM target.orders
UNION ALL
SELECT 'order_total_minor', sum(total_minor) FROM source.orders
UNION ALL
SELECT 'order_total_minor', sum(total_minor) FROM target.orders;
Buy, build or hire?
| Option | What you get | Choose this when |
|---|---|---|
| Platform migration app | Automated import between two supported platforms | You move between two mainstream platforms with a well-supported path and simple data |
| A scripted one-off migration | A mapped, dry-run, reconciled migration for your exact data | Your data is custom, or money and history must come across exactly |
| Do it with your own team | Full control, if they can map the schema and reconcile | They know both schemas and can handle minor units and relationships |
| A custom build with migration included | New store and a verified data migration done together | You are replatforming and the data move is part of the build |
How long does it take, and what is the risk?
Mapping the schema and writing the migration is usually several days to a couple of weeks depending on how custom the data is; the dry runs and reconciliation take as long as it takes to make every number match. The main risk of doing it yourself is declaring success on row counts alone while money or relationships are subtly wrong, so reconcile totals and open real orders, not just count rows. Keep the source read-only and intact until the new store has proven itself.
How RAITHub would build this
- Scope: map the old schema to the new field by field, convert money to integer minor units from the source's stored values, migrate in dependency order through a stable ID mapping, make the import idempotent, run full dry runs, and reconcile counts and totals before cutover.
- Timeline: a scoped data migration sits in the backend range of 6 to 12 weeks when it is part of a build, or a shorter fixed job on its own, agreed after the audit.
- What you receive: the migration scripts, the reconciliation reports, the dry-run results, tests in your CI, a cutover runbook, the IP assigned to you and an NDA as standard.
- Next step: a free 15-minute technical audit, then a written fixed quote.
RAITHub stores money as integer minor units and reconciles it in production: on Sundor Skin, amounts are held as integer poisha against database-enforced credit limits, with CI replaying all 76 migrations on an empty database among 530+ tests; TheSkinProof, the founder's own venture, computes prices on the server across four payment rails, with 750+ tests. If the migration is part of a replatform, read replatforming without losing SEO so URLs and rankings move too. See the eCommerce industry page, read about a custom build, or book the free 15-minute audit with your source platform and data volume.
Documentation checked on 11 October 2026.
Frequently asked questions
How do I migrate my store's data without losing anything?
Map the old schema to the new one field by field, keep money as integer minor units read from the source's stored values, migrate in dependency order through a stable ID mapping, make the import idempotent, run it against a copy first, and reconcile counts and totals before cutover. Keep the source read-only until the new store has proven itself.
Why should prices be stored as integer minor units?
Because floating-point numbers cannot represent many decimal amounts exactly, so money arithmetic drifts over time. Storing and moving every amount as an integer number of cents, pence or poisha keeps it exact, and reading from the source's stored value rather than a rounded display string avoids introducing errors during the migration.
Do I need to migrate old and cancelled orders?
Yes. Order history, including closed, cancelled and refunded orders with their timestamps and statuses, is the record your support, returns, tax and repeat-customer insight depend on. Migrating only open orders loses that history, and it is far harder to recover after the source is gone.
What order should I migrate the data in?
In dependency order so nothing points at a row that does not exist yet: customers first, then products and variants, then orders, then order lines, then payments and refunds. Keep a mapping from each old ID to its new one and resolve every foreign key through it, so relationships stay intact.
How do I verify the migration worked?
Reconcile, do not just count. Compare source and destination on the number of customers, products, orders and lines, and on the sum of all order totals, and explain every difference. Then open a sample of migrated orders by hand and confirm the customer, lines, money and status match the source exactly.
Should I run the migration straight onto the live store?
No. Run the full migration into a staging copy with real data volume first, as many times as it takes to make the numbers match. For the real cutover, freeze writes on the old store, run the migration, reconcile again, then open the new store, and keep the old data read-only until the new one holds.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.