From 5eefd840fa20c21ecee1cd32bdf3c36177736c89 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:41:01 +0700 Subject: [PATCH 1/2] fix(importer): trade-in titles in any order stay new (#1430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit detectProductCondition read new-device titles such as "Trợ giá lên đời máy cũ", "Lên đời máy cũ trợ giá" or "Hỗ trợ đổi máy cũ" as used, so ingest threw NON_NEW_LISTING on new listings. - The trade-in strip now takes a trade-in verb (thu, thu mua, đổi, lên đời, trợ giá, hỗ trợ) right before "(máy|điện thoại|đt) cũ", with a few filler words allowed, or "cũ" right before "lên đời" or "đổi/lấy/sang mới". - Fixtures: nine trade-in titles stay new, and five real used titles stay used. Refs #1430 Co-Authored-By: Claude Opus 5.5 --- .../shopee/__tests__/entity-matcher.test.ts | 24 +++++++++++++++++++ .../importer/src/shopee/product-normalizer.ts | 19 ++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/importer/src/shopee/__tests__/entity-matcher.test.ts b/apps/importer/src/shopee/__tests__/entity-matcher.test.ts index 55785b15..05054f53 100644 --- a/apps/importer/src/shopee/__tests__/entity-matcher.test.ts +++ b/apps/importer/src/shopee/__tests__/entity-matcher.test.ts @@ -280,6 +280,30 @@ async function runEntityMatcherSuite() { detectProductCondition('Điện thoại Samsung Galaxy S24 Ultra 256GB', 'Máy cũ, pin 90%'), 'used' ); + // A new device's title offering a trade-in in any order stays new (DOS/Rate#1430). + for (const title of [ + 'Samsung Galaxy S24 Ultra 256GB - Trợ giá lên đời máy cũ', + 'Samsung Galaxy S24 Ultra 256GB Lên đời máy cũ trợ giá 2 triệu', + 'Samsung Galaxy S24 Ultra 256GB Hỗ trợ đổi máy cũ', + 'iPhone 16 Pro Max 256GB Chính hãng VN/A - Đổi máy cũ', + 'iPhone 16 Pro Max 256GB Trợ giá máy cũ 1 triệu', + 'iPhone 16 Pro Max 256GB Thu cũ lên đời', + 'iPhone 16 Pro Max 256GB Máy cũ lên đời trợ giá', + 'iPhone 16 Pro Max 256GB Lên đời từ điện thoại cũ', + 'iPhone 16 Pro Max 256GB Thu đt cũ đổi mới', + ]) { + assert.equal(detectProductCondition(title), 'new', `${title} is a new device`); + } + // A real used listing stays used. + for (const title of [ + 'Điện thoại iPhone 12 Pro 128GB máy cũ đẹp', + 'iPhone 11 64GB Máy cũ giá rẻ bảo hành 3 tháng', + 'Samsung Galaxy S21 điện thoại cũ đã qua sử dụng', + 'Thanh lý máy cũ iPhone XS Max', + 'iPhone 13 cũ - máy cũ zin, trợ giá ship', + ]) { + assert.equal(detectProductCondition(title), 'used', `${title} is a used device`); + } for (const title of [ 'Tai nghe Apple AirPods Pro 2nd Gen USB-C Chính hãng VN/A', 'Apple iPhone SE 2nd generation 64GB', diff --git a/apps/importer/src/shopee/product-normalizer.ts b/apps/importer/src/shopee/product-normalizer.ts index 166d3b2a..8d2ad1b9 100644 --- a/apps/importer/src/shopee/product-normalizer.ts +++ b/apps/importer/src/shopee/product-normalizer.ts @@ -155,9 +155,22 @@ export class NonNewListingError extends Error { } } -/** A trade-in offer for the buyer's old device, which says nothing about the listing's own. */ -const TRADE_IN_PHRASE = - /(?:thu|đổi)\s*(?:mua\s*)?(?:(?:máy|điện\s*thoại)\s*)?cũ\s*(?:lên\s*đời|(?:lấy|đổi)\s*(?:(?:máy|điện\s*thoại)\s*)?mới)|thu\s*(?:mua\s*)?(?:máy|điện\s*thoại)\s*cũ/gi; +/** + * A trade-in offer for the buyer's old device, which says nothing about the listing's own: + * a trade-in verb right before "(máy|điện thoại) cũ" ("thu mua máy cũ", "lên đời máy cũ", + * "trợ giá máy cũ", "hỗ trợ đổi máy cũ"), or "cũ" right before "lên đời" or "đổi/lấy mới" + * ("thu cũ đổi mới", "máy cũ lên đời"). A few filler words may sit between verb and device. + */ +const OLD_DEVICE = String.raw`(?:(?:máy|điện\s*thoại|đt)\s*)?cũ`; +const TRADE_IN_PHRASE = new RegExp( + String.raw`(?:thu(?:\s*mua)?|đổi|lên\s*đời|trợ\s*giá|hỗ\s*trợ)\s*(?:(?:từ|cho|mọi|các|bằng|khi)\s*)*` + + OLD_DEVICE + + String.raw`(?:\s*(?:lên\s*đời|(?:lấy|đổi|sang)\s*(?:(?:máy|điện\s*thoại)\s*)?mới))?` + + '|' + + OLD_DEVICE + + String.raw`\s*(?:lên\s*đời|(?:lấy|đổi|sang)\s*(?:(?:máy|điện\s*thoại)\s*)?mới)`, + 'gi' +); /** * Detect listing physical condition from product title and optional description (Audit C6). From 11cd677311b5471e8ce578f208e044c2f5cbe3eb Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:43:26 +0700 Subject: [PATCH 2/2] fix(importer): keep warranty swaps used; allow amounts and hyphens (#1430 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Mới" followed by a number or a warranty term ("đổi mới 99%", "đổi mới trong 30 ngày") is not a trade-in, so those used listings stay used or like-new. - A bare "hỗ trợ" is no longer a trade-in verb; "hỗ trợ thu/đổi" still is. - "lại", "giá cao" and an amount ("2 triệu") may sit between the verb and the device, and a hyphen, colon or comma may separate the words. Refs #1430 Co-Authored-By: Claude Opus 5.5 --- .../shopee/__tests__/entity-matcher.test.ts | 9 +++++++++ apps/importer/src/shopee/product-normalizer.ts | 18 +++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/importer/src/shopee/__tests__/entity-matcher.test.ts b/apps/importer/src/shopee/__tests__/entity-matcher.test.ts index 05054f53..e6cdb7c5 100644 --- a/apps/importer/src/shopee/__tests__/entity-matcher.test.ts +++ b/apps/importer/src/shopee/__tests__/entity-matcher.test.ts @@ -291,6 +291,11 @@ async function runEntityMatcherSuite() { 'iPhone 16 Pro Max 256GB Máy cũ lên đời trợ giá', 'iPhone 16 Pro Max 256GB Lên đời từ điện thoại cũ', 'iPhone 16 Pro Max 256GB Thu đt cũ đổi mới', + 'iPhone 16 Pro Max 256GB Thu mua giá cao máy cũ', + 'iPhone 16 Pro Max 256GB Thu mua lại máy cũ', + 'iPhone 16 Pro Max 256GB Trợ giá 2 triệu cho máy cũ', + 'iPhone 16 Pro Max 256GB Lên đời - máy cũ trợ giá', + 'iPhone 16 Pro Max 256GB Lên-đời máy cũ', ]) { assert.equal(detectProductCondition(title), 'new', `${title} is a new device`); } @@ -301,9 +306,13 @@ async function runEntityMatcherSuite() { 'Samsung Galaxy S21 điện thoại cũ đã qua sử dụng', 'Thanh lý máy cũ iPhone XS Max', 'iPhone 13 cũ - máy cũ zin, trợ giá ship', + // A warranty swap is not a trade-in, and a bare "hỗ trợ" is not a trade-in verb. + 'iPhone 11 64GB máy cũ đổi mới trong 30 ngày', + 'iPhone 12 Pro hỗ trợ máy cũ', ]) { assert.equal(detectProductCondition(title), 'used', `${title} is a used device`); } + assert.equal(detectProductCondition('Điện thoại cũ đổi mới 99%'), 'like_new_99'); for (const title of [ 'Tai nghe Apple AirPods Pro 2nd Gen USB-C Chính hãng VN/A', 'Apple iPhone SE 2nd generation 64GB', diff --git a/apps/importer/src/shopee/product-normalizer.ts b/apps/importer/src/shopee/product-normalizer.ts index 8d2ad1b9..5321efaf 100644 --- a/apps/importer/src/shopee/product-normalizer.ts +++ b/apps/importer/src/shopee/product-normalizer.ts @@ -159,16 +159,24 @@ export class NonNewListingError extends Error { * A trade-in offer for the buyer's old device, which says nothing about the listing's own: * a trade-in verb right before "(máy|điện thoại) cũ" ("thu mua máy cũ", "lên đời máy cũ", * "trợ giá máy cũ", "hỗ trợ đổi máy cũ"), or "cũ" right before "lên đời" or "đổi/lấy mới" - * ("thu cũ đổi mới", "máy cũ lên đời"). A few filler words may sit between verb and device. + * ("thu cũ đổi mới", "máy cũ lên đời"). A few filler words or an amount ("2 triệu") may sit + * between verb and device, and a hyphen, colon or comma may separate the words. "Mới" is not + * a trade-in when a number or a warranty term follows it ("đổi mới 99%", "đổi mới trong 30 + * ngày"), and a bare "hỗ trợ" is not a trade-in verb ("hỗ trợ máy cũ"). */ -const OLD_DEVICE = String.raw`(?:(?:máy|điện\s*thoại|đt)\s*)?cũ`; +const SEP = String.raw`[\s\-:,]*`; +const OLD_DEVICE = String.raw`(?:(?:máy|điện\s*thoại|đt)${SEP})?cũ`; +const TRADE_IN_FILLER = String.raw`(?:(?:từ|cho|mọi|các|bằng|khi|lại|giá\s*cao|\d+(?:[.,]\d+)?\s*(?:triệu|tr|k|đ))${SEP})*`; +const TRADE_IN_TO_NEW = String.raw`(?:lên${SEP}đời|(?:lấy|đổi|sang)${SEP}(?:(?:máy|điện\s*thoại)${SEP})?mới(?!\s*(?:\d|trong|sau|nếu|khi)))`; const TRADE_IN_PHRASE = new RegExp( - String.raw`(?:thu(?:\s*mua)?|đổi|lên\s*đời|trợ\s*giá|hỗ\s*trợ)\s*(?:(?:từ|cho|mọi|các|bằng|khi)\s*)*` + + String.raw`(?:(?:hỗ\s*trợ${SEP})?(?:thu(?:\s*mua)?|đổi|lên${SEP}đời)|trợ\s*giá)${SEP}` + + TRADE_IN_FILLER + OLD_DEVICE + - String.raw`(?:\s*(?:lên\s*đời|(?:lấy|đổi|sang)\s*(?:(?:máy|điện\s*thoại)\s*)?mới))?` + + String.raw`(?:${SEP}${TRADE_IN_TO_NEW})?` + '|' + OLD_DEVICE + - String.raw`\s*(?:lên\s*đời|(?:lấy|đổi|sang)\s*(?:(?:máy|điện\s*thoại)\s*)?mới)`, + SEP + + TRADE_IN_TO_NEW, 'gi' );