Order history là asset đắt thứ hai của shop online sau khách hàng — chứa lifetime value, repeat buy pattern, doanh thu kê khai thuế, dữ liệu warranty, và remarketing audience. Mất order = mất luôn 4 thứ đó cùng lúc.
Bài này hướng dẫn migrate đơn từ Shopify, Haravan, Sapo sang WooCommerce: 3 chiến lược theo độ complete, schema mapping cho status code, script PHP dùng wc_create_order(), customer matching qua email normalize, và 5 verify check post-migration.
Vì sao order phức tạp gấp 3 lần product
Product chỉ có meta tĩnh: name, price, SKU, image. Order có 6 lớp dữ liệu liên kết với nhau: order ID unique, customer reference, status code đa platform, line item trỏ tới product ID đã migrate, payment record với transaction ID gateway, và shipping tracking với carrier code.
Một lỗi mapping ở bất kỳ lớp nào cũng phá orphan order: line item trỏ tới product không tồn tại, status hiển thị sai, customer link sai người. Tax compliance ở VN còn yêu cầu giữ đúng date created theo múi giờ +07.
Bảng độ phức tạp 6 thành phần order
| Thành phần | Độ phức tạp | Lý do |
|---|---|---|
| Order ID | Cao | Unique, không trùng order mới phát sinh trong quá trình migrate |
| Customer link | Cao | Match email cũ với customer ID WooCommerce — vướng email normalize case |
| Status mapping | Trung bình | Status code khác giữa 4 platform, một số state không có 1-1 |
| Line items | Cao | Phải reference product ID đã migrate đúng, SKU lookup |
| Payment record | Trung bình | Transaction ID gateway VN (VNPay, MoMo) giữ nguyên để tra cứu |
| Shipping tracking | Thấp | Carrier code + tracking number là plain text |
Bước 1 — Chọn 1 trong 3 chiến lược migrate
Không phải shop nào cũng cần migrate full order history. Chọn chiến lược theo volume order, mức độ phụ thuộc remarketing và yêu cầu compliance thuế của ngành.
Bảng so sánh 3 chiến lược
| Chiến lược | Volume coverage | Cost dev | Phù hợp |
|---|---|---|---|
| Full migrate | 100% order history | 15.000.000-30.000.000đ | Shop >500 đơn/tháng, ngành cần warranty dài hạn (điện tử, gia dụng) |
| Partial migrate | 12-24 tháng gần nhất | 5.000.000-15.000.000đ | 90% shop SME, đủ cho remarketing + warranty active |
| Reference-only | Archive read-only platform cũ | 1.000.000-3.000.000đ | Shop <100 đơn/tháng, không phụ thuộc lifetime value analytics |
3 câu hỏi quyết định chiến lược
- Shop có chính sách warranty trên 12 tháng không: nếu có (đồ điện tử, nội thất, gia dụng), bắt buộc migrate đủ order trong khoảng warranty + 6 tháng buffer để xử lý claim.
- Marketing có chạy remarketing dựa lifetime value không: nếu có (gửi voucher cho khách VIP, segment top spender), cần đủ data 12-24 tháng để compute LTV tin cậy.
- Kế toán có cần audit doanh thu năm cũ không: nếu shop kê khai thuế từ doanh thu shop online, giữ data ở read-only platform cũ thường đủ — không nhất thiết phải migrate vào WooCommerce.
Bước 2 — Export order từ platform nguồn
Mỗi platform có cách export khác. Shopify và Haravan có REST API trả về JSON đầy đủ metadata.
Sapo chủ yếu CSV qua admin. API export đầy đủ hơn 2-3 lần vì kèm custom field, fulfillment history, và metafield.
Export Shopify qua REST API với pagination
# Cần Admin API access token từ Shopify Apps → Develop apps
# Endpoint: /admin/api/2024-01/orders.json
# Export batch đầu — 250 order gần nhất:
curl -X GET "https://yourstore.myshopify.com/admin/api/2024-01/orders.json?status=any&limit=250&created_at_min=2024-01-01"
-H "X-Shopify-Access-Token: shpat_xxxxxxxxxxxx"
-o orders_batch_1.json
# Loop pagination cho shop >250 order:
PAGE_INFO=""
COUNTER=1
while true; do
RESPONSE=$(curl -sX GET "https://yourstore.myshopify.com/admin/api/2024-01/orders.json?limit=250&page_info=$PAGE_INFO"
-H "X-Shopify-Access-Token: shpat_xxxxxxxxxxxx"
-D headers.txt)
echo "$RESPONSE" > "orders_batch_$COUNTER.json"
# Parse Link header để lấy page_info cho batch sau:
PAGE_INFO=$(grep -i "link:" headers.txt | grep -oP 'page_info=K[^>]+')
[ -z "$PAGE_INFO" ] && break
COUNTER=$((COUNTER+1))
sleep 0.5 # rate limit Shopify 2 req/sec
done
# Output: N file JSON, mỗi file 250 order
Export Haravan và Sapo
- Haravan: endpoint
/com/orders.jsoncấu trúc tương tự Shopify, token lấy từ Admin → Settings → API. Pagination quapage=N&limit=250. - Sapo: default chỉ có CSV export qua Admin → Đơn hàng → Export.
Limit 5.000 row/lần. Shop lớn cần liên hệ Sapo support mở API riêng.
- WooCommerce target: nếu migrate từ WooCommerce A sang WooCommerce B, dùng plugin “Customer/Order/Coupon Export” để xuất CSV format chuẩn WooCommerce — import nhanh hơn 5 lần custom script.
Bước 3 — Schema status mapping giữa platforms
Status code khác nhau giữa Shopify, Haravan, Sapo, WooCommerce — đặc biệt vùng intermediate (đã thanh toán nhưng chưa ship).
Mapping sai khiến order hiển thị state không đúng, customer hỏi support, admin confused khi filter đơn cần xử lý.
Bảng mapping 4 platform
# Status mapping chuẩn — 4 platform về WooCommerce
Shopify Haravan Sapo WooCommerce
─────────────────────────────────────────────────────────────────────────
pending pending pending pending
authorized authorized unpaid on-hold
paid paid paid processing
fulfilled shipped shipped processing (in shipping)
delivered delivered delivered completed
cancelled cancelled cancelled cancelled
refunded refunded refunded refunded
partially_refunded partial_refund partial_refund refunded (custom meta _partial=1)
# Implementation trong migration script PHP:
function map_order_status($source_status, $platform) {
$mapping = [
'shopify' => [
'pending' => 'pending',
'authorized' => 'on-hold',
'paid' => 'processing',
'fulfilled' => 'processing',
'delivered' => 'completed',
'cancelled' => 'cancelled',
'refunded' => 'refunded',
],
'haravan' => [
'pending' => 'pending',
'paid' => 'processing',
'shipped' => 'processing',
'delivered' => 'completed',
'cancelled' => 'cancelled',
'refunded' => 'refunded',
],
'sapo' => [
'unpaid' => 'on-hold',
'paid' => 'processing',
'shipped' => 'processing',
'delivered' => 'completed',
'cancelled' => 'cancelled',
],
];
return $mapping[$platform][$source_status] ?? 'pending';
}
3 edge case status cần xử lý riêng
- Partially refunded: WooCommerce default chỉ có refunded fully — partial refund phải set status refunded + custom meta
_refund_partialvới amount, hiển thị qua hook woocommerce_admin_order_data_after_order_details. - On hold vì gateway pending: Shopify status authorized = đã capture nhưng chưa charge. Map sang on-hold của WooCommerce, không phải processing — tránh trigger workflow ship hàng tự động.
- Cancelled by customer vs cancelled by admin: WooCommerce chỉ có cancelled chung.
Nếu cần phân biệt cho analytics, add custom meta
_cancel_reason= “customer_request” hoặc “admin_decision”.
Bước 4 — Script PHP dùng wc_create_order()
WooCommerce KHÔNG có built-in import order qua CSV. Phải dùng PHP script gọi wc_create_order() API.
Cho shop trên 1.000 order, viết script chạy qua WP-CLI để tránh PHP timeout của web request.
Skeleton script migration hoàn chỉnh
// File: scripts/migrate-orders.php
// Chạy: wp eval-file scripts/migrate-orders.php
require_once __DIR__ . '/../../../../wp-load.php';
$json_files = glob(__DIR__ . '/data/orders_batch_*.json');
$success = 0;
$failed = 0;
$skipped = 0;
foreach ($json_files as $file) {
$batch = json_decode(file_get_contents($file), true);
foreach ($batch['orders'] as $src) {
try {
// Step 1 — Find/create customer (normalize email)
$email = strtolower(trim($src['email'] ?? ''));
$customer_id = find_or_create_customer($email, $src['customer']);
// Step 2 — Check duplicate (idempotent)
$existing = wc_get_orders([
'meta_key' => '_legacy_shopify_order_id',
'meta_value' => $src['id'],
'limit' => 1,
]);
if (! empty($existing)) {
$skipped++;
continue;
}
// Step 3 — Create order
$order = wc_create_order([
'customer_id' => $customer_id,
'status' => map_order_status($src['financial_status'], 'shopify'),
]);
// Step 4 — Add line items (lookup product by SKU)
foreach ($src['line_items'] as $item) {
$product = wc_get_product(wc_get_product_id_by_sku($item['sku']));
if (! $product) {
error_log("SKU {$item['sku']} not found for order {$src['id']}");
continue;
}
$order->add_product($product, $item['quantity'], [
'subtotal' => $item['price'] * $item['quantity'],
'total' => $item['price'] * $item['quantity'],
]);
}
// Step 5 — Billing/shipping address
$order->set_address([
'first_name' => $src['billing_address']['first_name'],
'last_name' => $src['billing_address']['last_name'],
'phone' => $src['billing_address']['phone'],
'address_1' => $src['billing_address']['address1'],
'city' => $src['billing_address']['city'],
'country' => 'VN',
], 'billing');
// Step 6 — Preserve original date (convert timezone UTC → +07)
$dt = new DateTime($src['created_at'], new DateTimeZone('UTC'));
$dt->setTimezone(new DateTimeZone('Asia/Ho_Chi_Minh'));
$order->set_date_created($dt->getTimestamp());
// Step 7 — Calculate + save
$order->calculate_totals();
$order->save();
// Step 8 — Link legacy ID để verify + idempotent
update_post_meta($order->get_id(), '_legacy_shopify_order_id', $src['id']);
update_post_meta($order->get_id(), '_legacy_platform', 'shopify');
$success++;
} catch (Exception $e) {
error_log("Failed order {$src['id']}: " . $e->getMessage());
$failed++;
}
}
}
echo "Done. Success: $success, Failed: $failed, Skipped: $skippedn";
// Helper: find or create customer với email normalize
function find_or_create_customer($email, $data) {
if (empty($email)) return 0; // guest order
$user = get_user_by('email', $email);
if ($user) return $user->ID;
return wc_create_new_customer($email, '', wp_generate_password(), [
'first_name' => $data['first_name'] ?? '',
'last_name' => $data['last_name'] ?? '',
]);
}
Bước 5 — Customer matching và edge case
Customer là điểm dễ duplicate nhất khi migrate.
Cùng 1 người mua hàng có thể có 2-3 email format khác nhau trong source data — gây tạo nhiều customer record cho cùng 1 người, phá lifetime value computation.
Normalize email pattern
- Lowercase + trim:
[email protected]và[email protected]là cùng 1 người. Strip whitespace đầu cuối, lowercase toàn bộ trước khi check exist. - Gmail dot variation: Gmail coi
[email protected]và[email protected]là cùng 1 inbox. Strip dấu chấm trong username phần trước @ chỉ khi domain là gmail.com. - Plus addressing:
[email protected]route về[email protected]. Strip phần+xxxtrước @ để match đúng. - Phone fallback: nếu email empty hoặc invalid (guest checkout), match theo phone đã chuẩn hoá +84 prefix. Customer mua nhiều lần guest checkout vẫn link được vào 1 record.
Bước 6 — Verify integrity 5 check
Critical step: verify mọi order migrate đúng trước khi switch DNS sang shop mới. Skip verify = rủi ro mất hàng nghìn order silently, phát hiện sau 2-3 tuần khi customer support nhận complaint.
5 check bắt buộc post-migration
- Count tổng: số order WooCommerce phải bằng số order export từ platform cũ (sau khi trừ skip duplicate). Mismatch trên 0.1% là báo động đỏ — investigate ngay.
- Total revenue match: SUM
order_totalWooCommerce vs SUM Shopify. Sai lệch trên 0.5% có nghĩa price hoặc quantity bị parse sai ở line item nào đó. - Spot check 30 order random: mở Admin → Orders → chọn 30 order random từ các giai đoạn khác nhau. Verify customer name, line items, total, shipping address khớp với screenshot platform cũ.
- Customer order count: chọn 5 customer cụ thể có nhiều order (VIP). Verify count order WooCommerce = count Shopify cho từng customer.
- Lệch = customer matching bị duplicate.
- Status distribution: chạy query
SELECT post_status, COUNT(*) FROM wp_posts WHERE post_type='shop_order' GROUP BY post_status— tỉ lệ status (pending, processing, completed) phải match Shopify. Lệch = status mapping table sai.
SQL query verify chi tiết
# Count total order migrated:
SELECT COUNT(*) FROM wp_posts
WHERE post_type = 'shop_order';
# Revenue total:
SELECT SUM(meta_value) FROM wp_postmeta pm
JOIN wp_posts p ON p.ID = pm.post_id
WHERE pm.meta_key = '_order_total'
AND p.post_type = 'shop_order'
AND p.post_status IN ('wc-processing', 'wc-completed');
# Find duplicate customer (cùng email lowercase):
SELECT LOWER(user_email), COUNT(*) c FROM wp_users
GROUP BY LOWER(user_email) HAVING c > 1;
# Order missing legacy ID (chưa link về source):
SELECT p.ID, p.post_date FROM wp_posts p
LEFT JOIN wp_postmeta pm ON pm.post_id = p.ID
AND pm.meta_key = '_legacy_shopify_order_id'
WHERE p.post_type = 'shop_order'
AND pm.meta_id IS NULL;
Query SQL kiểm duplicate customer thường gặp
# Tìm cùng phone nhưng email khác (cùng người, dedupe candidate)
SELECT meta_value AS phone, GROUP_CONCAT(user_id) AS user_ids
FROM wp_usermeta
WHERE meta_key = 'billing_phone'
AND meta_value != ''
GROUP BY meta_value
HAVING COUNT(*) > 1;
# Merge customer trùng — script PHP an toàn:
function merge_duplicate_customers($keep_id, $merge_ids) {
foreach ($merge_ids as $old_id) {
// Move order từ customer cũ sang customer giữ:
$orders = wc_get_orders([ 'customer_id' => $old_id, 'limit' => -1 ]);
foreach ($orders as $order) {
$order->set_customer_id($keep_id);
$order->save();
}
// Soft delete customer cũ (mark deleted, không xoá hẳn):
wp_delete_user($old_id, $keep_id);
}
}
Bước 6 — Payment record và refund history
Payment data là phần dễ bỏ sót khi migrate. Transaction ID gateway VN (VNPay, MoMo, ZaloPay) là khoá tra cứu khi customer khiếu nại hoặc kế toán đối soát doanh thu — mất transaction ID = không cross-check được với báo cáo gateway.
3 trường meta phải preserve cho payment
- _transaction_id: mã giao dịch gateway (VNPay TxnRef, MoMo OrderId). Lưu vào order meta khi migrate để admin click order xem ngay được mã, đối soát với báo cáo gateway end-of-month.
- _payment_method + _payment_method_title: phương thức (COD, vnpay, momo, banktransfer) và label hiển thị. Đảm bảo label match label đang dùng ở shop mới để filter Order list không bị phân mảnh.
- _paid_date: timestamp lúc charge thành công, khác với date_created (lúc tạo order). Quan trọng cho báo cáo doanh thu theo ngày charge, không phải ngày đặt.
Refund history khi shop có chính sách trả hàng
WooCommerce có refund object riêng (post_type=shop_order_refund) link về order parent. Migrate full refund history cần create refund object riêng, không phải chỉ set status order = refunded.
- Full refund: dùng
wc_create_refund()với amount = order total. Order parent tự update status thành refunded. - Partial refund: tạo refund với amount < total. Order parent giữ status processing/completed, hiển thị partial refund badge trong admin UI.
- Refund kèm restock item: refund object có flag
restock_refunded_items=trueđể Woo tự cộng lại stock — quan trọng nếu shop quản lý kho chặt.
5 lỗi phổ biến và cách fix
- Customer duplicate do email case: tạo 2 record cho cùng người. Normalize email lowercase + trim trước check exist, dedupe sau migrate qua SQL query.
- Product reference broken: SKU không tồn tại do migrate product chưa xong. Migrate product trước, audit list SKU missing bằng query trước khi chạy migrate order.
- Order date sai múi giờ: Shopify UTC, WooCommerce VN dùng +07. Convert timezone explicitly khi
set_date_created()như script trên. - Tax calculation lệch: WooCommerce auto-recalc tax theo current rate, Shopify lưu tax tại thời điểm order. Set tax explicitly từ source thay vì để Woo tự tính.
- Đơn COD đang giao bị duplicate: đơn shipper đang giao ở platform cũ — migrate xong settle về tài khoản nào. Hold migration cho đơn đang ship, settle xong ở platform cũ trước khi migrate batch cuối.
Bài liên quan trong cụm WooCommerce migration
Order migration là 1 trong 3 trụ của project chuyển platform. Bổ sung thêm các bài cùng cluster để có bức tranh đầy đủ:
- Export/import sản phẩm WooCommerce: cấu trúc CSV + variation + image bulk — migrate product CSV phải hoàn tất trước khi chạy migrate order.
- SEO redirect 301 khi migrate platform: chiến lược + implement + recovery — sau khi data đã sang, redirect URL giữ lại 95-99% link juice cho SEO.
- Migrate từ Shopify sang WooCommerce: 7 bước an toàn — workflow end-to-end, bài này là phần order trong đó.
- Migrate Haravan sang WooCommerce: API + CSV + verify — pattern tương tự áp dụng cho Haravan.
- Dịch vụ thiết kế website WooCommerce chuyên nghiệp — gói thi công shop WooCommerce trọn gói bao gồm migration.
Câu hỏi thường gặp
Có thể skip migrate order hoàn toàn không?
Có, phù hợp shop dưới 100 đơn/tháng và không có chính sách warranty dài hạn. Lưu Shopify hoặc Haravan archive read-only 12 tháng, export CSV final lưu local làm reference cho kế toán + warranty claim tra cứu manual.
Cost approach này thấp nhất, dưới 3.000.000đ. Trade-off: mất khả năng analyze lifetime value, segment customer theo spending, và customer support phải tra 2 hệ thống khi xử lý claim.
Plugin Cart2Cart vs custom script — chọn cái nào?
Cart2Cart (69-499 USD) auto handle 80% case standard nhưng không cho custom logic VN. Status mapping kiểu “đợi shipper xác nhận”, custom field như mã đơn ERP, hoặc rule customer matching bằng phone đều phải bỏ qua.
Custom script (15.000.000-30.000.000đ) flexible cho mọi case nhưng đắt và lâu (1-2 tuần dev + 1 tuần verify). Shop SME standard không có custom field — Cart2Cart đủ.
Shop có ERP/CRM tích hợp sâu hoặc workflow đặc thù — invest custom script.
Migration order mất bao lâu cho shop 10.000 order?
Phase prep (export + analyze data + viết script): 5-7 ngày. Phase chạy script trên staging: 1-2 ngày tuỳ hosting.
Phase verify integrity: 3-5 ngày spot check + fix edge case. Phase chạy production + switch DNS: 1 ngày.
Tổng 10-15 ngày calendar. Quan trọng nhất là không cắt phase verify — skip verify để rút thời gian là rủi ro phải redo toàn bộ migration nếu phát hiện lỗi systemic sau 2 tuần đã đi production.
Có giữ được tracking shipping cũ không?
Có nếu carrier code và tracking number lưu trong source data. Migrate vào WooCommerce qua plugin “Advanced Shipment Tracking” — meta key _tracking_number + _tracking_provider + _tracking_link.
Customer click tracking link cũ vẫn work nếu carrier (GHN, GHTK, J&T) không xoá data trong vòng 12 tháng.
Tracking quá 12 tháng thường bị carrier purge khỏi database tra cứu công khai. Lưu PDF screenshot trang tracking làm reference cho warranty claim ngành điện tử và gia dụng.
Cách handle đơn COD đang giao tại thời điểm migrate?
3 option khả thi. Option 1: hold toàn bộ migration cho tới khi mọi đơn ship xong — đợi 7-14 ngày.
Option 2: migrate đơn đã complete trước, giữ đơn đang ship ở platform cũ cho đến khi delivered rồi mới migrate batch cuối. Option 3: migrate full + tracking tiếp tục ở platform cũ, settle COD về tài khoản cũ.
Option 2 phổ biến nhất với shop production. Logic: customer đã có tracking link platform cũ, đổi giữa chừng confused.
Đợi delivered xong rồi migrate đơn đó vào WooCommerce với status completed, không cần update tracking nữa.
Báo giá shop WooCommerce + migration trọn gói
Migration order chiếm 40-60% effort của project chuyển platform. Việc còn lại bao gồm migrate product CSV, redirect 301 SEO, payment VN setup, theme tối ưu Core Web Vitals, và testing 2-3 tuần.
Web22 báo giá theo phạm vi cố định, scope rõ ràng từng phase.
Báo giá website bán hàng WooCommerce — bao gồm migration script + verify integrity + recovery plan nếu shop cần chuyển từ Shopify, Haravan, Sapo sang WooCommerce.


