Skip to content

fix(importer): reject used, like-new and refurbished listings at ingest - #1561

Merged
JOY (JOY) merged 4 commits into
devfrom
fix/1430-non-new-listings
Nov 1, 2026
Merged

JOY (JOY) merged 4 commits into
devfrom
fix/1430-non-new-listings

Conversation

@JOY

@JOY JOY (JOY) commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1430

Problem

rate.products stores no condition. A used, like-new or refurbished listing that keys to an existing master (exact model_key, a variant match, or a master hint) is linked to the new device. refresh_master_market_stats does not filter on condition, so the listing's lower price can become the new device's min_price and feed the deal radar.

Decision: option A of the issue (reject at ingest)

  • Options B and C both need DDL: a stored condition, or a flag that the stats function reads. The task allows code only, so this PR does option A.
  • It can be revisited as B or C later, through DOS-Me.
  • The trade-off: non-new listings are not stored at all, and they no longer seed draft masters.

Change

  • ShopeeIngestionPipeline.ingestProduct throws NonNewListingError (code: 'NON_NEW_LISTING', with condition) when the listing's title states a non-new condition.
    • It throws before anything is written, so there is no merchant, product or master write.
    • It applies with or without a master hint.
    • It is exported from @repo/crawlers.
  • Only the title decides.
    • The normalizer adds title_condition, read from the sanitized title, so control characters cannot split "Máy cũ".
    • A description hit still only sets needs_review, as before. Description boilerplate would otherwise drop new listings, for example "không bán hàng cũ" or "99% khách hàng hài lòng".
  • detectProductCondition gives fewer false "non-new" results, because they now drop a listing:
    • "2nd" means used only in "2nd hand". "AirPods Pro 2nd Gen" and "iPhone SE 2nd generation" are new.
    • A bare "98%" or "99%" (such as "Chống ồn 98%") is no longer like-new. A percentage counts only after máy, ngoại hình, hình thức, keng or pin, as well as the existing mới, đẹp and zin.
    • More trade-in offers are stripped before matching: "nhận thu máy cũ", "thu mua máy cũ", "thu cũ lên đời" and "thu/đổi máy cũ đổi/lấy máy mới".

Not in this PR

  • Rows already stored: non-new listings ingested earlier stay linked and in stock, so they can still set min_price.
    • A re-crawl now throws before the upsert, so these rows are no longer refreshed.
    • The read-only count SQL and an optional cleanup are in a runbook given to the PO. The cleanup marks the rows discontinued and refreshes the touched masters, and it is a prod write that needs JOY.
  • Rate-Services:
    • RS's queue worker special-cases only UNKEYABLE_PRODUCT_TITLE.
    • When RS vendors this change, it needs a matching case for NON_NEW_LISTING: complete the job without ingesting, count it apart, and mark a refresh job's listing discontinued.
    • Without that case, the rejection counts as a job failure and retries.
    • This is a cross-repo follow-up, reported to the PO.

Tests

  • shopee-to-supabase.test.ts:
    • Like-new without a hint, and used and refurbished with a verified hint, all throw NonNewListingError with the right condition, and write no listing.
    • So does a title with a NUL inside "Máy cũ".
    • New listings whose description reads as non-new are still written: a trade-in offer, a negation, and "99% khách hàng".
  • entity-matcher.test.ts:
    • The trade-in phrases read as new, and a real "Máy cũ" description reads as used.
    • "2nd Gen" and "2nd generation" read as new, and "2nd hand" as used.
    • "Chống ồn 98%" reads as new, and "máy 98% pin 99%" as like-new.
  • dist-exports.smoke checks the new export.
  • pnpm test (importer), tsc and eslint are clean.

Review

  • Fresh-context review:
    • P1: "2nd Gen" dropped new devices. Fixed.
    • P1: description boilerplate dropped new listings. Fixed: the check reads the title only.
    • P2: trade-in phrases were missing. Fixed.
    • P2: existing rows and the RS handling are listed above.
  • Codex round 1:
    • P1: a bare "98%" dropped new earbuds. Fixed.
    • P1: a control character slipped a used title past the check. Fixed.
  • Codex round 2: see the comment below.

