---
title: "Tan Doanh: a test copy that always matches the live shop, push on save plus a nightly check, 0 mismatches"
url: https://phatdev.net/en/case-studies/tandoanh-staging-mirror
lang: en
updated: 2026-11-08
alternate:
  vi: https://phatdev.net/vi/case-studies/tandoanh-staging-mirror
markdown: https://phatdev.net/en/case-studies/tandoanh-staging-mirror.md
---

# Tan Doanh: a test copy that always matches the live shop, push on save plus a nightly check, 0 mismatches

> Nobody dares to try a new theme on a shop that is selling. I built a test copy of Tan Doanh and keep it in step with the real site through three one-way sync layers, with the real site read only.

- Client: Tan Doanh (computer parts and prebuilt PC shop)
- Industry: Computer parts and PC retail
- Year: 2026
- Role: Planning, architecture and operations (code written with Claude Code)
- Duration: From 7 Jul 2026, running continuously
- Stack: WordPress, WooCommerce, PHP, MariaDB, Redis, Nginx, Bash

## Key results

- ~10 s: from saving on the real site to the test copy receiving it (operations notes, not re-timed)
- 0 mismatches: across 5,139 products and 213 variations, nightly check at 03:10 on 8 Nov 2026
- 1,297 → 0: products skipped by the price and stock check, before and after matching by ID
- 3: sync layers: push on save, nightly check, manual run

## Background

Nobody dares to try a new theme on a shop that is selling right now. Break one thing and customers see it at once, and orders can be lost. So Tan Doanh has two sites: tandoanh.vn is the real one, and td.phatdev.net is the test copy, where I build and try every change before touching the real shop.

A test copy is only useful if it looks like the real one. A different price, a different stock count, or a missing order makes every check on it doubtful. So my job is to keep the test copy in step with the real site, in one direction only: the real site is read only, and the test copy never writes back.

![A product page on the Tan Doanh test copy](https://phatdev.net/uploads/case/tandoanh-staging-mirror/pdp-desktop-1440.webp)

## The problem

The reconciliation system watches 5,139 products and 213 variations (measured 8 Nov 2026). Prices and stock change all day long: staff save a product, a new order arrives, an item sells out.

Three things make this hard. The first is speed: if the test copy needs an hour to learn a new price, it is useless. The second is accuracy: one wrong product shakes trust in the whole copy. The third is safety: the test copy must not send real emails to customers, must not feed numbers into the real shop's analytics, and must not show up on Google.

## How it was done

### 1. Three layers, each covering the gaps of the one before

I did not trust a single mechanism, so I built three layers.

The push on save layer is the main one. When staff save a product, a price, stock or an order on the real site, the real site sends a small notice to the test copy, and the test copy pulls exactly that item. According to my operations notes this takes about 10 seconds (I have not re-timed every run).

The nightly check at 03:10 compares the price and stock of every item again and reloads the ones that differ.

The manual run is one command that also pulls the slower-moving things: images, customers, old orders, reviews, the home page, logs.

![Diagram of three one-way sync layers from the real site to the test copy](https://phatdev.net/uploads/case/tandoanh-staging-mirror/layers-en-1920.webp)

### 2. Why the nightly check is needed

I found out that the push layer was not enough by looking at how the real site changes prices. A bulk price change through an import tool does not make WordPress record a new edit time for the product, so mechanisms that rely on edit time miss it. The nightly check covers exactly that gap. It builds a short fingerprint from each product's price, regular price, sale price, stock, stock status and product code, on both sides, and reloads only the items whose two fingerprints differ.

### 3. Match by ID, not by product code

This was the most expensive mistake I met. At first the test copy looked up the matching product by product code (SKU). But the real site has 135 duplicated codes (271 products), and 1,297 products with no code at all (figures from 2 Aug 2026). When two different products share one code, the test copy grabbed the first one and overwrote it. The log still said success. It happened twice with the same pair of hard drives.

The fix was to bring product IDs on the test copy into line with the IDs on the real site, then match by ID. Products created only on the test copy use an ID range above two million, so they can never collide with the real site.

![Comparison of matching by product code and matching by ID](https://phatdev.net/uploads/case/tandoanh-staging-mirror/sku-vs-id-en-1920.webp)

### 4. The test copy must be harmless

The test copy has its own safeguards. Outgoing mail is blocked, including mail that goes through an outside mail service. The analytics tracking code is switched off so the real shop's numbers stay clean. The pages carry a tag asking Google not to index them, and crawler bots are refused at the web server. For customers, the test copy mirrors the whole list from the real site, and stops by itself if it finds an account that exists only on the test copy, so nothing is deleted by mistake.

## Results

The numbers here come from the system's own reconciliation log, not from a separate test.

**Products skipped by the price and stock check**

| Metric | Before | After |
| --- | --- | --- |
| Nightly check | 1297 products | 0 products |

On the nights of 7 and 8 Nov 2026 the nightly check ran at 03:10 and compared 5,139 products and 213 variations. Both nights it found 0 mismatched items.

On 3 Nov 2026 I wiped the test copy's data and rebuilt it from the real site. That covered products, orders, customers, categories, and regenerating thumbnails for 34,360 images at the sizes the new theme uses. I took a backup first, and kept the test copy's own parts such as pages, menus and theme settings.

![Product page on a phone](https://phatdev.net/uploads/case/tandoanh-staging-mirror/m-pdp-780.webp)

One thing I have not managed. According to my review of 3 Nov 2026, the push layer sends its notice only once, does not retry and does not keep a log. If a notice is lost, the item is only repaired when the nightly check runs, and that check only compares price and stock. A changed description, a replaced image or an edited category waits for the manual run.

## Lessons

Even a good sync mechanism has gaps. What is worth doing is knowing where the gap is and placing one more layer right there.

The key used to pair two records must be something that is never duplicated. A product code sounds like a natural key, but real data differs from the theory.

A log saying success does not mean it is right. The overwrite bug above lasted because everyone trusted the word "ok". Since then I check using data from both sides, not the system's own report.

And a test copy has to be built to be harmless: one forgotten mail block is one customer receiving a test email.

## Need something similar?

If you have a shop that is running and you need a safe place to test, or the data between two systems has drifted and you do not know where from, send me a few lines about the setup. I will look at how the two sides talk to each other first, then tell you plainly what is worth fixing.

- See the project page: https://phatdev.net/en/projects/tandoanh-theme.md

## More case studies

- [Tan Doanh: a custom theme, a PC build tool and installments, mobile PageSpeed from 65 to 96](https://phatdev.net/en/case-studies/tandoanh-theme-buildpc.md)
- [Tan Doanh ERP: inventory, sales, warranty and accounting in one app, now in a trial run](https://phatdev.net/en/case-studies/erp-tan-doanh.md)
- [IGA: 5,256 gemstone products on a new design, mobile speed from 61 to 99, page addresses kept](https://phatdev.net/en/case-studies/iga-website.md)