🤖 Generated with Claude Code


📌 TL;DR

This PR introduces a strict ingestion guard that rejects used, like-new, or refurbished product listings to prevent incorrect min_price calculations in the deal radar. It also refines the condition detection logic to accurately distinguish between trade-in promotional offers and actual used device indicators.

🎯 Type of Change

  • 🚀 New feature
  • 🐛 Bugfix
  • 🧹 Refactor
  • ⚡ Performance
  • 📚 Documentation
  • ⚙️ CI / Configuration

🔍 Changes Walkthrough

File Summary of Changes
apps/importer/src/shopee/product-normalizer.ts Added NonNewListingError class and title_condition field to NormalizedProductCandidate. Expanded the TRADE_IN_PHRASE regex to catch more Vietnamese trade-in variations. Refined condition detection regexes to require context for percentages (e.g., "pin 99%") and to correctly identify "2nd hand".
apps/importer/src/shopee/shopee-to-supabase.ts Integrated the new guard into ShopeeIngestionPipeline.ingestProduct. The pipeline now throws NonNewListingError if the title-derived condition is not 'new', preventing any database writes for non-new items.
apps/importer/src/shopee/__tests__/entity-matcher.test.ts Added extensive test cases to verify that trade-in offers in descriptions do not misclassify new items, while explicit used/like-new indicators in titles are correctly detected.
apps/importer/src/shopee/__tests__/shopee-to-supabase.test.ts Added integration tests to ensure NonNewListingError is thrown for non-new listings (even with hints) and that no database upserts occur. Verified that description-only non-new keywords do not trigger rejection.
apps/importer/src/shopee/__tests__/dist-exports.smoke.mjs Updated smoke test to verify the export of the new NonNewListingError class.

📊 Architectural Flow

flowchart TD
    A[Shopee Product Ingest] --> B[ProductNormalizer]
    B --> C{Detect Condition}
    C -->|Title Analysis| D[title_condition]
    C -->|Title + Desc| E[condition]
    D --> F{Is title_condition 'new'?}
    F -->|No| G[Throw NonNewListingError]
    F -->|Yes| H[Proceed to Supabase Upsert]
    G --> I[Reject Listing]
    H --> J[Write to Database]
Loading

JOY (JOY) and others added 3 commits November 1, 2026 07:20
…st (#1430)

rate.products stores no condition, so a non-new listing linked to a
published master (exact model_key, variant or a master hint) fed its
lower price into the new device's min_price and the deal radar.

- ingestProduct throws NonNewListingError (code NON_NEW_LISTING) before
  anything is written when the normalizer detects a non-new condition,
  with or without a master hint (option A of #1430, no DDL).
- Trade-in offers for the buyer's old device ("nhận thu máy cũ",
  "thu cũ lên đời", "đổi máy cũ lấy máy mới") no longer read as used,
  so new listings are not dropped for them.
- The error is exported from @repo/crawlers (dist smoke).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#1430 review)

- The ingest check reads the title alone. Description boilerplate such as
  "không bán hàng cũ" or "99% khách hàng hài lòng" would drop new listings,
  so a description hit still only sets needs_review.
- "2nd" counts as used only in "2nd hand": "AirPods Pro 2nd Gen" and
  "iPhone SE 2nd generation" are new devices.
- The trade-in strip also covers "thu máy cũ đổi máy mới" and
  "thu mua máy cũ".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on (#1430 Codex)

- The normalizer sets title_condition from the sanitized title, and the
  ingest check reads it, so a control character inside "Máy cũ" no longer
  slips a used listing past the check.
- A bare "98%" or "99%" (such as "Chống ồn 98%") no longer reads as
  like-new; a percentage counts only after máy, ngoại hình, hình thức,
  keng or pin, besides the existing mới, đẹp and zin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
rate Ready Ready Preview Nov 1, 2026 12:30am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 54 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bfcdaabe-f2c4-4c42-80e4-ffa56f20ff18
Commits

Reviewing files that changed from the base of the PR and between cc57e2d and 472105c.

Files selected for processing: 5
  • apps/importer/src/shopee/__tests__/dist-exports.smoke.mjs
  • apps/importer/src/shopee/__tests__/entity-matcher.test.ts
  • apps/importer/src/shopee/__tests__/shopee-to-supabase.test.ts
  • apps/importer/src/shopee/product-normalizer.ts
  • apps/importer/src/shopee/shopee-to-supabase.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

dos[bot]
dos Bot approved these changes Nov 1, 2026

@dos dos Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⏱️ Code Review completed (5 eligible files · 11,146 chars · 4 model call(s) · 1 PR unit(s))

Whole diff with bounded context at base cc57e2d / head bff1ac7.

✅ No verified defects

1 possible issue(s) could not be grounded in or verified against the snapshot source, so they are not published.

Context investigation was bounded to the explicitly requested snapshot paths.

…Codex)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JOY

Copy link
Copy Markdown
Contributor Author

Codex round 2 (the last; the cap of 2 is reached):

  • P2: "Thu mua điện thoại cũ" in a new listing's title read as used, and dropped the listing. Fixed: the trade-in strip treats "điện thoại" like "máy". A test covers it, and "Điện thoại cũ iPhone 12" still reads as used.

The importer's pnpm test and eslint are clean.

@dos dos Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⏱️ Code Review completed (5 eligible files · 11,246 chars · 3 model call(s) · 1 PR unit(s))

Whole diff with bounded context at base cc57e2d / head 472105c.

🔍 Verified Adversarial Review Findings

🟡 IMPORTANT

  • apps/importer/src/shopee/product-normalizer.ts:160: Trade-in regex over-strips genuine used-device phrases
    • Source Side: RIGHT (head)
    • Failure Trace: detectProductCondition('iPhone 13 128GB máy cũ giá tốt') → TRADE_IN_PHRASE matches 'máy cũ' (second alternative 'thu\s*(?:mua\s*)?(?:máy|điện\sthoại)\scũ' with optional 'thu'/'mua' absent), replacing it with a space. The remaining text no longer contains 'máy cũ', so the used-device regex fails and the function returns 'new'. The ingest check (shopee-to-supabase.ts:546-548) then reads title_condition='new' and proceeds to upsert the used listing, whose lower price becomes the new device's min_price — exactly the #1430 scenario the guard is meant to prevent. The old regex /thu\scũ\sđổi\s*mới/ required the full 'thu cũ đổi mới' phrase and would not have stripped 'máy cũ' alone.
    • Evidence: apps/importer/src/shopee/product-normalizer.ts:160 at 472105cb05656dc05d7c869d20383740c094880a, apps/importer/src/shopee/shopee-to-supabase.ts:546 at 472105cb05656dc05d7c869d20383740c094880a, apps/importer/src/shopee/product-normalizer.ts:190 at 472105cb05656dc05d7c869d20383740c094880a

Context investigation was bounded to the explicitly requested snapshot paths.


/** 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 IMPORTANT: Trade-in regex over-strips genuine used-device phrases

Failure Trace:
detectProductCondition('iPhone 13 128GB máy cũ giá tốt') → TRADE_IN_PHRASE matches 'máy cũ' (second alternative 'thu\s*(?:mua\s*)?(?:máy|điện\sthoại)\scũ' with optional 'thu'/'mua' absent), replacing it with a space. The remaining text no longer contains 'máy cũ', so the used-device regex fails and the function returns 'new'. The ingest check (shopee-to-supabase.ts:546-548) then reads title_condition='new' and proceeds to upsert the used listing, whose lower price becomes the new device's min_price — exactly the #1430 scenario the guard is meant to prevent. The old regex /thu\scũ\sđổi\s*mới/ required the full 'thu cũ đổi mới' phrase and would not have stripped 'máy cũ' alone.

@JOY
JOY (JOY) merged commit 2877e90 into dev Nov 1, 2026
15 checks passed
@JOY
JOY (JOY) deleted the fix/1430-non-new-listings branch November 1, 2026 00:37

This branch was successfully deployed

1 active deployment
Preview — 472105cb Deployed Nov 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

importer: used and like-new listings link to the new-device master and feed its min_price

1 participant